From 3bfb201414264b22c98b5b8fd475c7f3ad072f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 31 Jan 2026 15:15:37 +0100 Subject: [PATCH 01/12] Evaluate during compilation --- lib/elixir/src/elixir_compiler.erl | 61 ++++-------------------------- 1 file changed, 8 insertions(+), 53 deletions(-) diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 803231ba05..abdfcd0992 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -44,68 +44,23 @@ maybe_fast_compile(Forms, E) -> end, ok. -compile(Quoted, ArgsList, CompilerOpts, #{line := Line} = E) -> +compile(Quoted, ArgsList, _CompilerOpts, #{line := Line, file := File} = E) -> Block = no_tail_optimize([{line, Line}], Quoted), {Expanded, SE, EE} = elixir_expand:expand(Block, elixir_env:env_to_ex(E), E), elixir_env:check_unused_vars(SE, EE), - {Module, Fun, LabelledLocals} = - elixir_erl_compiler:spawn(fun() -> spawned_compile(Expanded, CompilerOpts, E) end), + {Vars, TS} = elixir_erl_var:from_env(E), + {ErlExprs, _} = elixir_erl_pass:translate(Expanded, erl_anno:new(Line), TS), + Forms = code_eval(ErlExprs, Line, File, Vars), + {value, Fun, _} = erl_eval:expr(Forms, #{}), Args = list_to_tuple(ArgsList), - {dispatch(Module, Fun, Args, LabelledLocals), SE, EE}. + {Fun(Args), SE, EE}. -spawned_compile(ExExprs, CompilerOpts, #{line := Line, file := File} = E) -> - {Vars, S} = elixir_erl_var:from_env(E), - {ErlExprs, _} = elixir_erl_pass:translate(ExExprs, erl_anno:new(Line), S), - - Module = retrieve_compiler_module(), - Fun = code_fun(?key(E, module)), - Forms = code_mod(Fun, ErlExprs, Line, File, Module, Vars), - - {Module, Binary} = elixir_erl_compiler:noenv_forms(Forms, File, [nowarn_nomatch | CompilerOpts]), - code:load_binary(Module, "", Binary), - {Module, Fun, is_purgeable(Binary)}. - -is_purgeable(<<"FOR1", _Size:32, "BEAM", Rest/binary>>) -> - do_is_purgeable(Rest). - -do_is_purgeable(<<>>) -> true; -do_is_purgeable(<<"LocT", 4:32, 0:32, _/binary>>) -> true; -do_is_purgeable(<<"LocT", _:32, _/binary>>) -> false; -do_is_purgeable(<<_:4/binary, Size:32, Beam/binary>>) -> - <<_:(4 * trunc((Size+3) / 4))/binary, Rest/binary>> = Beam, - do_is_purgeable(Rest). - -dispatch(Module, Fun, Args, Purgeable) -> - Res = Module:Fun(Args), - return_compiler_module(Module, Purgeable), - Res. - -code_fun(nil) -> '__FILE__'; -code_fun(_) -> '__MODULE__'. - -code_mod(Fun, Expr, Line, File, Module, Vars) when is_binary(File), is_integer(Line) -> +code_eval(Expr, Line, File, Vars) when is_binary(File), is_integer(Line) -> Ann = erl_anno:new(Line), Tuple = {tuple, Ann, [{var, Ann, Var} || {_, Var} <- Vars]}, - Relative = elixir_utils:relative_to_cwd(File), - - [{attribute, Ann, file, {elixir_utils:characters_to_list(Relative), 1}}, - {attribute, Ann, module, Module}, - {attribute, Ann, compile, no_auto_import}, - {attribute, Ann, export, [{Fun, 1}, {'__RELATIVE__', 0}]}, - {function, Ann, Fun, 1, [ - {clause, Ann, [Tuple], [], [Expr]} - ]}, - {function, Ann, '__RELATIVE__', 0, [ - {clause, Ann, [], [], [elixir_erl:elixir_to_erl(Relative)]} - ]}]. - -retrieve_compiler_module() -> - elixir_code_server:call(retrieve_compiler_module). - -return_compiler_module(Module, Purgeable) -> - elixir_code_server:cast({return_compiler_module, Module, Purgeable}). + {'fun', Ann, {clauses, [{clause, Ann, [Tuple], [], [Expr]}]}}. allows_fast_compilation({'__block__', _, Exprs}) -> lists:all(fun allows_fast_compilation/1, Exprs); From b0f54184f4b14f5180cc0044a519ef55ac85cf42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 18 Feb 2026 14:13:45 +0100 Subject: [PATCH 02/12] Add :module_definition compiler option --- lib/elixir/lib/code.ex | 47 ++++++++++++++++++++++++++------------- lib/elixir/src/elixir.erl | 7 +++--- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 332a8251ee..45d26fdebb 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1702,9 +1702,6 @@ defmodule Code do Available options are: - * `:docs` - when `true`, retains documentation in the compiled module. - Defaults to `true`. - * `:debug_info` - when `true`, retains debug information in the compiled module. This option can also be overridden per module using the `@compile` directive. Defaults to `true`. @@ -1720,6 +1717,9 @@ defmodule Code do via the `:test_elixirc_options` project configuration, as there is typically no need to store debug chunks for test files. + * `:docs` - when `true`, retains documentation in the compiled module. + Defaults to `true`. + * `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn when a protocol has already been consolidated and a new implementation is added. Defaults to `false`. @@ -1738,17 +1738,25 @@ defmodule Code do via the `:test_elixirc_options` project configuration, as there is typically no need to infer signatures for test files. - * `:relative_paths` - when `true`, uses relative paths in quoted nodes, - warnings, and errors generated by the compiler. Note disabling this option - won't affect runtime warnings and errors. Defaults to `true`. + * `:module_definition` (since v1.20.0) - stores if the module definition should + be `:compiled` (the default) or `:interpreted`. Note this does not affect the + `.beam` file written to disk, only how the contents inside `defmodule` are + executed. Setting it to `:interpreted` may offer better compilation times for + large projects. * `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}` tuples that will not emit warnings that the module or function does not exist at compilation time. Pass atom `:all` to skip warning for all undefined functions. This can be useful when doing dynamic compilation. Defaults to `[]`. - * `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during - compilation. See the module docs for more information. Defaults to `[]`. + * `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`. + When `:raise` (the default), undefined variables will trigger a compilation + error. You may be set it to `:warn` if you want undefined variables to + emit a warning and expand as to a local call to the zero-arity function + of the same name (for example, `node` would be expanded as `node()`). + This `:warn` behavior only exists for compatibility reasons when working + with old dependencies, its usage is discouraged and it will be removed + in future releases. * `:parser_options` (since v1.10.0) - a keyword list of options to be given to the parser when compiling files. It accepts the same options as @@ -1759,14 +1767,12 @@ defmodule Code do and `compile_file/2` but not `string_to_quoted/2` and friends, as the latter is used for other purposes beyond compilation. - * `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`. - When `:raise` (the default), undefined variables will trigger a compilation - error. You may be set it to `:warn` if you want undefined variables to - emit a warning and expand as to a local call to the zero-arity function - of the same name (for example, `node` would be expanded as `node()`). - This `:warn` behavior only exists for compatibility reasons when working - with old dependencies, its usage is discouraged and it will be removed - in future releases. + * `:relative_paths` - when `true`, uses relative paths in quoted nodes, + warnings, and errors generated by the compiler. Note disabling this option + won't affect runtime warnings and errors. Defaults to `true`. + + * `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during + compilation. See the module docs for more information. Defaults to `[]`. It always returns `:ok`. Raises an error for invalid options. @@ -1806,6 +1812,15 @@ defmodule Code do :ok end + def put_compiler_option(:module_definition, value) do + if value not in [:interpreted, :compiled] do + raise "compiler option :module_definition should be either :interpreted or :compiled, got: #{inspect(value)}" + end + + :elixir_config.put(:infer_signatures, value) + :ok + end + def put_compiler_option(:infer_signatures, value) do value = cond do diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 1702dcb326..333f8719ad 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -92,16 +92,17 @@ start(_Type, _Args) -> {no_halt, false}, %% Compiler options + {debug_info, true}, {docs, true}, {ignore_already_consolidated, false}, {ignore_module_conflict, false}, - {initial_dbg_callback, InitialDbgCallback}, {infer_signatures, [elixir]}, + {initial_dbg_callback, InitialDbgCallback}, + {module_definition, compiled}, + {no_warn_undefined, []}, {on_undefined_variable, raise}, {parser_options, [{columns, true}]}, - {debug_info, true}, {relative_paths, true}, - {no_warn_undefined, []}, {tracers, []} | URIConfig ], From 148dac6f2d5b72b3a8bfbf3c14465d085e98a5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 10:48:12 +0100 Subject: [PATCH 03/12] Export erl_eval --- lib/elixir/src/elixir.erl | 55 ++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 333f8719ad..5a6cb307f4 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -10,9 +10,8 @@ -export([start/2, stop/1, config_change/3]). -export([ string_to_tokens/5, tokens_to_quoted/3, string_to_quoted/5, 'string_to_quoted!'/5, - env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3, - eval_quoted/4, eval_local_handler/2, eval_external_handler/3, - emit_warnings/3 + env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3, eval_quoted/4, + erl_eval/3, eval_local_handler/2, eval_external_handler/3, emit_warnings/3 ]). -include("elixir.hrl"). -define(system, 'Elixir.System'). @@ -325,33 +324,7 @@ eval_forms(Tree, Binding, OrigE, Opts) -> end; _ -> - Exprs = - case Erl of - {block, _, BlockExprs} -> BlockExprs; - _ -> [Erl] - end, - - %% We use remote names so eval works across Elixir versions. - LocalHandler = {value, fun ?MODULE:eval_local_handler/2}, - ExternalHandler = {value, fun ?MODULE:eval_external_handler/3}, - - {value, Value, NewBinding} = - try - %% ?elixir_eval_env is used by the external handler. - %% - %% The reason why we use the process dictionary to pass the environment - %% is because we want to avoid passing closures to erl_eval, as that - %% would effectively tie the eval code to the Elixir version and it is - %% best if it depends solely on Erlang/OTP. - %% - %% The downside is that functions that escape the eval context will no - %% longer have the original environment they came from. - erlang:put(?elixir_eval_env, NewE), - erl_eval:exprs(Exprs, ErlBinding, LocalHandler, ExternalHandler) - after - erlang:erase(?elixir_eval_env) - end, - + {value, Value, NewBinding} = erl_eval(Erl, ErlBinding, NewE), PruneBefore = if Prune -> length(Binding); true -> -1 end, {DumpedBinding, DumpedVars} = @@ -360,6 +333,28 @@ eval_forms(Tree, Binding, OrigE, Opts) -> {Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}} end. +%% Evaluate Erlang code with careful handling of local and external functions +erl_eval(Expr, Binding, Env) -> + %% We use remote names so eval works across Elixir versions + LocalHandler = {value, fun ?MODULE:eval_local_handler/2}, + ExternalHandler = {value, fun ?MODULE:eval_external_handler/3}, + + try + %% ?elixir_eval_env is used by the external handler. + %% + %% The reason why we use the process dictionary to pass the environment + %% is because we want to avoid passing closures to erl_eval, as that + %% would effectively tie the eval code to the Elixir version and it is + %% best if it depends solely on Erlang/OTP. + %% + %% The downside is that functions that escape the eval context will no + %% longer have the original environment they came from. + erlang:put(?elixir_eval_env, Env), + erl_eval:expr(Expr, Binding, LocalHandler, ExternalHandler) + after + erlang:erase(?elixir_eval_env) + end. + eval_local_handler(FunName, Args) -> {current_stacktrace, Stack} = erlang:process_info(self(), current_stacktrace), Opts = [{module, nil}, {function, FunName}, {arity, length(Args)}, {reason, 'undefined local'}], From 18be4dc7749ee9a5520774397854969b8b21b652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 11:04:03 +0100 Subject: [PATCH 04/12] Allow mode to be chosen --- lib/elixir/src/elixir_compiler.erl | 104 ++++++++++++++++++++++------- lib/elixir/src/elixir_module.erl | 8 ++- 2 files changed, 87 insertions(+), 25 deletions(-) diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index abdfcd0992..2a53e310b1 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -20,7 +20,7 @@ quoted(Forms, File, Callback) -> elixir_lexical:run( Env, - fun (LexicalEnv) -> maybe_fast_compile(Forms, LexicalEnv) end, + fun (LexicalEnv) -> optimize_defmodule(Forms, LexicalEnv) end, fun (#{lexical_tracker := Pid}) -> Callback(File, Pid) end ), @@ -33,45 +33,103 @@ file(File, Callback) -> {ok, Bin} = file:read_file(File), string(elixir_utils:characters_to_list(Bin), File, Callback). -%% Evaluates the given code through the Erlang compiler. -%% It may end-up evaluating the code if it is deemed a -%% more efficient strategy depending on the code snippet. -maybe_fast_compile(Forms, E) -> - case (?key(E, module) == nil) andalso allows_fast_compilation(Forms) andalso +%% In case the forms only holds defmodules, we optimize +%% it by expanding them directly. +optimize_defmodule(Forms, E) -> + case (?key(E, module) == nil) andalso only_defmodule(Forms) andalso (not elixir_config:is_bootstrap()) of - true -> fast_compile(Forms, E); + true -> expand_defmodule(Forms, E); false -> compile(Forms, [], [], E) end, ok. -compile(Quoted, ArgsList, _CompilerOpts, #{line := Line, file := File} = E) -> - Block = no_tail_optimize([{line, Line}], Quoted), - {Expanded, SE, EE} = elixir_expand:expand(Block, elixir_env:env_to_ex(E), E), +%% A version of compilation that uses eval (interpreted) +interpret(Quoted, ArgsList, #{line := Line} = E) -> + {Expanded, SE, EE} = elixir_expand:expand(Quoted, elixir_env:env_to_ex(E), E), elixir_env:check_unused_vars(SE, EE), {Vars, TS} = elixir_erl_var:from_env(E), {ErlExprs, _} = elixir_erl_pass:translate(Expanded, erl_anno:new(Line), TS), - Forms = code_eval(ErlExprs, Line, File, Vars), - {value, Fun, _} = erl_eval:expr(Forms, #{}), + ListBinding = lists:zipwith(fun({_, Var}, Arg) -> {Var, Arg} end, Vars, ArgsList), + Binding = maps:from_list(ListBinding), + + {value, Result, _} = elixir:erl_eval(ErlExprs, Binding, E), + {Result, SE, EE}. + +compile(Quoted, ArgsList, CompilerOpts, #{line := Line} = E) -> + Block = no_tail_optimize([{line, Line}], Quoted), + {Expanded, SE, EE} = elixir_expand:expand(Block, elixir_env:env_to_ex(E), E), + elixir_env:check_unused_vars(SE, EE), + + {Module, Fun, LabelledLocals} = + elixir_erl_compiler:spawn(fun() -> spawned_compile(Expanded, CompilerOpts, E) end), + Args = list_to_tuple(ArgsList), - {Fun(Args), SE, EE}. + {dispatch(Module, Fun, Args, LabelledLocals), SE, EE}. -code_eval(Expr, Line, File, Vars) when is_binary(File), is_integer(Line) -> +spawned_compile(ExExprs, CompilerOpts, #{line := Line, file := File} = E) -> + {Vars, S} = elixir_erl_var:from_env(E), + {ErlExprs, _} = elixir_erl_pass:translate(ExExprs, erl_anno:new(Line), S), + + Module = retrieve_compiler_module(), + Fun = code_fun(?key(E, module)), + Forms = code_mod(Fun, ErlExprs, Line, File, Module, Vars), + + {Module, Binary} = elixir_erl_compiler:noenv_forms(Forms, File, [nowarn_nomatch | CompilerOpts]), + code:load_binary(Module, "", Binary), + {Module, Fun, is_purgeable(Binary)}. + +is_purgeable(<<"FOR1", _Size:32, "BEAM", Rest/binary>>) -> + do_is_purgeable(Rest). + +do_is_purgeable(<<>>) -> true; +do_is_purgeable(<<"LocT", 4:32, 0:32, _/binary>>) -> true; +do_is_purgeable(<<"LocT", _:32, _/binary>>) -> false; +do_is_purgeable(<<_:4/binary, Size:32, Beam/binary>>) -> + <<_:(4 * trunc((Size+3) / 4))/binary, Rest/binary>> = Beam, + do_is_purgeable(Rest). + +dispatch(Module, Fun, Args, Purgeable) -> + Res = Module:Fun(Args), + return_compiler_module(Module, Purgeable), + Res. + +code_fun(nil) -> '__FILE__'; +code_fun(_) -> '__MODULE__'. + +code_mod(Fun, Expr, Line, File, Module, Vars) when is_binary(File), is_integer(Line) -> Ann = erl_anno:new(Line), Tuple = {tuple, Ann, [{var, Ann, Var} || {_, Var} <- Vars]}, - {'fun', Ann, {clauses, [{clause, Ann, [Tuple], [], [Expr]}]}}. - -allows_fast_compilation({'__block__', _, Exprs}) -> - lists:all(fun allows_fast_compilation/1, Exprs); -allows_fast_compilation({defmodule, _, [_, [{do, _}]]}) -> + Relative = elixir_utils:relative_to_cwd(File), + + [{attribute, Ann, file, {elixir_utils:characters_to_list(Relative), 1}}, + {attribute, Ann, module, Module}, + {attribute, Ann, compile, no_auto_import}, + {attribute, Ann, export, [{Fun, 1}, {'__RELATIVE__', 0}]}, + {function, Ann, Fun, 1, [ + {clause, Ann, [Tuple], [], [Expr]} + ]}, + {function, Ann, '__RELATIVE__', 0, [ + {clause, Ann, [], [], [elixir_erl:elixir_to_erl(Relative)]} + ]}]. + +retrieve_compiler_module() -> + elixir_code_server:call(retrieve_compiler_module). + +return_compiler_module(Module, Purgeable) -> + elixir_code_server:cast({return_compiler_module, Module, Purgeable}). + +only_defmodule({'__block__', _, Exprs}) -> + lists:all(fun only_defmodule/1, Exprs); + only_defmodule({defmodule, _, [_, [{do, _}]]}) -> true; -allows_fast_compilation(_) -> +only_defmodule(_) -> false. -fast_compile({'__block__', _, Exprs}, E) -> - lists:foldl(fun(Expr, _) -> fast_compile(Expr, E) end, nil, Exprs); -fast_compile({defmodule, Meta, [Mod, [{do, Block}]]}, NoLineE) -> +expand_defmodule({'__block__', _, Exprs}, E) -> + lists:foldl(fun(Expr, _) -> expand_defmodule(Expr, E) end, nil, Exprs); +expand_defmodule({defmodule, Meta, [Mod, [{do, Block}]]}, NoLineE) -> E = NoLineE#{line := ?line(Meta)}, Expanded = case Mod of diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 8d978aa459..88166254ea 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -472,8 +472,12 @@ build(Module, Line, File, E) -> eval_form(Line, Module, DataBag, Block, Vars, Prune, E) -> %% Given Elixir modules can get very long to compile due to metaprogramming, - %% we disable expansions that take linear time to code size. - {Value, ExS, EE} = elixir_compiler:compile(Block, Vars, [no_bool_opt, no_ssa_opt], E), + %% we disable expansions that have linear time to code size. + {Value, ExS, EE} = + case elixir_config:get(module_definition) of + interpreted -> elixir_compiler:interpret(Block, Vars, E); + compiled -> elixir_compiler:compile(Block, Vars, [no_bool_opt, no_ssa_opt], E) + end, elixir_overridable:store_not_overridden(Module), EV = (elixir_env:reset_vars(EE))#{line := Line}, EC = eval_callbacks(Line, DataBag, before_compile, [EV], EV), From bc829366f4cc83d4d021fd0660ad4f0a5be99491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 11:16:27 +0100 Subject: [PATCH 05/12] Include module_definition in compiler available options --- lib/elixir/lib/code.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 45d26fdebb..34289cf60b 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -312,7 +312,12 @@ defmodule Code do @available_compiler_options @boolean_compiler_options ++ @list_compiler_options ++ - [:on_undefined_variable, :infer_signatures, :no_warn_undefined] + [ + :on_undefined_variable, + :infer_signatures, + :no_warn_undefined, + :module_definition + ] @doc """ Lists all required files. From 99b6034ccc07d2e93d13cf7d67743a125dd92885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 11:17:55 +0100 Subject: [PATCH 06/12] Fix option name --- lib/elixir/lib/code.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 34289cf60b..21bceda05c 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1822,7 +1822,7 @@ defmodule Code do raise "compiler option :module_definition should be either :interpreted or :compiled, got: #{inspect(value)}" end - :elixir_config.put(:infer_signatures, value) + :elixir_config.put(:module_definition, value) :ok end From 6dee37b848d07b7e2ff65d01c239808a62eaca6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 11:18:34 +0100 Subject: [PATCH 07/12] Make function public --- lib/elixir/src/elixir_compiler.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 2a53e310b1..0e51f6693d 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -4,7 +4,7 @@ %% Elixir compiler front-end to the Erlang backend. -module(elixir_compiler). --export([string/3, quoted/3, bootstrap/0, file/2, compile/4]). +-export([string/3, quoted/3, bootstrap/0, file/2, compile/4, interpret/3]). -include("elixir.hrl"). string(Contents, File, Callback) -> From 3620b84a5654aa4d546fbd9dc2c9b026b26257af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 11:40:48 +0100 Subject: [PATCH 08/12] Improve stacktrace reporting --- lib/elixir/src/elixir.erl | 6 ++---- lib/elixir/src/elixir_compiler.erl | 9 ++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 5a6cb307f4..9fd8bc1269 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -401,10 +401,8 @@ eval_external_handler(Ann, FunOrModFun, Args) -> %% Add file+line information at the bottom Bottom = case erlang:get(?elixir_eval_env) of - #{file := File} -> - [{elixir_eval, '__FILE__', 1, - [{file, elixir_utils:characters_to_list(File)}, {line, erl_anno:line(Ann)}]}]; - + #{'__struct__' := 'Elixir.Macro.Env'} = E -> + 'Elixir.Macro.Env':stacktrace(E#{line := erl_anno:line(Ann)}); _ -> [] end, diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 0e51f6693d..6629f0688f 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -54,7 +54,14 @@ interpret(Quoted, ArgsList, #{line := Line} = E) -> ListBinding = lists:zipwith(fun({_, Var}, Arg) -> {Var, Arg} end, Vars, ArgsList), Binding = maps:from_list(ListBinding), - {value, Result, _} = elixir:erl_eval(ErlExprs, Binding, E), + {value, Result, _} = + try + elixir:erl_eval(ErlExprs, Binding, E) + catch + Kind:Reason:Stacktrace -> + erlang:raise(Kind, Reason, Stacktrace ++ 'Elixir.Macro.Env':stacktrace(E)) + end, + {Result, SE, EE}. compile(Quoted, ArgsList, CompilerOpts, #{line := Line} = E) -> From 9cf8454b821ef7a1b3e33f3a3bf87bdb809042e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 11:46:06 +0100 Subject: [PATCH 09/12] Add known limitations to docs --- lib/elixir/lib/code.ex | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 21bceda05c..0102932102 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1746,8 +1746,15 @@ defmodule Code do * `:module_definition` (since v1.20.0) - stores if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are - executed. Setting it to `:interpreted` may offer better compilation times for - large projects. + executed. Using the `:interpreted` mode may offer better compilation times for + large projects, however, it comes with some downsides: + + * Errors during compilation may have less precise stacktraces + + * Anonymous functions within `defmodule` can have only up to 20 arguments. + If this is an issue, you can use maps or tuples to group the data. + Note the functions themselves inside `defmodule`, such as the ones defined + inside `def` and friends, can still have up to 255 arguments * `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}` tuples that will not emit warnings that the module or function does not exist From 434a26d5ef8e90f8e5df39f0dedcb033db779fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 12:22:18 +0100 Subject: [PATCH 10/12] Properly prune stacktrace --- lib/iex/lib/iex/evaluator.ex | 2 +- lib/iex/test/iex/interaction_test.exs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 8c0b8370d3..6830916c5b 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -448,7 +448,7 @@ defmodule IEx.Evaluator do defp prune_stacktrace(stack) do stack |> Enum.reverse() - |> Enum.drop_while(&(elem(&1, 0) != :elixir_eval)) + |> Enum.drop_while(&(elem(&1, 0) != :elixir_compiler)) |> Enum.reverse() |> case do [] -> stack diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index 26635c4f00..f351da9345 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -174,10 +174,9 @@ defmodule IEx.InteractionTest do test "exception" do exception = Regex.escape("** (ArithmeticError) bad argument in arithmetic expression") - assert capture_iex("1 + :atom\n:this_is_still_working") =~ - ~r/^#{exception}.+\n:this_is_still_working$/s - - refute capture_iex("1 + :atom\n:this_is_still_working") =~ ~r/erl_eval/s + result = capture_iex("1 + :atom\n:this_is_still_working") + assert result =~ ~r/^#{exception}.+\n:this_is_still_working$/s + refute result =~ ~r/erl_eval/s end test "exception while invoking conflicting helpers" do From 044261b517fa2946bbb2a0c10d2d8232fdf27d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 14:27:10 +0100 Subject: [PATCH 11/12] Tests --- lib/elixir/test/elixir/code_test.exs | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 4542008208..0d1144fef3 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -751,6 +751,36 @@ defmodule Code.SyncTest do defp refute_cached(_path), do: :ok end + test "evaluates module definitions" do + Code.put_compiler_option(:module_definition, :interpreted) + + defmodule CodeTest.EvalModule do + {:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace) + assert Enum.find(stacktrace, &(elem(&1, 0) == :erl_eval)) + end + after + Code.put_compiler_option(:module_definition, :compiled) + end + + test "evaluates module definitions with stacktraces" do + Code.put_compiler_option(:module_definition, :interpreted) + + try do + defmodule CodeTest.EvalModuleRaise do + Enum.map(1..10, fn x -> x <> "example" end) + end + rescue + e -> + assert e.__struct__ == ArgumentError + assert Enum.find(__STACKTRACE__, &(elem(&1, 0) == Code.SyncTest.CodeTest.EvalModuleRaise)) + assert Enum.find(__STACKTRACE__, &(elem(&1, 0) == :erl_eval)) + else + _ -> flunk("defmodule should have failed") + end + after + Code.put_compiler_option(:module_definition, :compiled) + end + test "prepend_path" do path = Path.join(__DIR__, "fixtures") true = Code.prepend_path(path) From 4d682cf470e78087c5716bb476f3c25cfe8ccbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 19 Feb 2026 18:11:12 +0100 Subject: [PATCH 12/12] Apply suggestion from @josevalim --- lib/elixir/lib/code.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 0102932102..6f5765976d 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1747,7 +1747,8 @@ defmodule Code do be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for - large projects, however, it comes with some downsides: + large projects, especially on machines with high core count, however, it comes + with some downsides: * Errors during compilation may have less precise stacktraces