Skip to content

Commit ed8cd22

Browse files
committed
compiler: Add compile:string/1,2 for compiling source code strings
Add compile:string/1,2 and compile:noenv_string/2 that compile Erlang source code from a string or binary. Unlike compile:forms, the source goes through the Erlang preprocessor (epp), supporting -define, -include, -ifdef, records, and all other preprocessor directives. The `{include_path_open, Fun}` option is passed through to epp, enabling fully in-memory compilation without file system access for include files.
1 parent 7e676a3 commit ed8cd22

2 files changed

Lines changed: 285 additions & 3 deletions

File tree

lib/compiler/src/compile.erl

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ source code.
208208
%% High-level interface.
209209
-export([file/1,file/2,noenv_file/2,format_error/1]).
210210
-export([forms/1,forms/2,noenv_forms/2]).
211+
-export([string/1,string/2,noenv_string/2]).
211212
-export([output_generated/1,noenv_output_generated/1]).
212213
-export([options/0]).
213214
-export([env_compiler_options/0]).
@@ -1047,6 +1048,51 @@ noenv_forms(Forms, Opts) when is_list(Opts) ->
10471048
noenv_forms(Forms, Opt) when is_atom(Opt) ->
10481049
noenv_forms(Forms, [Opt|?DEFAULT_OPTIONS]).
10491050

1051+
-doc #{ equiv => string(String, []) }.
1052+
-spec string(String :: unicode:chardata()) -> CompRet :: comp_ret().
1053+
1054+
string(String) -> string(String, ?DEFAULT_OPTIONS).
1055+
1056+
-doc """
1057+
Compiles an Erlang source code string.
1058+
1059+
Analogous to [`file/1`](`file/1`), but takes a `t:unicode:chardata/0` containing
1060+
Erlang source code as first argument. The source code is run through the
1061+
Erlang preprocessor (`epp`) just as when compiling a file, so directives
1062+
such as `-include`, `-define`, and `-ifdef` are supported.
1063+
1064+
Option `binary` is implicit, that is, no object code file is produced.
1065+
1066+
The `source` option can be used to set the source file name that will
1067+
appear in error messages and the `module_info/1` function. If not given,
1068+
it defaults to the module name with a `.erl` extension.
1069+
1070+
The `{include_path_open, Fun}` option can be used to provide a custom function
1071+
for opening include files (see `epp:open/1`), allowing compilation
1072+
entirely in memory without touching the file system.
1073+
1074+
```erlang
1075+
1> compile:string("-module(foo). -export([bar/0]). bar() -> ok.").
1076+
{ok,foo,<<...>>}
1077+
```
1078+
""".
1079+
-spec string(String :: unicode:chardata(), Options :: [option()] | option()) ->
1080+
CompRet :: comp_ret().
1081+
1082+
string(String, Opts) when is_list(Opts) ->
1083+
do_compile({string,String}, [binary|Opts++env_default_opts()]);
1084+
string(String, Opt) when is_atom(Opt) ->
1085+
string(String, [Opt|?DEFAULT_OPTIONS]).
1086+
1087+
-doc #{ equiv => noenv_string(String, []) }.
1088+
-spec noenv_string(String :: unicode:chardata(), Options :: [option()] | option()) ->
1089+
comp_ret().
1090+
1091+
noenv_string(String, Opts) when is_list(Opts) ->
1092+
do_compile({string,String}, [binary|Opts]);
1093+
noenv_string(String, Opt) when is_atom(Opt) ->
1094+
noenv_string(String, [Opt|?DEFAULT_OPTIONS]).
1095+
10501096
-doc """
10511097
Works like `output_generated/1`, except that the environment variable
10521098
`ERL_COMPILER_OPTIONS` is not consulted.
@@ -1301,6 +1347,14 @@ internal({forms,Forms}, Opts0) ->
13011347
strip_columns(Forms)
13021348
end,
13031349
internal_comp(Ps, NewForms, Source, "", Compile);
1350+
internal({string,String}, Opts0) ->
1351+
Bin = unicode:characters_to_binary(String),
1352+
{ok, Fd} = file:open(Bin, [ram, read, binary, cooked]),
1353+
try
1354+
do_parse_string(Fd, Opts0)
1355+
after
1356+
file:close(Fd)
1357+
end;
13041358
internal({file,File}, Opts) ->
13051359
{Ext,Ps} = passes(file, Opts),
13061360
Compile = build_compile(Opts),
@@ -2004,6 +2058,71 @@ parse_module(_Code, St) ->
20042058
Ret
20052059
end.
20062060

2061+
do_parse_string(Fd, Opts0) ->
2062+
StartLocation = case with_columns(Opts0) of
2063+
true ->
2064+
{1,1};
2065+
false ->
2066+
1
2067+
end,
2068+
case erl_features:init_parse_state(Opts0, fun erl_scan:f_reserved_word/1) of
2069+
{ok, {Features, ResWordFun}} ->
2070+
PathOpenOpt = case proplists:get_value(include_path_open, Opts0) of
2071+
undefined -> [];
2072+
PathOpenFun -> [{include_path_open, PathOpenFun}]
2073+
end,
2074+
Name = proplists:get_value(source, Opts0, "string"),
2075+
EppOpts = [{fd, Fd},
2076+
{name, Name},
2077+
{includes, ["." | inc_paths(Opts0)]},
2078+
{macros, pre_defs(Opts0)},
2079+
{default_encoding, utf8},
2080+
{location, StartLocation},
2081+
{reserved_word_fun, ResWordFun},
2082+
{features, Features},
2083+
extra |
2084+
PathOpenOpt ++
2085+
case member(check_ssa, Opts0) of
2086+
true ->
2087+
[{compiler_internal, [ssa_checks]}];
2088+
false ->
2089+
[]
2090+
end],
2091+
{ok, Epp, Extra0} = epp:open(EppOpts),
2092+
try
2093+
Forms0 = epp:parse_file(Epp),
2094+
Epp ! {get_features, self()},
2095+
UsedFtrs = receive {features, X} -> X end,
2096+
Extra = [{features, UsedFtrs} | Extra0],
2097+
Encoding = proplists:get_value(encoding, Extra),
2098+
{_, Ps} = passes(forms, Opts0),
2099+
Source = proplists:get_value(source, Opts0, source_from_forms(Forms0)),
2100+
Opts1 = proplists:delete(source, Opts0),
2101+
Compile0 = build_compile(Opts1),
2102+
St0 = metadata_add_features(UsedFtrs, Compile0),
2103+
Opts2 = [{features, UsedFtrs} | St0#compile.options],
2104+
St1 = St0#compile{encoding=Encoding, options=Opts2},
2105+
Forms = case with_columns(Opts2 ++ compile_options(Forms0)) of
2106+
true ->
2107+
Forms0;
2108+
false ->
2109+
strip_columns(Forms0)
2110+
end,
2111+
internal_comp(Ps, Forms, Source, "", St1)
2112+
after
2113+
epp:close(Epp)
2114+
end;
2115+
{error, {Mod, Reason}} ->
2116+
Source = proplists:get_value(source, Opts0, "string"),
2117+
Es = [{Source, [{none, Mod, Reason}]}],
2118+
{error, {errors, Es, []}}
2119+
end.
2120+
2121+
source_from_forms([{attribute,_,module,Mod}|_]) ->
2122+
atom_to_list(Mod) ++ ".erl";
2123+
source_from_forms([_|T]) -> source_from_forms(T);
2124+
source_from_forms([]) -> "string".
2125+
20072126
deterministic_filename(#compile{ifile=File,options=Opts}) ->
20082127
SourceName0 = proplists:get_value(source, Opts, File),
20092128
case member(deterministic, Opts) of

lib/compiler/test/compile_SUITE.erl

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
-include_lib("stdlib/include/erl_compile.hrl").
2828
-include_lib("stdlib/include/assert.hrl").
2929

30-
-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
30+
-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
3131
init_per_group/2,end_per_group/2,
3232
app_test/1,appup_test/1,bigE_roundtrip/1,
3333
debug_info/4, custom_debug_info/1, custom_compile_info/1,
34-
file_1/1, forms_2/1, module_mismatch/1, outdir/1,
34+
file_1/1, forms_2/1, string_1/1, module_mismatch/1, outdir/1,
3535
binary/1, makedep/1, cond_and_ifdef/1, listings/1, listings_big/1,
3636
other_output/1, encrypted_abstr/1,
3737
strict_record/1, utf8_atoms/1, utf8_functions/1, extra_chunks/1,
@@ -60,7 +60,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}].
6060

6161
all() ->
6262
[app_test, appup_test, bigE_roundtrip, file_1,
63-
forms_2, module_mismatch, outdir,
63+
forms_2, string_1, module_mismatch, outdir,
6464
binary, makedep, cond_and_ifdef, listings, listings_big,
6565
other_output, encrypted_abstr, tuple_calls,
6666
strict_record, utf8_atoms, utf8_functions, extra_chunks,
@@ -326,6 +326,169 @@ forms_compile_and_load(Code, Opts) ->
326326
false = code:purge(simple),
327327
ok.
328328

329+
string_1(_Config) ->
330+
%% Basic compilation from string.
331+
{ok,foo,BinFoo} = compile:string("-module(foo). -export([bar/0]). bar() -> hello."),
332+
{module,foo} = code:load_binary(foo, "foo.erl", BinFoo),
333+
hello = foo:bar(),
334+
true = code:delete(foo),
335+
false = code:purge(foo),
336+
337+
%% Binary input.
338+
{ok,binmod,BinMod} = compile:string(<<"-module(binmod). -export([g/0]). g() -> ok.">>),
339+
{module,binmod} = code:load_binary(binmod, "binmod.erl", BinMod),
340+
ok = binmod:g(),
341+
true = code:delete(binmod),
342+
false = code:purge(binmod),
343+
344+
%% Preprocessor: macros with -define.
345+
{ok,macmod,BinMac} = compile:string(
346+
"-module(macmod). -export([f/0]).\n"
347+
"-define(X, 42).\n"
348+
"f() -> ?X.\n"),
349+
{module,macmod} = code:load_binary(macmod, "macmod.erl", BinMac),
350+
42 = macmod:f(),
351+
true = code:delete(macmod),
352+
false = code:purge(macmod),
353+
354+
%% Preprocessor: records.
355+
{ok,recmod,BinRec} = compile:string(
356+
"-module(recmod). -export([new/0]).\n"
357+
"-record(point, {x = 0, y = 0}).\n"
358+
"new() -> #point{x = 1, y = 2}.\n"),
359+
{module,recmod} = code:load_binary(recmod, "recmod.erl", BinRec),
360+
{point,1,2} = recmod:new(),
361+
true = code:delete(recmod),
362+
false = code:purge(recmod),
363+
364+
%% Preprocessor: -ifdef/-endif.
365+
{ok,ifmod,BinIf} = compile:string(
366+
"-module(ifmod). -export([f/0]).\n"
367+
"-ifdef(NOTDEFINED).\n"
368+
"f() -> wrong.\n"
369+
"-else.\n"
370+
"f() -> right.\n"
371+
"-endif.\n"),
372+
{module,ifmod} = code:load_binary(ifmod, "ifmod.erl", BinIf),
373+
right = ifmod:f(),
374+
true = code:delete(ifmod),
375+
false = code:purge(ifmod),
376+
377+
%% Preprocessor: predefined macros via options.
378+
{ok,defmod,BinDef} = compile:string(
379+
"-module(defmod). -export([f/0]).\n"
380+
"f() -> ?MY_VALUE.\n",
381+
[{d,'MY_VALUE',99}]),
382+
{module,defmod} = code:load_binary(defmod, "defmod.erl", BinDef),
383+
99 = defmod:f(),
384+
true = code:delete(defmod),
385+
false = code:purge(defmod),
386+
387+
%% Preprocessor: ?MODULE.
388+
{ok,modmac,BinModMac} = compile:string(
389+
"-module(modmac). -export([name/0]).\n"
390+
"name() -> ?MODULE.\n"),
391+
{module,modmac} = code:load_binary(modmac, "modmac.erl", BinModMac),
392+
modmac = modmac:name(),
393+
true = code:delete(modmac),
394+
false = code:purge(modmac),
395+
396+
%% Source option.
397+
{ok,srcmod,BinSrc} = compile:string(
398+
"-module(srcmod). -export([f/0]). f() -> ok.",
399+
[{source,"my_source.erl"}]),
400+
{module,srcmod} = code:load_binary(srcmod, "srcmod.erl", BinSrc),
401+
Info = srcmod:module_info(compile),
402+
SrcInfo = proplists:get_value(source, Info),
403+
true = lists:suffix("my_source.erl", SrcInfo),
404+
true = code:delete(srcmod),
405+
false = code:purge(srcmod),
406+
407+
%% Syntax error returns error.
408+
error = compile:string("-module(bad). f( ->"),
409+
410+
%% Syntax error with return_errors option.
411+
{error,[{"string",[{_,_,_}|_]}],_} =
412+
compile:string("-module(bad). f( ->", [return_errors]),
413+
414+
%% Source option affects error filenames.
415+
{error,[{"bad.erl",[{_,_,_}|_]}],_} =
416+
compile:string("-module(bad). f( ->",
417+
[return_errors, {source, "bad.erl"}]),
418+
419+
%% Cover: option not in a list (undocumented feature).
420+
{ok,smod,_} = compile:string("-module(smod). -export([f/0]). f() -> ok.", binary),
421+
422+
%% noenv_string/2.
423+
{ok,nmod,_} = compile:noenv_string(
424+
"-module(nmod). -export([f/0]). f() -> ok.", []),
425+
426+
%% include_path_open: include from in-memory files.
427+
Headers = #{
428+
"my_header.hrl" =>
429+
<<"-record(point, {x, y}).\n"
430+
"-define(ORIGIN, #point{x=0, y=0}).\n">>
431+
},
432+
PathOpen = fun(_Path, Name, _Modes) ->
433+
case maps:find(Name, Headers) of
434+
{ok, Content} ->
435+
{ok, Fd} = file:open(Content, [ram, read, binary, cooked]),
436+
{ok, Fd, Name};
437+
error ->
438+
{error, enoent}
439+
end
440+
end,
441+
{ok,incmod,BinInc} = compile:string(
442+
"-module(incmod).\n"
443+
"-include(\"my_header.hrl\").\n"
444+
"-export([f/0]).\n"
445+
"f() -> ?ORIGIN.\n",
446+
[{include_path_open, PathOpen}]),
447+
{module,incmod} = code:load_binary(incmod, "incmod.erl", BinInc),
448+
{point,0,0} = incmod:f(),
449+
true = code:delete(incmod),
450+
false = code:purge(incmod),
451+
452+
%% include_path_open: nested includes from in-memory files.
453+
Headers2 = #{
454+
"types.hrl" =>
455+
<<"-type my_int() :: integer().\n">>,
456+
"all.hrl" =>
457+
<<"-include(\"types.hrl\").\n"
458+
"-define(DEFAULT, 0).\n">>
459+
},
460+
PathOpen2 = fun(_Path, Name, _Modes) ->
461+
case maps:find(Name, Headers2) of
462+
{ok, Content} ->
463+
{ok, Fd} = file:open(Content, [ram, read, binary, cooked]),
464+
{ok, Fd, Name};
465+
error ->
466+
{error, enoent}
467+
end
468+
end,
469+
{ok,nestmod,BinNest} = compile:string(
470+
"-module(nestmod).\n"
471+
"-include(\"all.hrl\").\n"
472+
"-export([f/0]).\n"
473+
"-spec f() -> my_int().\n"
474+
"f() -> ?DEFAULT.\n",
475+
[{include_path_open, PathOpen2}]),
476+
{module,nestmod} = code:load_binary(nestmod, "nestmod.erl", BinNest),
477+
0 = nestmod:f(),
478+
true = code:delete(nestmod),
479+
false = code:purge(nestmod),
480+
481+
%% include_path_open: missing include returns error.
482+
PathOpenNone = fun(_Path, _Name, _Modes) -> {error, enoent} end,
483+
{error, _, _} = compile:string(
484+
"-module(missinc).\n"
485+
"-include(\"nonexistent.hrl\").\n"
486+
"-export([f/0]).\n"
487+
"f() -> ok.\n",
488+
[{include_path_open, PathOpenNone}, return_errors]),
489+
490+
ok.
491+
329492
module_mismatch(Config) when is_list(Config) ->
330493
DataDir = proplists:get_value(data_dir, Config),
331494
File = filename:join(DataDir, "wrong_module_name.erl"),

0 commit comments

Comments
 (0)