Skip to content

Commit 804d034

Browse files
committed
Add prune_binding to Code.eval_quoted_with_env/4
1 parent 29a2bb4 commit 804d034

File tree

10 files changed

+112
-58
lines changed

10 files changed

+112
-58
lines changed

lib/elixir/lib/code.ex

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ defmodule Code do
181181
"""
182182

183183
@typedoc """
184-
A list with all variable bindings.
184+
A list with all variables and their values.
185185
186186
The binding keys are usually atoms, but they may be a tuple for variables
187187
defined in a different context.
@@ -344,7 +344,7 @@ defmodule Code do
344344
@doc """
345345
Evaluates the contents given by `string`.
346346
347-
The `binding` argument is a list of variable bindings.
347+
The `binding` argument is a list of all variables and their values.
348348
The `opts` argument is a keyword list of environment options.
349349
350350
**Warning**: `string` can be any Elixir code and will be executed with
@@ -362,17 +362,15 @@ defmodule Code do
362362
* `:line` - the line on which the script starts
363363
364364
Additionally, you may also pass an environment as second argument,
365-
so the evaluation happens within that environment. However, if the evaluated
366-
code requires or compiles another file, the environment given to this function
367-
will not apply to said files.
365+
so the evaluation happens within that environment.
368366
369367
Returns a tuple of the form `{value, binding}`, where `value` is the value
370368
returned from evaluating `string`. If an error occurs while evaluating
371-
`string` an exception will be raised.
369+
`string`, an exception will be raised.
372370
373-
`binding` is a list with all variable bindings after evaluating `string`.
374-
The binding keys are usually atoms, but they may be a tuple for variables
375-
defined in a different context.
371+
`binding` is a list with all variable names and their values after evaluating
372+
`string`. The binding keys are usually atoms, but they may be a tuple for variables
373+
defined in a different context. The names are in no particular order.
376374
377375
## Examples
378376
@@ -419,13 +417,13 @@ defmodule Code do
419417
defp validated_eval_string(string, binding, opts_or_env) do
420418
%{line: line, file: file} = env = env_for_eval(opts_or_env)
421419
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
422-
{value, binding, _env} = eval_verify(:eval_forms, forms, binding, env)
420+
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
423421
{value, binding}
424422
end
425423

426-
defp eval_verify(fun, forms, binding, env) do
424+
defp eval_verify(fun, args) do
427425
Module.ParallelChecker.verify(fn ->
428-
apply(:elixir, fun, [forms, binding, env])
426+
apply(:elixir, fun, args)
429427
end)
430428
end
431429

@@ -822,7 +820,9 @@ defmodule Code do
822820
"""
823821
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
824822
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
825-
{value, binding, _env} = eval_verify(:eval_quoted, quoted, binding, env_for_eval(env_or_opts))
823+
{value, binding, _env} =
824+
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
825+
826826
{value, binding}
827827
end
828828

@@ -859,10 +859,17 @@ defmodule Code do
859859
Therefore, the first time you call this function, you must compute
860860
the initial environment with `env_for_eval/1`. The remaining calls
861861
must pass the environment that was returned by this function.
862+
863+
## Options
864+
865+
* `:prune_binding` - (since v1.14.2) prune binding to keep only
866+
variables read or written by the evaluated code
867+
862868
"""
863869
@doc since: "1.14.0"
864-
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env) when is_list(binding) do
865-
eval_verify(:eval_forms, quoted, binding, env)
870+
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
871+
when is_list(binding) do
872+
eval_verify(:eval_quoted, [quoted, binding, env, opts])
866873
end
867874

868875
@doc ~S"""

lib/elixir/src/elixir.erl

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
-behaviour(application).
55
-export([start_cli/0,
66
string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5,
7-
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3]).
7+
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3,
8+
eval_quoted/4]).
89
-include("elixir.hrl").
910
-define(system, 'Elixir.System').
1011

@@ -260,18 +261,20 @@ env_for_eval(Opts) when is_list(Opts) ->
260261

261262
%% Quoted evaluation
262263

263-
eval_quoted(Tree, Binding, Opts) when is_list(Opts) ->
264-
eval_quoted(Tree, Binding, env_for_eval(Opts));
265-
eval_quoted(Tree, Binding, #{line := Line} = E) ->
266-
eval_forms(elixir_quote:linify(Line, line, Tree), Binding, E).
264+
eval_quoted(Tree, Binding, E) ->
265+
eval_quoted(Tree, Binding, E, []).
266+
eval_quoted(Tree, Binding, #{line := Line} = E, Opts) ->
267+
eval_forms(elixir_quote:linify(Line, line, Tree), Binding, E, Opts).
267268

268-
eval_forms(Tree, Binding, Opts) when is_list(Opts) ->
269-
eval_forms(Tree, Binding, env_for_eval(Opts));
270269
eval_forms(Tree, Binding, OrigE) ->
270+
eval_forms(Tree, Binding, OrigE, []).
271+
eval_forms(Tree, Binding, OrigE, Opts) ->
272+
Prune = proplists:get_value(prune_binding, Opts, false),
271273
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding),
272274
E = elixir_env:with_vars(OrigE, ExVars),
273-
S = elixir_erl_var:from_env(E, ErlVars),
274-
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, E, S),
275+
ExS = elixir_env:env_to_ex(E, Prune),
276+
ErlS = elixir_erl_var:from_env(E, ErlVars),
277+
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
275278

276279
case Erl of
277280
{atom, _, Atom} ->
@@ -286,7 +289,8 @@ eval_forms(Tree, Binding, OrigE) ->
286289

287290
ExternalHandler = eval_external_handler(NewE),
288291
{value, Value, NewBinding} = erl_eval:exprs(Exprs, ErlBinding, none, ExternalHandler),
289-
{Value, elixir_erl_var:dump_binding(NewBinding, NewExS, NewErlS), NewE}
292+
PruneBefore = if Prune -> length(Binding); true -> 0 end,
293+
{Value, elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore), NewE}
290294
end.
291295

292296
%% TODO: Remove conditional once we require Erlang/OTP 25+.
@@ -356,13 +360,14 @@ eval_external_handler(_Env) ->
356360
%% Converts a quoted expression to Erlang abstract format
357361

358362
quoted_to_erl(Quoted, E) ->
359-
{_, S} = elixir_erl_var:from_env(E),
360-
quoted_to_erl(Quoted, E, S).
363+
{_, ErlS} = elixir_erl_var:from_env(E),
364+
ExS = elixir_env:env_to_ex(E),
365+
quoted_to_erl(Quoted, ErlS, ExS, E).
361366

362-
quoted_to_erl(Quoted, Env, Scope) ->
367+
quoted_to_erl(Quoted, ErlS, ExS, Env) ->
363368
{Expanded, #elixir_ex{vars={ReadVars, _}} = NewExS, NewEnv} =
364-
elixir_expand:expand(Quoted, elixir_env:env_to_ex(Env), Env),
365-
{Erl, NewErlS} = elixir_erl_pass:translate(Expanded, erl_anno:new(?key(Env, line)), Scope),
369+
elixir_expand:expand(Quoted, ExS, Env),
370+
{Erl, NewErlS} = elixir_erl_pass:translate(Expanded, erl_anno:new(?key(Env, line)), ErlS),
366371
{Erl, NewErlS, NewExS, NewEnv#{versioned_vars := ReadVars}}.
367372

368373
%% Converts a given string (charlist) into quote expression

lib/elixir/src/elixir_env.erl

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-module(elixir_env).
22
-include("elixir.hrl").
33
-export([
4-
new/0, to_caller/1, with_vars/2, reset_vars/1, env_to_ex/1,
4+
new/0, to_caller/1, with_vars/2, reset_vars/1, env_to_ex/1, env_to_ex/2,
55
reset_unused_vars/1, check_unused_vars/2, merge_and_check_unused_vars/3,
66
trace/2, format_error/1,
77
reset_read/2, prepare_write/1, close_write/2
@@ -47,11 +47,19 @@ reset_vars(Env) ->
4747

4848
%% CONVERSIONS
4949

50-
env_to_ex(#{context := match, versioned_vars := Vars}) ->
50+
env_to_ex(Env) ->
51+
env_to_ex(Env, false).
52+
53+
env_to_ex(#{context := match, versioned_vars := Vars}, Prune) ->
5154
Counter = map_size(Vars),
52-
#elixir_ex{prematch={Vars, Counter}, vars={Vars, false}, unused={#{}, Counter}};
53-
env_to_ex(#{versioned_vars := Vars}) ->
54-
#elixir_ex{vars={Vars, false}, unused={#{}, map_size(Vars)}}.
55+
Unused = unused(Vars, Prune),
56+
#elixir_ex{prematch={Vars, Counter}, vars={Vars, false}, unused={Unused, Counter}};
57+
env_to_ex(#{versioned_vars := Vars}, Prune) ->
58+
Unused = unused(Vars, Prune),
59+
#elixir_ex{vars={Vars, false}, unused={Unused, map_size(Vars)}}.
60+
61+
unused(_Vars, false) -> #{};
62+
unused(Vars, true) -> maps:from_list([{K, {0, false}} || K <- maps:to_list(Vars)]).
5563

5664
%% VAR HANDLING
5765

@@ -83,7 +91,7 @@ reset_unused_vars(#elixir_ex{unused={_Unused, Version}} = S) ->
8391

8492
check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) ->
8593
[elixir_errors:form_warn([{line, Line}], E, ?MODULE, {unused_var, Name, Overridden}) ||
86-
{{Name, _}, {Line, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
94+
{{{Name, nil}, _}, {Line, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
8795
E.
8896

8997
merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _Version}}, E) ->
@@ -93,20 +101,18 @@ merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _V
93101

94102
merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) ->
95103
maps:fold(fun
96-
({Name, Count} = Key, false, Acc) ->
97-
Var = {Name, nil},
98-
99-
%% The parent knows it, so we have to propagate it was used up.
104+
({Var, Count} = Key, false, Acc) ->
100105
case Current of
101106
#{Var := CurrentCount} when Count =< CurrentCount ->
107+
%% The parent knows it, so we have to propagate it was used up.
102108
Acc#{Key => false};
103109

104110
#{} ->
105111
Acc
106112
end;
107113

108-
({Name, _Count}, {Line, Overridden}, Acc) ->
109-
case is_unused_var(Name) of
114+
({{Name, Kind}, _Count}, {Line, Overridden}, Acc) ->
115+
case (Kind == nil) andalso is_unused_var(Name) of
110116
true ->
111117
Warn = {unused_var, Name, Overridden},
112118
elixir_errors:form_warn([{line, Line}], E, ?MODULE, Warn);

lib/elixir/src/elixir_erl_var.erl

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
-module(elixir_erl_var).
33
-export([
44
translate/4, assign/2, build/2,
5-
load_binding/1, dump_binding/3,
5+
load_binding/1, dump_binding/4,
66
from_env/1, from_env/2
77
]).
88
-include("elixir.hrl").
@@ -101,9 +101,16 @@ load_binding([], ExVars, ErlVars, Normalized, _Counter) ->
101101
load_pair({Key, Value}) when is_atom(Key) -> {{Key, nil}, Value};
102102
load_pair({Pair, Value}) -> {Pair, Value}.
103103

104-
dump_binding(Binding, #elixir_ex{vars={ExVars, _}}, #elixir_erl{var_names=ErlVars}) ->
104+
dump_binding(Binding, ErlS, ExS, PruneBefore) ->
105+
#elixir_erl{var_names=ErlVars} = ErlS,
106+
#elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS,
107+
105108
maps:fold(fun
106-
({Var, Kind} = Pair, Version, Acc) when is_atom(Kind) ->
109+
({Var, Kind} = Pair, Version, Acc)
110+
% The variable has to have an atom context
111+
% and it must be versioned after the original
112+
% binding or be a used part of the original binding
113+
when is_atom(Kind), (Version >= PruneBefore orelse map_get({Pair, Version}, Unused) == false) ->
107114
Key = case Kind of
108115
nil -> Var;
109116
_ -> Pair

lib/elixir/src/elixir_expand.erl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -607,16 +607,16 @@ mapfold(_Fun, S, E, [], Acc) ->
607607

608608
%% Match/var helpers
609609

610-
var_unused({Name, Kind}, Meta, Version, Unused, Override) ->
610+
var_unused({_, Kind} = Pair, Meta, Version, Unused, Override) ->
611611
case (Kind == nil) andalso should_warn(Meta) of
612-
true -> Unused#{{Name, Version} => {?line(Meta), Override}};
612+
true -> Unused#{{Pair, Version} => {?line(Meta), Override}};
613613
false -> Unused
614614
end.
615615

616-
var_used({Name, Kind}, Version, Unused) ->
617-
case Kind of
618-
nil -> Unused#{{Name, Version} => false};
619-
_ -> Unused
616+
var_used({_, Kind} = Pair, Version, Unused) ->
617+
if
618+
is_atom(Kind) -> Unused#{{Pair, Version} => false};
619+
true -> Unused
620620
end.
621621

622622
maybe_warn_underscored_var_repeat(Meta, Name, Kind, E) ->

lib/elixir/test/elixir/code_test.exs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,35 @@ defmodule CodeTest do
177177
assert Macro.Env.vars(env) == [{:x, nil}]
178178
end
179179

180+
test "eval_quoted_with_env/3 with pruning" do
181+
env = Code.env_for_eval(__ENV__)
182+
183+
fun = fn quoted, binding ->
184+
{_, binding, _} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
185+
binding
186+
end
187+
188+
assert fun.(quote(do: 123), []) == []
189+
assert fun.(quote(do: 123), x: 2, y: 3) == []
190+
191+
assert fun.(quote(do: var!(x) = 1), []) == [x: 1]
192+
assert fun.(quote(do: var!(x) = 1), x: 2, y: 3) == [x: 1]
193+
194+
assert fun.(quote(do: var!(x, :foo) = 1), []) == [{{:x, :foo}, 1}]
195+
assert fun.(quote(do: var!(x, :foo) = 1), x: 2, y: 3) == [{{:x, :foo}, 1}]
196+
197+
assert fun.(quote(do: var!(x, :foo) = 1), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) ==
198+
[{{:x, :foo}, 1}]
199+
200+
assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), []) == []
201+
assert fun.(quote(do: fn -> var!(x, :foo) = 1 end), x: 1, y: 2) == []
202+
203+
assert fun.(quote(do: fn -> var!(x) end), x: 2, y: 3) == [x: 2]
204+
205+
assert fun.(quote(do: fn -> var!(x, :foo) end), [{{:x, :foo}, 2}, {{:y, :foo}, 3}]) ==
206+
[{{:x, :foo}, 2}]
207+
end
208+
180209
test "compile_file/1" do
181210
assert Code.compile_file(fixture_path("code_sample.exs")) == []
182211
refute fixture_path("code_sample.exs") in Code.required_files()

lib/elixir/test/erlang/atom_test.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
-include_lib("eunit/include/eunit.hrl").
44

55
eval(Content) ->
6-
{Value, Binding, _} =
7-
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
6+
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
7+
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
88
{Value, Binding}.
99

1010
kv([{Key, nil}]) -> Key.

lib/elixir/test/erlang/function_test.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
-include_lib("eunit/include/eunit.hrl").
33

44
eval(Content) ->
5-
{Value, Binding, _} =
6-
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
5+
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
6+
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
77
{Value, lists:sort(Binding)}.
88

99
function_arg_do_end_test() ->

lib/elixir/test/erlang/string_test.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
-include_lib("eunit/include/eunit.hrl").
44

55
eval(Content) ->
6-
{Value, Binding, _} =
7-
elixir:eval_forms(elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []), [], []),
6+
Quoted = elixir:'string_to_quoted!'(Content, 1, 1, <<"nofile">>, []),
7+
{Value, Binding, _} = elixir:eval_forms(Quoted, [], elixir:env_for_eval([])),
88
{Value, Binding}.
99

1010
extract_interpolations(String) ->

lib/iex/lib/iex/pry.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ defmodule IEx.Pry do
8282

8383
def pry(binding, opts) when is_list(opts) do
8484
vars = for {k, _} when is_atom(k) <- binding, do: {k, nil}
85-
pry(binding, opts |> :elixir.env_for_eval() |> :elixir_env.with_vars(vars))
85+
pry(binding, opts |> Code.env_for_eval() |> :elixir_env.with_vars(vars))
8686
end
8787

8888
@doc false

0 commit comments

Comments
 (0)