Skip to content

Commit 9814a4c

Browse files
author
José Valim
committed
Do not expand interpolation twice, closes #9709
1 parent 3dfdd80 commit 9814a4c

File tree

4 files changed

+49
-46
lines changed

4 files changed

+49
-46
lines changed

lib/elixir/src/elixir_bitstring.erl

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,17 +132,16 @@ compute_alignment(_, _, _) -> unknown.
132132

133133
%% Expands the expression of a bitstring, that is, the LHS of :: or
134134
%% an argument of the bitstring (such as "foo" in "<<foo>>").
135+
%% If we are inside a match/guard, we inline interpolations explicitly,
136+
%% otherwise they are inlined by elixir_rewrite.erl.
135137

136-
expand_expr(Meta, {{'.', M1, [Mod, to_string]}, M2, [Arg]}, Fun, E)
137-
when Mod == 'Elixir.Kernel'; Mod == 'Elixir.String.Chars' ->
138+
expand_expr(_Meta, {{'.', _, [Mod, to_string]}, _, [Arg]} = AST, Fun, {#{context := Context}, _} = E)
139+
when Context /= nil, (Mod == 'Elixir.Kernel') orelse (Mod == 'Elixir.String.Chars') ->
138140
case Fun(Arg, E) of
139141
{EBin, EE} when is_binary(EBin) -> {EBin, EE};
140-
_ -> do_expand_expr(Meta, {{'.', M1, ['Elixir.String.Chars', to_string]}, M2, [Arg]}, Fun, E)
142+
_ -> Fun(AST, E) % Let it raise
141143
end;
142144
expand_expr(Meta, Component, Fun, E) ->
143-
do_expand_expr(Meta, Component, Fun, E).
144-
145-
do_expand_expr(Meta, Component, Fun, E) ->
146145
case Fun(Component, E) of
147146
{EComponent, {ErrorE, _}} when is_list(EComponent); is_atom(EComponent) ->
148147
form_error(Meta, ErrorE, ?MODULE, {invalid_literal, EComponent});

lib/elixir/src/elixir_erl_pass.erl

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -528,29 +528,20 @@ extract_bit_type({Other, _, []}, Acc) ->
528528
%% Optimizations that are specific to Erlang and change
529529
%% the format of the AST.
530530

531-
translate_remote('Elixir.Access' = Mod, get, Meta, [Container, Value], S) ->
532-
Ann = ?ann(Meta),
533-
{TArgs, SA} = translate_args([Container, Value, nil], S),
534-
{?remote(Ann, Mod, get, TArgs), SA};
535531
translate_remote('Elixir.String.Chars', to_string, Meta, [Arg], S) ->
536-
case is_always_string(Arg) of
537-
true ->
538-
translate(Arg, S);
539-
false ->
540-
{TArg, TS} = translate(Arg, S),
541-
{VarName, VS} = elixir_erl_var:build('_', TS),
542-
543-
Generated = ?ann(?generated(Meta)),
544-
Var = {var, Generated, VarName},
545-
Guard = ?remote(Generated, erlang, is_binary, [Var]),
546-
Slow = ?remote(Generated, 'Elixir.String.Chars', to_string, [Var]),
547-
Fast = Var,
548-
549-
{{'case', Generated, TArg, [
550-
{clause, Generated, [Var], [[Guard]], [Fast]},
551-
{clause, Generated, [Var], [], [Slow]}
552-
]}, VS}
553-
end;
532+
{TArg, TS} = translate(Arg, S),
533+
{VarName, VS} = elixir_erl_var:build('_', TS),
534+
535+
Generated = ?ann(?generated(Meta)),
536+
Var = {var, Generated, VarName},
537+
Guard = ?remote(Generated, erlang, is_binary, [Var]),
538+
Slow = ?remote(Generated, 'Elixir.String.Chars', to_string, [Var]),
539+
Fast = Var,
540+
541+
{{'case', Generated, TArg, [
542+
{clause, Generated, [Var], [[Guard]], [Fast]},
543+
{clause, Generated, [Var], [], [Slow]}
544+
]}, VS};
554545
translate_remote(maps, put, Meta, [Key, Value, Map], S) ->
555546
Ann = ?ann(Meta),
556547

@@ -600,20 +591,6 @@ translate_remote(Left, Right, Meta, Args, S) ->
600591
{{call, Ann, {remote, Ann, TLeft, TRight}, TArgs}, SA}
601592
end.
602593

603-
is_always_string({{'.', _, [Module, Function]}, _, Args}) ->
604-
is_always_string(Module, Function, length(Args));
605-
%% Binary literals were already excluded in earlier passes.
606-
is_always_string(_Ast) ->
607-
false.
608-
609-
is_always_string('Elixir.Enum', join, _) -> true;
610-
is_always_string('Elixir.Enum', map_join, _) -> true;
611-
is_always_string('Elixir.Kernel', inspect, _) -> true;
612-
is_always_string('Elixir.Macro', to_string, _) -> true;
613-
is_always_string('Elixir.String.Chars', to_string, _) -> true;
614-
is_always_string('Elixir.Path', join, _) -> true;
615-
is_always_string(_Module, _Function, _Args) -> false.
616-
617594
generate_struct_name_guard([{map_field_exact, Ann, {atom, _, '__struct__'} = Key, Var} | Rest], Acc, S0) ->
618595
{ModuleVarName, S1} = elixir_erl_var:build('_', S0),
619596
Generated = erl_anno:set_generated(true, Ann),

lib/elixir/src/elixir_rewrite.erl

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,11 @@ inner_inline(_, _, _, _) -> false.
223223
%% as they may change the number of arguments. However, they
224224
%% don't add new code (such as case statements), at best they
225225
%% perform dead code removal.
226-
rewrite(?string_chars, _, to_string, _, [String]) when is_binary(String) ->
227-
String;
228-
rewrite(?string_chars, _, to_string, _, [{{'.', _, [?kernel, inspect]}, _, _} = Call]) ->
229-
Call;
226+
rewrite(?string_chars, DotMeta, to_string, Meta, [Arg]) ->
227+
case is_always_string(Arg) of
228+
true -> Arg;
229+
false -> {{'.', DotMeta, [?string_chars, to_string]}, Meta, [Arg]}
230+
end;
230231
rewrite(Receiver, DotMeta, Right, Meta, Args) ->
231232
{EReceiver, ERight, EArgs} = inner_rewrite(ex_to_erl, DotMeta, Receiver, Right, Args),
232233
{{'.', DotMeta, [EReceiver, ERight]}, Meta, EArgs}.
@@ -333,3 +334,16 @@ format_error({invalid_match, Receiver, Right, Arity}) ->
333334
format_error({invalid_match_append, Arg}) ->
334335
io_lib:format("invalid argument for ++ operator inside a match, expected a literal proper list, got: ~ts",
335336
['Elixir.Macro':to_string(Arg)]).
337+
338+
is_always_string({{'.', _, [Module, Function]}, _, Args}) ->
339+
is_always_string(Module, Function, length(Args));
340+
is_always_string(Ast) ->
341+
is_binary(Ast).
342+
343+
is_always_string('Elixir.Enum', join, _) -> true;
344+
is_always_string('Elixir.Enum', map_join, _) -> true;
345+
is_always_string('Elixir.Kernel', inspect, _) -> true;
346+
is_always_string('Elixir.Macro', to_string, _) -> true;
347+
is_always_string('Elixir.String.Chars', to_string, _) -> true;
348+
is_always_string('Elixir.Path', join, _) -> true;
349+
is_always_string(_Module, _Function, _Args) -> false.

lib/elixir/test/elixir/kernel/expansion_test.exs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ Code.require_file("../test_helper.exs", __DIR__)
33
defmodule Kernel.ExpansionTarget do
44
defmacro seventeen, do: 17
55
defmacro bar, do: "bar"
6+
7+
defmacro message_hello(arg) do
8+
send(self(), :hello)
9+
arg
10+
end
611
end
712

813
defmodule Kernel.ExpansionTest do
@@ -2149,6 +2154,14 @@ defmodule Kernel.ExpansionTest do
21492154
test "inlines binaries inside interpolation" do
21502155
import Kernel.ExpansionTarget
21512156

2157+
# Check expansion happens only once
2158+
assert expand(quote(do: "foo#{message_hello("bar")}")) |> clean_meta([:alignment]) ==
2159+
quote(do: <<"foo"::binary(), "bar"::binary()>>)
2160+
2161+
assert_received :hello
2162+
refute_received :hello
2163+
2164+
# And it also works in match
21522165
assert expand(quote(do: "foo#{bar()}" = "foobar")) |> clean_meta([:alignment]) ==
21532166
quote(do: <<"foo"::binary(), "bar"::binary()>> = "foobar")
21542167
end

0 commit comments

Comments
 (0)