Skip to content

Recursively interpret Core.eval expressions - #783

Merged
aviatesk merged 1 commit into
masterfrom
avi/recurse-eval
Oct 3, 2026
Merged

aviatesk merged 1 commit into
masterfrom
avi/recurse-eval

Conversation

@aviatesk

@aviatesk aviatesk commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

With RecursiveInterpreter, the code passed to Core.eval, also through Base.eval, @eval, and include, is now interpreted in top-level frames linked to the calling frame, instead of being evaluated natively. FrameCode replaces the runtime call in the method of Core.eval with a call to eval_in_frame, which evaluate_eval! handles. Like the compilation of llvmcalls, this is done regardless of optimize, so that debugger commands can step into the evaluation also with optimize=false. Breakpoints, stepping, and exception handling extend into the evaluated code: the calling frame's handlers are passed down (caller_will_catch_err), and interpreted dynamic scopes reach both the evaluation and the lowering of the code. Evaluation starts in the latest world while the calling method keeps its own, and :module expressions go through the native module evaluation of Frame. NonRecursiveInterpreter, or adding the method of Core.eval to compiled_methods, keeps native evaluation.

Top-level frames now follow native world semantics: on Julia 1.12 and later they advance their world only at :latestworld, so a nested eval does not by itself advance the enclosing thunk's world. Thunks start in the latest world, which internally created ones take when they are created, and which Frame(mod, ex) and Frame(mod, src::CodeInfo) now use by default. Selective evaluation must still advance the world at the :latestworld statements it skips, which LoweredCodeUtils does in a coordinated release (xref: JuliaDebug/LoweredCodeUtils.jl#171). A top-level frame with a caller, e.g. from an eval in a method, runs in its own world even when a debugger resumes it with a method at the root; the task world is switched only when it differs, to keep stepping fast.

Related fixes to top-level interpretation:

  • A statement that fails to lower throws the error of native evaluation.
  • The frame of a nested :thunk is linked to its caller, so breakpoints, errors, and its value propagate (including a :method returned by older lowering).
  • A ReturnNode value as a surface statement is not taken for the driver frame's own return.
  • get_return of a driver frame returns nothing if its last statement recorded no value, as when an Interpreter overload handles statements.

Stepping with :s inside the evaluated code goes through statements of top-level frames driven by Frame(mod, ex), which builds on the fix of :s for them in #784.

@codecov

codecov Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.18072% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.86%. Comparing base (ff4d2f8) to head (b8925b8).

Files with missing lines Patch % Lines
src/interpret.jl 96.15% 2 Missing ⚠️
src/commands.jl 66.66% 1 Missing ⚠️
src/optimize.jl 90.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #783      +/-   ##
==========================================
+ Coverage   90.83%   90.86%   +0.02%     
==========================================
  Files          12       12              
  Lines        3535     3579      +44     
==========================================
+ Hits         3211     3252      +41     
- Misses        324      327       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aviatesk
aviatesk force-pushed the avi/module-interpretation branch from 152f442 to 5e5d4cd Compare October 3, 2026 10:45
Base automatically changed from avi/module-interpretation to master October 3, 2026 11:32
@aviatesk
aviatesk force-pushed the avi/recurse-eval branch 3 times, most recently from c834403 to 24d6182 Compare October 3, 2026 11:45
aviatesk added a commit that referenced this pull request Oct 3, 2026
`:s` on a statement of a top-level driver frame, as created by
`Frame(mod, ex)` for a `:toplevel` or `:module` expression, entered the
callee of the surface call expression directly. Since the surface expression
is not lowered, this failed when the call's arguments contained calls (e.g.
`f(g(1))` threw "unknown call f introduced by ccall lowering g"), and it
skipped the rest of the statement, such as the assignment of `x = f(1)`.

Such frames come only from `Frame(mod, ex)` called at the root, which
debugging sessions rarely do, but #783
interprets the code passed to `Core.eval` in such frames, so `:s` inside
`@eval`ed or `include`d code takes this path too. The two changes are
independent of each other, but stepping into evaluated code with `:s` needs
both.

Now `:s` enters the lowered frame of the statement like a callee, before any
of the statement's side effects, and stops at its first call. As when
stepping into a method, it steps through the statements before that call:
global declarations, which are builtin calls on some Julia versions
(`Core.set_binding_type!` for typed globals on 1.10-1.11, and
`Core.declare_global` on 1.13+), `:latestworld`, and global loads. The
handling of method frames is factored out together into
`maybe_step_through_prelude!`. `:si` stops at the entry of the lowered frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aviatesk added a commit that referenced this pull request Oct 3, 2026
`:s` on a statement of a top-level driver frame, as created by
`Frame(mod, ex)` for a `:toplevel` or `:module` expression, entered the
callee of the surface call expression directly. Since the surface expression
is not lowered, this failed when the call's arguments contained calls (e.g.
`f(g(1))` threw "unknown call f introduced by ccall lowering g"), and it
skipped the rest of the statement, such as the assignment of `x = f(1)`.

Such frames come only from `Frame(mod, ex)` called at the root, which
debugging sessions rarely do, but #783
interprets the code passed to `Core.eval` in such frames, so `:s` inside
`@eval`ed or `include`d code takes this path too. The two changes are
independent of each other, but stepping into evaluated code with `:s` needs
both.

Now `:s` enters the lowered frame of the statement like a callee, before any
of the statement's side effects, and stops at its first call. As when
stepping into a method, it steps through the statements before that call:
global declarations, which are builtin calls on some Julia versions
(`Core.set_binding_type!` for typed globals on 1.10-1.11, and
`Core.declare_global` on 1.13+), `:latestworld`, and global loads. The
handling of method frames is factored out together into
`maybe_step_through_prelude!`. `:si` stops at the entry of the lowered frame.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With `RecursiveInterpreter`, the code passed to `Core.eval`, also
through `Base.eval`, `@eval`, and `include`, is now interpreted in
top-level frames linked to the calling frame, instead of being evaluated
natively. `FrameCode` replaces the runtime call in the method of
`Core.eval` with a call to `eval_in_frame`, which `evaluate_eval!`
handles. Like the compilation of `llvmcall`s, this is done regardless of
`optimize`, so that debugger commands can step into the evaluation also
with `optimize=false`. Breakpoints, stepping, and exception handling
extend into the evaluated code: the calling frame's handlers are passed
down (`caller_will_catch_err`), and interpreted dynamic scopes reach
both the evaluation and the lowering of the code. Evaluation starts in
the latest world while the calling method keeps its own, and `:module`
expressions go through the native module evaluation of `Frame`.
`NonRecursiveInterpreter`, or adding the method of `Core.eval` to
`compiled_methods`, keeps native evaluation.

Top-level frames now follow native world semantics: on Julia 1.12 and
later they advance their world only at `:latestworld`, so a nested eval
does not by itself advance the enclosing thunk's world. Thunks start in
the latest world, which internally created ones take when they are
created, and which `Frame(mod, ex)` and `Frame(mod, src::CodeInfo)` now
use by default. Selective evaluation must still advance the world at the
`:latestworld` statements it skips, which LoweredCodeUtils does in a
coordinated release. A top-level frame with a caller, e.g. from an eval
in a method, runs in its own world even when a debugger resumes it with
a method at the root; the task world is switched only when it differs,
to keep stepping fast.

Related fixes to top-level interpretation:
- A statement that fails to lower throws the error of native evaluation.
- The frame of a nested `:thunk` is linked to its caller, so
  breakpoints, errors, and its value propagate (including a `:method`
  returned by older lowering).
- A `ReturnNode` value as a surface statement is not taken for the
  driver frame's own return.
- `get_return` of a driver frame returns `nothing` if its last statement
  recorded no value, as when an `Interpreter` overload handles
  statements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aviatesk
aviatesk merged commit 882b8f5 into master Oct 3, 2026
16 checks passed
@aviatesk
aviatesk deleted the avi/recurse-eval branch October 3, 2026 12:50
@aviatesk aviatesk mentioned this pull request Oct 3, 2026
aviatesk referenced this pull request Oct 3, 2026
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
aviatesk added a commit to aviatesk/JETLS.jl that referenced this pull request Oct 4, 2026
JuliaInterpreter 0.12 interprets the code passed to `Core.eval`
(JuliaDebug/JuliaInterpreter.jl#783), and JET 0.12.3 builds on it, so
full analysis of scripts, notebooks and test files now interprets
`@eval`ed code like other top-level code: methods and structs defined
by it are analyzed, `concretization_timeout` can stop it, and the stack
traces of timeout and error reports extend into it. The new JET also
reports malformed module expressions generated by macros as top-level
errors instead of aborting the analysis.

This commit updates JET to the JETLS branch rebased onto JET 0.12.3,
and Revise and TestRunner to revisions that allow JuliaInterpreter
0.12. Revise-based package analysis and package source analysis run
`Core.eval` natively and are unaffected. The documentation and schema
descriptions of `concretization_timeout` no longer list `Core.eval`
among the native calls that cannot be interrupted, and name the
`__init__` functions of modules it evaluates instead.

The concretization timeout test now expects the interpreted frames of
the `@eval`ed `sleep` in script mode. New tests cover diagnostics in
methods and structs defined by `@eval`, and malformed module
expressions generated by macros.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aviatesk added a commit to aviatesk/JETLS.jl that referenced this pull request Oct 4, 2026
JuliaInterpreter 0.12 interprets the code passed to `Core.eval`
(JuliaDebug/JuliaInterpreter.jl#783), and JET 0.12.3 builds on it, so
full analysis of scripts, notebooks and test files now interprets
`@eval`ed code like other top-level code: methods and structs defined
by it are analyzed, `concretization_timeout` can stop it, and the stack
traces of timeout and error reports extend into it. The new JET also
reports malformed module expressions generated by macros as top-level
errors instead of aborting the analysis.

This commit updates JET to the JETLS branch rebased onto JET 0.12.3,
and Revise and TestRunner to revisions that allow JuliaInterpreter
0.12. Revise-based package analysis and package source analysis run
`Core.eval` natively and are unaffected. The documentation and schema
descriptions of `concretization_timeout` no longer list `Core.eval`
among the native calls that cannot be interrupted, and name the
`__init__` functions of modules it evaluates instead.

The concretization timeout test now expects the interpreted frames of
the `@eval`ed `sleep` in script mode. New tests cover diagnostics in
methods and structs defined by `@eval`, and malformed module
expressions generated by macros.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant