From 42b7c4ba63da83e2519d5560b4591d03aef7f17f Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Sat, 3 Oct 2026 17:20:13 +0900 Subject: [PATCH 1/2] Recognize `GlobalRef` callees in `ismethod_with_name` and remove `is_Typeof_for_anonymous_methoddef` `ismethod_with_name` recognized calls to `Core.svec`, `Core.Typeof`, `Core.apply_type` and `UnionAll` only when the callee was a `QuoteNode`. JuliaInterpreter produces that form only when it folds `const` globals: for optimized frames, and on Julia 1.12+ not in top-level code. Without folding, the callees are `GlobalRef`s. For example, on Julia 1.12+ `ismethod_with_name` failed to find `Foo` in the signature of `(::Foo)(x) = x`. On Julia 1.10, the selective evaluation of `struct NoParam end` in the tests broke for a frame built with `optimize=false`. Match the callees with `callee_matches`, which accepts both forms, as the other callee checks in this package already do. `is_Typeof_for_anonymous_methoddef` had the same problem, but is removed instead. It made `signature` return `nothing` on reaching `Core.Typeof(name)` for an undefined `name`, so that `rename_framemethods!` left the name of a gensymmed method as it is when the method's caller is not defined. Since Julia 1.11, lowering loads `name` in a separate statement that `Core.Typeof` refers to. `signature` evaluates that statement first and throws an `UndefVarError` before it reaches the check, so the check no longer fires. This was the warning reported in #110. #111 fixed it by having `set_to_running_name!` catch the `UndefVarError`, which leaves the name as it is, the same result the check gives. The check still fires on Julia 1.10, but the catch handles those cases as well. The only difference is that `signature(frame, pc)` throws in such a case on Julia 1.10, as it already does on Julia 1.11+. The LoweredCodeUtils tests pass on Julia 1.13 and 1.10, including with a JuliaInterpreter that never folds `const` globals. The Revise test suite gives the same results as before on Julia 1.13 and 1.10, without new warnings. Co-Authored-By: Claude Opus 5.5 --- src/signatures.jl | 16 ---------------- src/utils.jl | 8 ++++---- test/signatures.jl | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/signatures.jl b/src/signatures.jl index 0f90bcb..d839ce5 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -51,13 +51,10 @@ In this case, `lastpc == pc`. If no 3-argument `:method` expression is found, `nothing` will be returned in place of `(mt, sigt)`. """ function signature(interp::Interpreter, frame::Frame, @nospecialize(stmt), pc::Int) - mod = moduleof(frame) lastpc = frame.pc = pc while !ismethod3(stmt) # wait for the 3-arg :method or 4-arg define_method if isanonymous_typedef(stmt) lastpc = pc = step_through_methoddef(interp, frame, stmt) # define an anonymous function - elseif is_Typeof_for_anonymous_methoddef(stmt, frame.framecode.src.code, mod) - return nothing, pc else lastpc = pc pc = throw_if_breakpoint(step_expr!(interp, frame, stmt, true)) @@ -84,19 +81,6 @@ end signature(interp::Interpreter, frame::Frame, pc::Int) = signature(interp, frame, pc_expr(frame, pc), pc) signature(frame::Frame, pc::Int) = signature(RecursiveInterpreter(), frame, pc) -function is_Typeof_for_anonymous_methoddef(@nospecialize(stmt), code::Vector{Any}, mod::Module) - isexpr(stmt, :call) || return false - f = stmt.args[1] - isa(f, QuoteNode) || return false - f.value === Core.Typeof || return false - arg1 = stmt.args[2] - if isa(arg1, SSAValue) - arg1 = code[arg1.id] - end - arg1 isa Symbol || return false - return !isdefined(mod, arg1) -end - function minid(@nospecialize(node), stmts, id) if isa(node, SSAValue) id = min(id, node.id) diff --git a/src/utils.jl b/src/utils.jl index f7a0047..36b7056 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -150,16 +150,16 @@ function ismethod_with_name(src::CodeInfo, @nospecialize(stmt), target::Abstract while !isdone if name isa AnySSAValue || name isa AnySlotNumber name = src.code[name.id] - elseif isexpr(name, :call) && is_quotenode_egal(name.args[1], Core.svec) + elseif isexpr(name, :call) && callee_matches(name.args[1], Core, :svec) name = name.args[2] - elseif isexpr(name, :call) && is_quotenode_egal(name.args[1], Core.Typeof) + elseif isexpr(name, :call) && callee_matches(name.args[1], Core, :Typeof) name = name.args[2] - elseif isexpr(name, :call) && is_quotenode_egal(name.args[1], Core.apply_type) + elseif isexpr(name, :call) && callee_matches(name.args[1], Core, :apply_type) for arg in name.args[2:end] ismethod_with_name(src, arg, target; reentrant=true) && return true end isdone = true - elseif isexpr(name, :call) && is_quotenode_egal(name.args[1], UnionAll) + elseif isexpr(name, :call) && callee_matches(name.args[1], Core, :UnionAll) for arg in name.args[2:end] ismethod_with_name(src, arg, target; reentrant=true) && return true end diff --git a/test/signatures.jl b/test/signatures.jl index 26c51fd..1f5570e 100644 --- a/test/signatures.jl +++ b/test/signatures.jl @@ -627,6 +627,24 @@ end @test LoweredCodeUtils.identify_framemethod_calls(frame) isa Any # must not throw end +module FoldedCallees +struct Callable end +end + +@testset "Callees with and without JuliaInterpreter's const folding" begin + # JuliaInterpreter folds `const` callees such as `Core.svec` into `QuoteNode`s only when + # it optimizes the frame, and not in top-level code on Julia 1.12+; otherwise they remain + # `GlobalRef`s. Both forms must be recognized. + lwr = Meta.lower(FoldedCallees, :((::Callable)(x) = x)) + for optimize in (true, false) + src = Frame(FoldedCallees, lwr.args[1]::CodeInfo; optimize).framecode.src + # the method has no name, so its signature is walked to find `Callable` + @test any(src.code) do stmt + LoweredCodeUtils.ismethod3(stmt) && LoweredCodeUtils.ismethod_with_name(src, stmt, "Callable") + end + end +end + module BreakpointRefTest end @testset "BreakpointRef from JuliaInterpreter is an error, not a pc" begin From b82122968820f1670fe6dd695bd8e1122d026b9b Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Sat, 3 Oct 2026 18:08:13 +0900 Subject: [PATCH 2/2] Walk the signature of `define_method` calls in `ismethod_with_name` On Julia 1.14, lowering defines methods with `Core.define_method(mod, name, sigdata, codeinfo)` calls instead of 3-argument `:method` expressions. For a method without a name, `ismethod_with_name` walks the signature to find the target name, but it took the signature only from a `:method` expression. It thus missed the name in the `define_method` form, e.g. `Callable` in the signature of `(::Callable)(x) = x`, which the test added in the previous commit caught on nightly. Add `method_sig`, which returns the signature data of either form, and use it in `ismethod_with_name` and `signature_top`, which already handled both forms inline. The LoweredCodeUtils tests pass on Julia 1.14.0-DEV.3439, 1.13 and 1.10. Co-Authored-By: Claude Opus 5.5 --- src/signatures.jl | 5 +---- src/utils.jl | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/signatures.jl b/src/signatures.jl index d839ce5..7cda176 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -96,10 +96,7 @@ end function signature_top(frame, stmt::Expr, pc) @assert ismethod3(stmt) - if is_define_method_call_4arg(stmt) - return minid(stmt.args[4], frame.framecode.src.code, pc) - end - return minid(stmt.args[2], frame.framecode.src.code, pc) + return minid(method_sig(stmt), frame.framecode.src.code, pc) end function step_through_methoddef(interp::Interpreter, frame::Frame, @nospecialize(stmt)) diff --git a/src/utils.jl b/src/utils.jl index 36b7056..0526918 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -126,6 +126,16 @@ function method_module(@nospecialize(stmt)) return nothing end +# Extract the signature data from a method3 statement. +# For Expr(:method, name, sig, body) it's args[2]; for define_method(mod, name, sigdata, body) it's args[4]. +function method_sig(@nospecialize(stmt)) + if is_define_method_call_4arg(stmt) + return stmt.args[4] + else + return stmt.args[2] + end +end + # Extract the CodeInfo body from a method3 statement. # For Expr(:method, name, sig, body) it's args[3]; for define_method(mod, name, sigdata, body) it's args[5]. function method_body(@nospecialize(stmt)) @@ -142,8 +152,8 @@ function ismethod_with_name(src::CodeInfo, @nospecialize(stmt), target::Abstract else ismethod3(stmt) || return false name = method_name(stmt) - if name === nothing && isexpr(stmt, :method) - name = stmt.args[2] + if name === nothing + name = method_sig(stmt) end end isdone = false