From f09730f172fb2ef70551444d9742767ea1400fe9 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Fri, 2 Oct 2026 11:24:17 +0900 Subject: [PATCH] Advance the world at skipped `:latestworld` statements Since Julia 1.12, top-level code advances its world only at `:latestworld` statements, and JuliaInterpreter is going to follow that instead of moving top-level frames to the latest world before every statement. Selective evaluation skips the statements it does not select, `:latestworld` ones included, so later statements could then read bindings in a world prior to their definition, or fail to call methods defined earlier in the thunk. `next_or_nothing!` now advances the world of the frame when it skips a `:latestworld` statement. This covers `selective_eval!` and the callers that skip statements themselves with `next_or_nothing!`, such as Revise. It is compatible with current JuliaInterpreter versions, whose top-level frames are always in the latest world anyway. Selecting the `:latestworld` statements in `lines_required!` instead would grow the slice: a selected statement pulls in its control flow and type definition, so branch conditions, loop iterations, and struct definitions that the slice does not need would get evaluated. Advancing the world while skipping has no effect on the program state, and matches native execution, which advances the world at every `:latestworld` it reaches. Co-Authored-By: Claude Opus 5.5 --- src/signatures.jl | 10 ++++++++++ test/codeedges.jl | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/signatures.jl b/src/signatures.jl index 7cda176..2448267 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -488,12 +488,22 @@ end Advance the program counter without executing the corresponding line. If `frame` is finished, `nextpc` will be `nothing`. + +`next_or_nothing!` still advances the world of `frame` for a `:latestworld` statement, so +that the statements that follow see the definitions made before it. """ next_or_nothing(frame::Frame, pc::Int) = next_or_nothing(RecursiveInterpreter(), frame, pc) next_or_nothing(::Interpreter, frame::Frame, pc::Int) = pc < nstatements(frame.framecode) ? pc+1 : nothing next_or_nothing!(frame::Frame) = next_or_nothing!(RecursiveInterpreter(), frame) function next_or_nothing!(::Interpreter, frame::Frame) pc = frame.pc + # Since Julia 1.12, top-level code advances its world only at `:latestworld`, which + # JuliaInterpreter follows, so skipping one would leave later statements reading + # bindings in a world prior to their definition. This is done here rather than by + # selecting `:latestworld` in `lines_required!`, since a selected statement pulls in its + # control flow and type definition, which would grow the slice by branch conditions, + # loops, and types it does not need. + isexpr(pc_expr(frame, pc), :latestworld) && (frame.world = Base.get_world_counter()) if pc < nstatements(frame.framecode) return frame.pc = pc + 1 end diff --git a/test/codeedges.jl b/test/codeedges.jl index 80f2177..c66c7e6 100644 --- a/test/codeedges.jl +++ b/test/codeedges.jl @@ -725,4 +725,23 @@ end end end +@testset "skipped :latestworld still advances the world" begin + # Selective evaluation skips the `:latestworld` statements it does not select, e.g. + # the one after the anonymous function of `max_values` in the "CodeEdges" tests above, + # which then fail on JuliaInterpreter versions that advance the world of top-level code + # only at `:latestworld`. + m = Module(:SkippedLatestworld) + world = Base.get_world_counter() + Core.eval(m, :(defined_after = 1)) + lwsrc = (Meta.lower(m, :(global gl = 1; gl))::Expr).args[1]::Core.CodeInfo + lwidx = findfirst(stmt -> Meta.isexpr(stmt, :latestworld), lwsrc.code) + if lwidx !== nothing + frame = Frame(m, lwsrc; world) + frame.pc = lwidx + LoweredCodeUtils.next_or_nothing!(frame) + @test frame.pc == lwidx + 1 + @test frame.world > world + end +end + end # module codeedges