Skip to content

Commit 3bb0a83

Browse files
committed
Disable tail call optimization on file root entries
1 parent fe0d57c commit 3bb0a83

File tree

3 files changed

+28
-24
lines changed

3 files changed

+28
-24
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4903,17 +4903,6 @@ defmodule Kernel do
49034903
{expanded, nil}
49044904
end
49054905

4906-
# We do this so that the block is not tail-call optimized and stacktraces
4907-
# are not messed up. Basically, we just insert something between the return
4908-
# value of the block and what is returned by defmodule. Using just ":ok" or
4909-
# similar doesn't work because it's likely optimized away by the compiler.
4910-
block =
4911-
quote do
4912-
result = unquote(block)
4913-
:elixir_utils.noop()
4914-
result
4915-
end
4916-
49174906
escaped =
49184907
case env do
49194908
%{function: nil, lexical_tracker: pid} when is_pid(pid) ->

lib/elixir/src/elixir_compiler.erl

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ quoted(Forms, File, Callback) ->
1616

1717
elixir_lexical:run(
1818
Env,
19-
fun (LexicalEnv) -> eval_or_compile(Forms, [], LexicalEnv) end,
19+
fun (LexicalEnv) -> maybe_fast_compile(Forms, [], LexicalEnv) end,
2020
fun (#{lexical_tracker := Pid}) -> Callback(File, Pid) end
2121
),
2222

@@ -32,16 +32,17 @@ file(File, Callback) ->
3232
%% Evaluates the given code through the Erlang compiler.
3333
%% It may end-up evaluating the code if it is deemed a
3434
%% more efficient strategy depending on the code snippet.
35-
eval_or_compile(Forms, Args, E) ->
35+
maybe_fast_compile(Forms, Args, E) ->
3636
case (?key(E, module) == nil) andalso allows_fast_compilation(Forms) andalso
3737
(not elixir_config:is_bootstrap()) of
3838
true -> fast_compile(Forms, E);
3939
false -> compile(Forms, Args, E)
4040
end,
4141
ok.
4242

43-
compile(Quoted, ArgsList, E) ->
44-
{Expanded, SE, EE} = elixir_expand:expand(Quoted, elixir_env:env_to_ex(E), E),
43+
compile(Quoted, ArgsList, #{line := Line} = E) ->
44+
Block = no_tail_optimize([{line, Line}], Quoted),
45+
{Expanded, SE, EE} = elixir_expand:expand(Block, elixir_env:env_to_ex(E), E),
4546
elixir_env:check_unused_vars(SE, EE),
4647

4748
{Module, Fun, Purgeable} =
@@ -55,7 +56,7 @@ spawned_compile(ExExprs, #{line := Line, file := File} = E) ->
5556
{ErlExprs, _} = elixir_erl_pass:translate(ExExprs, erl_anno:new(Line), S),
5657

5758
Module = retrieve_compiler_module(),
58-
Fun = code_fun(?key(E, module)),
59+
Fun = code_fun(?key(E, module)),
5960
Forms = code_mod(Fun, ErlExprs, Line, File, Module, Vars),
6061

6162
{Module, Binary} = elixir_erl_compiler:noenv_forms(Forms, File, [nowarn_nomatch, no_bool_opt, no_ssa_opt]),
@@ -106,15 +107,9 @@ allows_fast_compilation(_) ->
106107

107108
fast_compile({'__block__', _, Exprs}, E) ->
108109
lists:foldl(fun(Expr, _) -> fast_compile(Expr, E) end, nil, Exprs);
109-
fast_compile({defmodule, Meta, [Mod, [{do, TailBlock}]]}, NoLineE) ->
110+
fast_compile({defmodule, Meta, [Mod, [{do, Block}]]}, NoLineE) ->
110111
E = NoLineE#{line := ?line(Meta)},
111112

112-
Block = {'__block__', Meta, [
113-
{'=', Meta, [{result, Meta, ?MODULE}, TailBlock]},
114-
{{'.', Meta, [elixir_utils, noop]}, Meta, []},
115-
{result, Meta, ?MODULE}
116-
]},
117-
118113
Expanded = case Mod of
119114
{'__aliases__', _, _} ->
120115
case elixir_aliases:expand_or_concat(Mod, E) of
@@ -129,6 +124,13 @@ fast_compile({defmodule, Meta, [Mod, [{do, TailBlock}]]}, NoLineE) ->
129124
ContextModules = [Expanded | ?key(E, context_modules)],
130125
elixir_module:compile(Expanded, Block, [], false, E#{context_modules := ContextModules}).
131126

127+
no_tail_optimize(Meta, Block) ->
128+
{'__block__', Meta, [
129+
{'=', Meta, [{result, Meta, ?MODULE}, Block]},
130+
{{'.', Meta, [elixir_utils, noop]}, Meta, []},
131+
{result, Meta, ?MODULE}
132+
]}.
133+
132134
%% Bootstrapper
133135

134136
bootstrap() ->

lib/elixir/test/elixir/code_test.exs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,12 @@ defmodule CodeTest do
347347
assert env.versioned_vars == %{}
348348

349349
assert_receive {:trace, {:on_module, _, _}, %{module: CodeTest.TracingPruning} = trace_env}
350-
assert trace_env.versioned_vars == %{{:result, Kernel} => 5, {:x, nil} => 1, {:y, nil} => 4}
350+
351+
assert trace_env.versioned_vars == %{
352+
{:result, :elixir_compiler} => 5,
353+
{:x, nil} => 1,
354+
{:y, nil} => 4
355+
}
351356
end
352357

353358
test "with defguard" do
@@ -496,6 +501,14 @@ defmodule CodeTest do
496501
:code.purge(CompileCrossSample)
497502
:code.delete(CompileCrossSample)
498503
end
504+
505+
test "disables tail call optimization at the root" do
506+
try do
507+
Code.compile_string("List.flatten(123)")
508+
rescue
509+
_ -> assert Enum.any?(__STACKTRACE__, &match?({_, :__FILE__, 1, _}, &1))
510+
end
511+
end
499512
end
500513

501514
test "format_string/2 returns empty iodata for empty string" do

0 commit comments

Comments
 (0)