Skip to content

Commit 7967ff9

Browse files
Add :dbg_callback option to Code eval functions (#15189)
1 parent fb2657a commit 7967ff9

4 files changed

Lines changed: 142 additions & 54 deletions

File tree

lib/elixir/lib/code.ex

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,20 @@ defmodule Code do
291291
]
292292

293293
@typedoc """
294-
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
294+
Options for evaluation environment, accepted by `env_for_eval/1`.
295295
"""
296-
@type env_eval_opts :: [
297-
file: binary(),
298-
line: pos_integer(),
299-
module: module(),
300-
prune_binding: boolean()
301-
]
296+
@type env_eval_opt ::
297+
{:file, binary()}
298+
| {:line, pos_integer()}
299+
| {:module, module()}
300+
301+
@typedoc """
302+
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
303+
and `eval_quoted_with_env/4`.
304+
"""
305+
@type eval_opt ::
306+
{:prune_binding, boolean()}
307+
| {:dbg_callback, {module(), atom(), list()}}
302308

303309
@boolean_compiler_options [
304310
:docs,
@@ -573,9 +579,11 @@ defmodule Code do
573579
574580
## Options
575581
576-
It accepts the same options as `env_for_eval/1`. Additionally, you may
577-
also pass an environment as second argument, so the evaluation happens
578-
within that environment.
582+
It accepts the same options as both `env_for_eval/1` and
583+
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
584+
as third argument, so the evaluation happens within that environment.
585+
586+
## Return
579587
580588
Returns a tuple of the form `{value, binding}`, where `value` is the value
581589
returned from evaluating `string`. If an error occurs while evaluating
@@ -605,7 +613,7 @@ defmodule Code do
605613
iex> Enum.sort(binding)
606614
[a: 3, b: 2]
607615
608-
For convenience, you can pass `__ENV__/0` as the `opts` argument and
616+
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and
609617
all imports, requires and aliases defined in the current environment
610618
will be automatically carried over:
611619
@@ -617,15 +625,16 @@ defmodule Code do
617625
[a: 1, b: 2]
618626
619627
"""
620-
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
621-
def eval_string(string, binding \\ [], opts \\ [])
628+
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
629+
{term, binding}
630+
def eval_string(string, binding \\ [], opts_or_env \\ [])
622631

623632
def eval_string(string, binding, %Macro.Env{} = env) do
624-
validated_eval_string(string, validate_binding(binding), env)
633+
validated_eval_string(string, validate_binding(binding), env_for_eval(env), [])
625634
end
626635

627636
def eval_string(string, binding, opts) when is_list(opts) do
628-
validated_eval_string(string, validate_binding(binding), opts)
637+
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts)
629638
end
630639

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

637-
defp validated_eval_string(string, binding, opts_or_env) do
638-
%{line: line, file: file} = env = env_for_eval(opts_or_env)
646+
defp validated_eval_string(string, binding, env, opts) do
647+
%{line: line, file: file} = env
639648
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
640-
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
649+
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts])
641650
{value, binding}
642651
end
643652

@@ -1140,7 +1149,8 @@ defmodule Code do
11401149
returned quoted expressions (instead of evaluated).
11411150
11421151
See `eval_string/3` for a description of arguments and return types.
1143-
The options are described under `env_for_eval/1`.
1152+
It accepts the same options as both `env_for_eval/1` and
1153+
`eval_quoted_with_env/4`.
11441154
11451155
## Examples
11461156
@@ -1162,11 +1172,20 @@ defmodule Code do
11621172
[a: 1, b: 2]
11631173
11641174
"""
1165-
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
1166-
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
1167-
{value, binding, _env} =
1168-
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
1175+
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) ::
1176+
{term, binding}
1177+
def eval_quoted(quoted, binding \\ [], env_or_opts \\ [])
11691178

1179+
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
1180+
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
1181+
end
1182+
1183+
def eval_quoted(quoted, binding, opts) when is_list(opts) do
1184+
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
1185+
end
1186+
1187+
defp eval_quoted(quoted, binding, env, opts) do
1188+
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
11701189
{value, binding}
11711190
end
11721191

@@ -1194,14 +1213,9 @@ defmodule Code do
11941213
11951214
* `:module` - the module to run the environment on
11961215
1197-
* `:prune_binding` - (since v1.14.2) prune binding to keep only
1198-
variables read or written by the evaluated code. Note that
1199-
variables used by modules are always pruned, even if later used
1200-
by the modules. You can submit to the `:on_module` tracer event
1201-
and access the variables used by the module from its environment.
12021216
"""
12031217
@doc since: "1.14.0"
1204-
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
1218+
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
12051219
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
12061220

12071221
@doc """
@@ -1215,11 +1229,19 @@ defmodule Code do
12151229
12161230
## Options
12171231
1218-
It accepts the same options as `env_for_eval/1`.
1232+
* `:prune_binding` - (since v1.14.2) prune binding to keep only
1233+
variables read or written by the evaluated code. Note that
1234+
variables used by modules are always pruned, even if later used
1235+
by the modules. You can submit to the `:on_module` tracer event
1236+
and access the variables used by the module from its environment.
1237+
1238+
* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
1239+
used in the evaluated code. It must be a `{module, function, args}`
1240+
tuple, see `dbg/2` for more details.
12191241
12201242
"""
12211243
@doc since: "1.14.0"
1222-
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
1244+
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) ::
12231245
{term, binding, Macro.Env.t()}
12241246
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
12251247
when is_list(binding) do

lib/elixir/lib/kernel.ex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6326,7 +6326,15 @@ defmodule Kernel do
63266326
"""
63276327
@doc since: "1.14.0"
63286328
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
6329-
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
6329+
# The compiling process may override the callback by putting it in
6330+
# the process dictionary.
6331+
dbg_callback =
6332+
case :erlang.get({:elixir, :dbg_callback}) do
6333+
:undefined -> Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
6334+
value -> value
6335+
end
6336+
6337+
{mod, fun, args} = dbg_callback
63306338
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
63316339
end
63326340

lib/elixir/src/elixir.erl

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
-export([start/2, stop/1, config_change/3]).
1111
-export([
1212
string_to_tokens/5, tokens_to_quoted/3, string_to_quoted/5, 'string_to_quoted!'/5,
13-
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3, eval_quoted/4,
13+
env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_forms/4, eval_quoted/3, eval_quoted/4,
1414
erl_eval/3, eval_local_handler/2, eval_external_handler/3, emit_warnings/3
1515
]).
1616
-include("elixir.hrl").
@@ -304,27 +304,35 @@ eval_forms(Tree, Binding, OrigE) ->
304304
eval_forms(Tree, Binding, OrigE, []).
305305
eval_forms(Tree, Binding, OrigE, Opts) ->
306306
Prune = proplists:get_value(prune_binding, Opts, false),
307-
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
308-
E = elixir_env:with_vars(OrigE, ExVars),
309-
ExS = elixir_env:env_to_ex(E),
310-
ErlS = elixir_erl_var:from_env(E, ErlVars),
311-
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
312-
313-
case Erl of
314-
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
315-
if
316-
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
317-
true -> {Value, Binding, NewE}
318-
end;
319-
320-
_ ->
321-
{value, Value, NewBinding} = erl_eval(Erl, ErlBinding, NewE),
322-
PruneBefore = if Prune -> length(Binding); true -> -1 end,
323-
324-
{DumpedBinding, DumpedVars} =
325-
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),
326-
327-
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
307+
case proplists:get_value(dbg_callback, Opts) of
308+
undefined -> ok;
309+
DbgCallback -> erlang:put({elixir, dbg_callback}, DbgCallback)
310+
end,
311+
try
312+
{ExVars, ErlVars, ErlBinding} = elixir_erl_var:load_binding(Binding, Prune),
313+
E = elixir_env:with_vars(OrigE, ExVars),
314+
ExS = elixir_env:env_to_ex(E),
315+
ErlS = elixir_erl_var:from_env(E, ErlVars),
316+
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, ErlS, ExS, E),
317+
318+
case Erl of
319+
{Literal, _, Value} when Literal == atom; Literal == float; Literal == integer ->
320+
if
321+
Prune -> {Value, [], NewE#{versioned_vars := #{}}};
322+
true -> {Value, Binding, NewE}
323+
end;
324+
325+
_ ->
326+
{value, Value, NewBinding} = erl_eval(Erl, ErlBinding, NewE),
327+
PruneBefore = if Prune -> length(Binding); true -> -1 end,
328+
329+
{DumpedBinding, DumpedVars} =
330+
elixir_erl_var:dump_binding(NewBinding, NewErlS, NewExS, PruneBefore),
331+
332+
{Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}}
333+
end
334+
after
335+
erlang:erase({elixir, dbg_callback})
328336
end.
329337

330338
%% Evaluate Erlang code with careful handling of local and external functions

lib/elixir/test/elixir/code_test.exs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,21 @@ defmodule CodeTest do
246246
}
247247
] = diagnostics
248248
end
249+
250+
test "with :prune_binding" do
251+
opts = [prune_binding: true]
252+
assert {2, [x: 1]} = Code.eval_string("x + 1", [x: 1, y: 2], opts)
253+
end
254+
255+
test "with :debug_callback" do
256+
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
257+
assert {2, _binding} = Code.eval_string("dbg(1)", [], opts)
258+
259+
# Maintains the default behaviour when called again without the option.
260+
ExUnit.CaptureIO.capture_io(fn ->
261+
assert {1, _binding} = Code.eval_string("dbg(1)", [])
262+
end)
263+
end
249264
end
250265

251266
describe "eval_quoted/1" do
@@ -270,6 +285,23 @@ defmodule CodeTest do
270285
{"foo", []} = Code.eval_string("Chars.to_string(:foo)", [], __ENV__)
271286
end
272287
end
288+
289+
test "with :prune_binding" do
290+
quoted = quote(do: var!(x) + 1)
291+
opts = [prune_binding: true]
292+
assert {2, [x: 1]} = Code.eval_quoted(quoted, [x: 1, y: 2], opts)
293+
end
294+
295+
test "with :dbg_callback" do
296+
quoted = quote(do: dbg(1))
297+
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
298+
assert {2, _binding} = Code.eval_quoted(quoted, [], opts)
299+
300+
# Maintains the default behaviour when called again without the option.
301+
ExUnit.CaptureIO.capture_io(fn ->
302+
assert {1, _binding} = Code.eval_quoted(quoted, [])
303+
end)
304+
end
273305
end
274306

275307
test "eval_file/1" do
@@ -381,6 +413,24 @@ defmodule CodeTest do
381413
assert binding == []
382414
assert Macro.Env.vars(env) == []
383415
end
416+
417+
test "with :dbg_callback" do
418+
quoted = quote(do: dbg(1))
419+
env = Code.env_for_eval(__ENV__)
420+
opts = [dbg_callback: {__MODULE__, :dbg_callback_add_one, []}]
421+
assert {2, _binding, _env} = Code.eval_quoted_with_env(quoted, [], env, opts)
422+
423+
# Maintains the default behaviour when called again without the option.
424+
ExUnit.CaptureIO.capture_io(fn ->
425+
assert {1, _binding, _env} = Code.eval_quoted_with_env(quoted, [], env, [])
426+
end)
427+
end
428+
end
429+
430+
def dbg_callback_add_one(code, _options, _caller) do
431+
quote do
432+
unquote(code) + 1
433+
end
384434
end
385435

386436
describe "compile_file/1" do

0 commit comments

Comments
 (0)