Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 53 additions & 31 deletions lib/elixir/lib/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,20 @@ defmodule Code do
]

@typedoc """
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
Options for evaluation environment, accepted by `env_for_eval/1`.
"""
@type env_eval_opts :: [
file: binary(),
line: pos_integer(),
module: module(),
prune_binding: boolean()
]
@type env_eval_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:module, module()}

@typedoc """
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
and `eval_quoted_with_env/4`.
"""
@type eval_opt ::
{:prune_binding, boolean()}
| {:dbg_callback, {module(), atom(), list()}}

@boolean_compiler_options [
:docs,
Expand Down Expand Up @@ -573,9 +579,11 @@ defmodule Code do

## Options

It accepts the same options as `env_for_eval/1`. Additionally, you may
also pass an environment as second argument, so the evaluation happens
within that environment.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
as third argument, so the evaluation happens within that environment.

## Return

Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
Expand Down Expand Up @@ -605,7 +613,7 @@ defmodule Code do
iex> Enum.sort(binding)
[a: 3, b: 2]

For convenience, you can pass `__ENV__/0` as the `opts` argument and
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and
all imports, requires and aliases defined in the current environment
will be automatically carried over:

Expand All @@ -617,15 +625,16 @@ defmodule Code do
[a: 1, b: 2]

"""
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_string(string, binding \\ [], opts \\ [])
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that it's not possible to pass %Macro.Env{} and eval option such as :prune_binding, but that's an inherent limitation of the current API. We could add opts \\ [] as another argument, but having three arguments with default values is likely too much.

It's not necessarily an issue though, because Code.eval_quoted_with_env/4 already provides more control by having separate env and opts options.

{term, binding}
def eval_string(string, binding \\ [], opts_or_env \\ [])

def eval_string(string, binding, %Macro.Env{} = env) do
validated_eval_string(string, validate_binding(binding), env)
validated_eval_string(string, validate_binding(binding), env_for_eval(env), [])
end

def eval_string(string, binding, opts) when is_list(opts) do
validated_eval_string(string, validate_binding(binding), opts)
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts)
end

defp validate_binding(binding) when is_list(binding), do: binding
Expand All @@ -634,10 +643,10 @@ defmodule Code do
raise ArgumentError, "binding must be a list, got: #{inspect(binding)}"
end

defp validated_eval_string(string, binding, opts_or_env) do
%{line: line, file: file} = env = env_for_eval(opts_or_env)
defp validated_eval_string(string, binding, env, opts) do
%{line: line, file: file} = env
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts])
{value, binding}
end

Expand Down Expand Up @@ -1140,7 +1149,8 @@ defmodule Code do
returned quoted expressions (instead of evaluated).

See `eval_string/3` for a description of arguments and return types.
The options are described under `env_for_eval/1`.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`.

## Examples

Expand All @@ -1162,11 +1172,20 @@ defmodule Code do
[a: 1, b: 2]

"""
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
{value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
{term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ [])

def eval_quoted(quoted, binding, %Macro.Env{} = env) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
end

def eval_quoted(quoted, binding, opts) when is_list(opts) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
end

defp eval_quoted(quoted, binding, env, opts) do
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
{value, binding}
end

Expand Down Expand Up @@ -1194,14 +1213,9 @@ defmodule Code do

* `:module` - the module to run the environment on

* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
"""
@doc since: "1.14.0"
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)

@doc """
Expand All @@ -1215,11 +1229,19 @@ defmodule Code do

## Options

It accepts the same options as `env_for_eval/1`.
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.

* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
used in the evaluated code. It must be a `{module, function, args}`
tuple, see `dbg/2` for more details.

"""
@doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) ::
{term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
Expand Down
10 changes: 9 additions & 1 deletion lib/elixir/lib/kernel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6326,7 +6326,15 @@ defmodule Kernel do
"""
@doc since: "1.14.0"
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
# The compiling process may override the callback by putting it in
# the process dictionary.
dbg_callback =
case :erlang.get({:elixir, :dbg_callback}) do
:undefined -> Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
value -> value
end

{mod, fun, args} = dbg_callback
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
end

Expand Down
52 changes: 30 additions & 22 deletions lib/elixir/src/elixir.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
-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,
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_forms/4, eval_quoted/3, eval_quoted/4,
erl_eval/3, eval_local_handler/2, eval_external_handler/3, emit_warnings/3
]).
-include("elixir.hrl").
Expand Down Expand Up @@ -304,27 +304,35 @@ eval_forms(Tree, Binding, OrigE) ->
eval_forms(Tree, Binding, OrigE, []).
eval_forms(Tree, Binding, OrigE, Opts) ->
Prune = proplists:get_value(prune_binding, Opts, false),
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
E = elixir_env:with_vars(OrigE, ExVars),
ExS = elixir_env:env_to_ex(E),
ErlS = elixir_erl_var:from_env(E, ErlVars),
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),

case Erl of
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
if
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
true -> {Value, Binding, NewE}
end;

_ ->
{value, Value, NewBinding} = erl_eval(Erl, ErlBinding, NewE),
PruneBefore = if Prune -> length(Binding); true -> -1 end,

{DumpedBinding, DumpedVars} =
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),

{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
case proplists:get_value(dbg_callback, Opts) of
undefined -> ok;
DbgCallback -> erlang:put({elixir, dbg_callback}, DbgCallback)
end,
try
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
E = elixir_env:with_vars(OrigE, ExVars),
ExS = elixir_env:env_to_ex(E),
ErlS = elixir_erl_var:from_env(E, ErlVars),
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),

case Erl of
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
if
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
true -> {Value, Binding, NewE}
end;

_ ->
{value, Value, NewBinding} = erl_eval(Erl, ErlBinding, NewE),
PruneBefore = if Prune -> length(Binding); true -> -1 end,

{DumpedBinding, DumpedVars} =
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),

{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
end
after
erlang:erase({elixir, dbg_callback})
end.

%% Evaluate Erlang code with careful handling of local and external functions
Expand Down
50 changes: 50 additions & 0 deletions lib/elixir/test/elixir/code_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,21 @@ defmodule CodeTest do
}
] = diagnostics
end

test "with :prune_binding" do
opts = [prune_binding: true]
assert {2, [x: 1]} = Code.eval_string("x + 1", [x: 1, y: 2], opts)
end

test "with :debug_callback" do
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
assert {2, _binding} = Code.eval_string("dbg(1)", [], opts)

# Maintains the default behaviour when called again without the option.
ExUnit.CaptureIO.capture_io(fn ->
assert {1, _binding} = Code.eval_string("dbg(1)", [])
end)
end
end

describe "eval_quoted/1" do
Expand All @@ -270,6 +285,23 @@ defmodule CodeTest do
{"foo", []} = Code.eval_string("Chars.to_string(:foo)", [], __ENV__)
end
end

test "with :prune_binding" do
quoted = quote(do: var!(x) + 1)
opts = [prune_binding: true]
assert {2, [x: 1]} = Code.eval_quoted(quoted, [x: 1, y: 2], opts)
end

test "with :dbg_callback" do
quoted = quote(do: dbg(1))
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
assert {2, _binding} = Code.eval_quoted(quoted, [], opts)

# Maintains the default behaviour when called again without the option.
ExUnit.CaptureIO.capture_io(fn ->
assert {1, _binding} = Code.eval_quoted(quoted, [])
end)
end
end

test "eval_file/1" do
Expand Down Expand Up @@ -381,6 +413,24 @@ defmodule CodeTest do
assert binding == []
assert Macro.Env.vars(env) == []
end

test "with :dbg_callback" do
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a single test for :dbg_callback is enough, since I added a test for :prune_binding to all of the functions to make sure the operations are propagated. Let me know if you have a preference!

quoted = quote(do: dbg(1))
env = Code.env_for_eval(__ENV__)
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
assert {2, _binding, _env} = Code.eval_quoted_with_env(quoted, [], env, opts)

# Maintains the default behaviour when called again without the option.
ExUnit.CaptureIO.capture_io(fn ->
assert {1, _binding, _env} = Code.eval_quoted_with_env(quoted, [], env, [])
end)
end
end

def dbg_callback_add_one(code, _options, _caller) do
quote do
unquote(code) + 1
end
end

describe "compile_file/1" do
Expand Down
Loading