From db0a6cc712f82f5cdd4f160e8e55cf312f6d85a9 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 16 Sep 2026 13:24:45 +0900 Subject: [PATCH 1/5] Remove unused variables detected by JETLS --- src/codeedges.jl | 10 ++++------ src/packagedef.jl | 4 ++-- src/signatures.jl | 6 +++--- test/codeedges.jl | 9 +++------ test/signatures.jl | 21 ++++++++++----------- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/codeedges.jl b/src/codeedges.jl index a9926ca..610277f 100644 --- a/src/codeedges.jl +++ b/src/codeedges.jl @@ -326,7 +326,6 @@ function direct_links!(cl::CodeLinks, src::CodeInfo) target = P(SSAValue(i), cl.ssapreds[i]) elseif (lhs_rhs = get_lhs_rhs(stmt); lhs_rhs !== nothing) # An assignment - stmt = stmt::Expr lhs, rhs = lhs_rhs if @issslotnum(lhs) lhs = lhs::AnySlotNumber @@ -481,7 +480,7 @@ struct CodeEdges succs::Vector{Vector{Int}} byname::Dict{GlobalRef,Variable} end -CodeEdges(n::Integer) = CodeEdges([Int[] for i = 1:n], [Int[] for i = 1:n], Dict{GlobalRef,Variable}()) +CodeEdges(n::Integer) = CodeEdges([Int[] for _ = 1:n], [Int[] for _ = 1:n], Dict{GlobalRef,Variable}()) function Base.show(io::IO, edges::CodeEdges) println(io, "CodeEdges:") @@ -526,7 +525,6 @@ function CodeEdges(src::CodeInfo, cl::CodeLinks) for (i, stmt) in enumerate(src.code) # Identify line predecessors for slots and named variables if (lhs_rhs = get_lhs_rhs(stmt); lhs_rhs !== nothing) - stmt = stmt::Expr lhs, _ = lhs_rhs # Mark predecessors and successors of this line by following ssas & named assignments if @issslotnum(lhs) @@ -705,7 +703,7 @@ On return, the complete set of required statements will be marked `true`. `norequire` keyword argument specifies statements (represented as iterator of `Int`s) that should _not_ be marked as a requirement. -For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're +For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src)` if you're extracting method signatures and not evaluating new definitions. """ function lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges, @@ -715,7 +713,7 @@ function lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges: return lines_required!(isrequired, objs, src, edges, controller; kwargs...) end -function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges) +function exclude_named_typedefs(src::CodeInfo) norequire = BitSet() i = 1 nstmts = length(src.code) @@ -1296,7 +1294,7 @@ function print_with_code(io::IO, src::CodeInfo, isrequired::AbstractVector{Bool} preprint(::IO) = nothing preprint(io::IO, idx::Int) = (c = isrequired[idx]; printstyled(io, lpad(idx, nd), ' ', c ? "t " : "f "; color = c ? :cyan : :plain)) postprint(::IO) = nothing - postprint(::IO, idx::Int, bbchanged::Bool) = nothing + postprint(::IO, _idx::Int, _bbchanged::Bool) = nothing print_with_code(preprint, postprint, io, src) end diff --git a/src/packagedef.jl b/src/packagedef.jl index 69a79bb..71c4841 100644 --- a/src/packagedef.jl +++ b/src/packagedef.jl @@ -50,10 +50,10 @@ if ccall(:jl_generating_output, Cint, ()) == 1 edges = CodeEdges(@__MODULE__, src) isrequired = lines_required(GlobalRef(@__MODULE__, :s), src, edges) lines_required(GlobalRef(@__MODULE__, :s), src, edges; norequire=()) - lines_required(GlobalRef(@__MODULE__, :s), src, edges; norequire=exclude_named_typedefs(src, edges)) + lines_required(GlobalRef(@__MODULE__, :s), src, edges; norequire=exclude_named_typedefs(src)) for isreq in (isrequired, convert(Vector{Bool}, isrequired)) lines_required!(isreq, src, edges; norequire=()) - lines_required!(isreq, src, edges; norequire=exclude_named_typedefs(src, edges)) + lines_required!(isreq, src, edges; norequire=exclude_named_typedefs(src)) end frame = Frame(@__MODULE__, src) # selective_eval_fromstart!(frame, isrequired, true) diff --git a/src/signatures.jl b/src/signatures.jl index 409f08c..1e0c421 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -331,7 +331,7 @@ function set_to_running_name!(interp::Interpreter, replacements::Dict{GlobalRef, throw(err) end replacements[callee] = cname - mi = methodinfos[cname] = methodinfos[callee] + methodinfos[cname] = methodinfos[callee] src = frame.framecode.src replacename!(src, callee=>cname) # the method itself return replacements @@ -363,7 +363,7 @@ function _rename_framemethods!(interp::Interpreter, frame::Frame, end end for sc in selfcalls - linetop, linebody, callee, caller = sc.linetop, sc.linebody, sc.callee, sc.caller + linetop, callee = sc.linetop, sc.callee cname = get(replacements, callee, nothing) if cname !== nothing && cname !== callee replacename!(method_body(src.code[linetop])::CodeInfo, callee=>cname) @@ -570,7 +570,7 @@ By default the method will be defined (evaluated). You can prevent this by setti This is recommended if you are simply extracting signatures from code that has already been evaluated. """ function methoddef!(interp::Interpreter, signatures::Vector{MethodInfoKey}, frame::Frame, @nospecialize(stmt), pc::Int; define::Bool=true) - framecode, pcin = frame.framecode, pc + framecode = frame.framecode if ismethod3(stmt) pc3 = pc arg1 = method_name(stmt) diff --git a/test/codeedges.jl b/test/codeedges.jl index fe4ce02..fc0941d 100644 --- a/test/codeedges.jl +++ b/test/codeedges.jl @@ -507,7 +507,7 @@ module ModSelective end frame = Frame(ModEval, ex) src = frame.framecode.src edges = CodeEdges(ModEval, src) - isrequired = minimal_evaluation(@nospecialize(stmt)->(LoweredCodeUtils.ismethod3(stmt),false), src, edges; norequire=exclude_named_typedefs(src, edges)) # initially mark only the constructor + isrequired = minimal_evaluation(@nospecialize(stmt)->(LoweredCodeUtils.ismethod3(stmt),false), src, edges; norequire=exclude_named_typedefs(src)) # initially mark only the constructor bbs = CC.compute_basic_blocks(src.code) for (iblock, block) in enumerate(bbs.blocks) r = LoweredCodeUtils.rng(block) @@ -545,7 +545,7 @@ module ModSelective end src = thk.args[1] edges = CodeEdges(Main, src) idx = findfirst(LoweredCodeUtils.ismethod, src.code) - lr = lines_required(idx, src, edges; norequire=exclude_named_typedefs(src, edges)) + lr = lines_required(idx, src, edges; norequire=exclude_named_typedefs(src)) idx = findfirst(@nospecialize(stmt)->Meta.isexpr(stmt, :(=)) && Meta.isexpr(stmt.args[2], :call) && is_global_ref(stmt.args[2].args[1], Core, :Box), src.code) @test lr[idx] # but make sure we don't break primitivetype & abstracttype (https://github.com/timholy/Revise.jl/pull/611) @@ -553,7 +553,6 @@ module ModSelective end primitive type WindowsRawSocket sizeof(Ptr) * 8 end end) src = thk.args[1] - edges = CodeEdges(Main, src) idx = findfirst(istypedef, src.code) r = LoweredCodeUtils.typedef_range(src, idx) # 1 before :latestworld, 2 after @@ -592,14 +591,12 @@ module ModSelective end # CodeEdges edges = CodeEdges(Main, src) show(io, edges) - str = String(take!(io)) LoweredCodeUtils.print_with_code(io, src, edges) - str = String(take!(io)) # Works with Frames too frame = Frame(ModSelective, ex) edges = CodeEdges(ModSelective, frame.framecode.src) LoweredCodeUtils.print_with_code(io, frame, edges) - str = String(take!(io)) + _ = String(take!(io)) # display slot names ex = :(let diff --git a/test/signatures.jl b/test/signatures.jl index ed41389..41119a1 100644 --- a/test/signatures.jl +++ b/test/signatures.jl @@ -5,7 +5,6 @@ using InteractiveUtils using CodeTracking: MethodInfoKey using JuliaInterpreter using Core: CodeInfo -using Base.Meta: isexpr using Test module Lowering @@ -115,7 +114,7 @@ bodymethtest5(x, y=Dict(1=>2)) = 5 Core.eval(Lowering, ex) frame = Frame(Lowering, ex) rename_framemethods!(frame) - pc = methoddefs!(signatures, frame; define=false) + methoddefs!(signatures, frame; define=false) push!(newcode, frame.framecode.src) end @@ -234,7 +233,7 @@ bodymethtest5(x, y=Dict(1=>2)) = 5 signatures = MethodInfoKey[] methoddef!(signatures, frame; define=false) @test length(signatures) == 1 - mt, sig = first(signatures) + _mt, sig = first(signatures) @test sig == which(Base.max_values, Tuple{Type{Int16}}).sig # define @@ -301,10 +300,10 @@ bodymethtest5(x, y=Dict(1=>2)) = 5 empty!(signatures) stmt = JuliaInterpreter.pc_expr(frame) if !LoweredCodeUtils.ismethod(stmt) - pc = JuliaInterpreter.next_until!(LoweredCodeUtils.ismethod, frame, true) + JuliaInterpreter.next_until!(LoweredCodeUtils.is_frame_at_method, frame, true) end - pc, _ = methoddef!(signatures, frame; define=false) # this tests that the return isn't `nothing` - pc, _ = methoddef!(signatures, frame; define=false) + methoddef!(signatures, frame; define=false) # this tests that the return isn't `nothing` + methoddef!(signatures, frame; define=false) @test length(signatures) == 2 # both the GeneratedFunctionStub and the main method # With anonymous functions in signatures @@ -353,10 +352,10 @@ bodymethtest5(x, y=Dict(1=>2)) = 5 methoddefs!(signatures, frame; define=true) ex = :(typedsig(x::Int) = 2) frame = Frame(Lowering, ex) - JuliaInterpreter.next_until!(LoweredCodeUtils.ismethod3, frame, true) + JuliaInterpreter.next_until!(LoweredCodeUtils.is_frame_at_method3, frame, true) empty!(signatures) methoddefs!(signatures, frame; define=true) - mt, sig = first(signatures) + _mt, sig = first(signatures) @test sig.parameters[end] == Int # Multiple keyword arg methods per frame @@ -435,7 +434,7 @@ bodymethtest5(x, y=Dict(1=>2)) = 5 Core.eval(Lowering422, ex) frame = Frame(Lowering422, ex) rename_framemethods!(frame) - pc = methoddefs!(signatures, frame; define=false) + methoddefs!(signatures, frame; define=false) @test typeof(Lowering422.fneg) ∈ Set(Base.unwrap_unionall(sig).parameters[1] for (_, sig) in signatures) # Scoped names (https://github.com/timholy/Revise.jl/issues/568) @@ -450,7 +449,7 @@ bodymethtest5(x, y=Dict(1=>2)) = 5 while pc < pcstop pc = JuliaInterpreter.step_expr!(frame, true) end - pc = methoddef!(signatures, frame, pc; define=true) + methoddef!(signatures, frame, pc; define=true) @test MethodInfoKey(nothing, Tuple{typeof(Lowering.f568)}) ∈ signatures @test Lowering.f568() == -2 @@ -493,7 +492,7 @@ bodymethtest5(x, y=Dict(1=>2)) = 5 Core.eval(m, ex) frame = Frame(m, ex) rename_framemethods!(frame) - pc = methoddefs!(signatures, frame; define=false) + methoddefs!(signatures, frame; define=false) @test !isempty(signatures) # really we just need to know that `methoddefs!` completed without getting stuck finally Pkg.activate(oldenv; io=devnull) # back to the original environment From 8881ea601ac9829ebac53a9a4bbf7e8a284435fc Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 16 Sep 2026 13:29:05 +0900 Subject: [PATCH 2/5] Separate `expr` and `Frame` method query overloads `ismethod[3](expr)` and `ismethod[3](::Frame)` are distinct queries. Avoiding an unnecessary overload prevents unwanted type instability. --- src/signatures.jl | 4 ++-- src/utils.jl | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/signatures.jl b/src/signatures.jl index 1e0c421..526fcdf 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -678,7 +678,7 @@ function methoddef!(interp::Interpreter, signatures::Vector{MethodInfoKey}, fram pc = frame.pc stmt = pc_expr(frame, pc) if !ismethod(stmt) - pc = next_until!(ismethod, interp, frame, true) + pc = next_until!(is_frame_at_method, interp, frame, true) end pc === nothing && error("pc at end of frame without finding a method") methoddef!(interp, signatures, frame, pc; define) @@ -717,7 +717,7 @@ function _methoddefs!(interp::Interpreter, signatures::Vector{MethodInfoKey}, fr while pc !== nothing stmt = pc_expr(frame, pc) if !ismethod(stmt) - pc = next_until!(ismethod, interp, frame, true) + pc = next_until!(is_frame_at_method, interp, frame, true) end pc === nothing && break ret = methoddef!(interp, signatures, frame, pc; define) diff --git a/src/utils.jl b/src/utils.jl index 6907ece..f7a0047 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -78,8 +78,8 @@ function getrhs(@nospecialize(stmt)) return lhs_rhs === nothing ? stmt : lhs_rhs[2] end -ismethod(frame::Frame) = ismethod(pc_expr(frame)) -ismethod3(frame::Frame) = ismethod3(pc_expr(frame)) +is_frame_at_method(frame::Frame) = ismethod(pc_expr(frame)) +is_frame_at_method3(frame::Frame) = ismethod3(pc_expr(frame)) # Check if a call argument refers to Core.define_method function is_define_method_ref(@nospecialize(f)) @@ -104,9 +104,9 @@ function is_define_method_call_4arg(@nospecialize(stmt)) return is_define_method_ref(stmt.args[1]) end -ismethod(stmt) = isexpr(stmt, :method) || is_define_method_call_2arg(stmt) || is_define_method_call_4arg(stmt) -ismethod1(stmt) = isexpr(stmt, :method, 1) || is_define_method_call_2arg(stmt) -ismethod3(stmt) = isexpr(stmt, :method, 3) || is_define_method_call_4arg(stmt) +ismethod(@nospecialize stmt) = isexpr(stmt, :method) || is_define_method_call_2arg(stmt) || is_define_method_call_4arg(stmt) +ismethod1(@nospecialize stmt) = isexpr(stmt, :method, 1) || is_define_method_call_2arg(stmt) +ismethod3(@nospecialize stmt) = isexpr(stmt, :method, 3) || is_define_method_call_4arg(stmt) # Extract the "name" argument from a method-definition statement. # For Expr(:method, name, ...) it's args[1]; for define_method(mod, name, ...) it's args[3]. @@ -136,7 +136,7 @@ function method_body(@nospecialize(stmt)) end end -function ismethod_with_name(src, stmt, target::AbstractString; reentrant::Bool=false) +function ismethod_with_name(src::CodeInfo, @nospecialize(stmt), target::AbstractString; reentrant::Bool=false) if reentrant name = stmt else From 1f3895f903dbc7c8e2fdbfdb2013a79b713c1a52 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 16 Sep 2026 13:29:27 +0900 Subject: [PATCH 3/5] Use `===` for untyped symbol comparison --- src/signatures.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/signatures.jl b/src/signatures.jl index 526fcdf..a448267 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -452,7 +452,7 @@ function replacename!(args::AbstractVector, pr) replacename!(a.val::Expr, pr) elseif a === oldname args[i] = newname - elseif a == oldname.name + elseif a === oldname.name args[i] = newname.name end end @@ -728,11 +728,11 @@ end function is_self_call(@nospecialize(stmt), slotnames, argno::Integer=1) if isa(stmt, Expr) - if stmt.head == :call + if stmt.head === :call a = stmt.args[argno] if isa(a, SlotNumber) || isa(a, Core.SlotNumber) sn = slotnames[a.id] - if sn == Symbol("#self#") || sn == Symbol("") # allow empty to fix https://github.com/timholy/CodeTracking.jl/pull/48 + if sn === Symbol("#self#") || sn === Symbol("") # allow empty to fix https://github.com/timholy/CodeTracking.jl/pull/48 return true end end From fe53c94ea141a5fcaff1cd670bf8991f5eb35b5a Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 16 Sep 2026 13:32:48 +0900 Subject: [PATCH 4/5] Add restrictive argument type declarations to internal subroutines Add restrictive argument type declarations to internal utilities. This improves JETLS diagnostics and makes the intent of the codebase clearer. --- src/codeedges.jl | 51 +++++++++++++++++++++++++++-------------------- test/codeedges.jl | 5 ++--- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/codeedges.jl b/src/codeedges.jl index 610277f..417e558 100644 --- a/src/codeedges.jl +++ b/src/codeedges.jl @@ -730,9 +730,11 @@ function exclude_named_typedefs(src::CodeInfo) return norequire end -function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo, edges::CodeEdges, - controller::SelectiveEvalController=SelectiveEvalController(); - norequire = ()) +function lines_required!( + isrequired::AbstractVector{Bool}, objs::Set{GlobalRef}, src::CodeInfo, edges::CodeEdges, + controller::SelectiveEvalController=SelectiveEvalController(); + norequire = () + ) # A controller describes one particular slice. Recompute it from scratch so # callers can safely reuse the same object for another slice. empty!(controller.termination_points) @@ -780,7 +782,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo, return isrequired end -function add_requests!(isrequired, objs, edges::CodeEdges, norequire) +function add_requests!( + isrequired::AbstractVector{Bool}, objs::Set{GlobalRef}, edges::CodeEdges, + norequire + ) objsnew = Set{GlobalRef}() for obj in objs add_obj!(isrequired, objsnew, obj, edges, norequire) @@ -788,7 +793,9 @@ function add_requests!(isrequired, objs, edges::CodeEdges, norequire) return objsnew end -function add_ssa_preds!(isrequired, src::CodeInfo, edges::CodeEdges, norequire) +function add_ssa_preds!( + isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges, norequire + ) changed = false for idx = 1:length(src.code) if isrequired[idx] @@ -798,18 +805,22 @@ function add_ssa_preds!(isrequired, src::CodeInfo, edges::CodeEdges, norequire) return changed end -function add_named_dependencies!(isrequired, edges::CodeEdges, objs, norequire) +function add_named_dependencies!( + isrequired::AbstractVector{Bool}, edges::CodeEdges, objs::Set{GlobalRef}, norequire + ) changed = false for (obj, uses) in edges.byname obj ∈ objs && continue - if any(view(isrequired, uses.succs)) + if any(view(isrequired, uses.succs))::Bool changed |= add_obj!(isrequired, objs, obj, edges, norequire) end end return changed end -function add_preds!(isrequired, idx, edges::CodeEdges, norequire) +function add_preds!( + isrequired::AbstractVector{Bool}, idx::Int, edges::CodeEdges, norequire + ) chngd = false preds = edges.preds[idx] for p in preds @@ -821,18 +832,10 @@ function add_preds!(isrequired, idx, edges::CodeEdges, norequire) end return chngd end -function add_succs!(isrequired, idx, edges::CodeEdges, succs, norequire) - chngd = false - for p in succs - isrequired[p] && continue - p ∈ norequire && continue - isrequired[p] = true - chngd = true - add_succs!(isrequired, p, edges, edges.succs[p], norequire) - end - return chngd -end -function add_obj!(isrequired, objs, obj::GlobalRef, edges::CodeEdges, norequire) +function add_obj!( + isrequired::AbstractVector{Bool}, objs::Set{GlobalRef}, obj::GlobalRef, + edges::CodeEdges, norequire + ) chngd = false for p in edges.byname[obj].preds p ∈ norequire && continue @@ -1049,7 +1052,11 @@ end # New struct definitions, including their constructors, get spread out over many # statements. If we're evaluating any of them, it's important to evaluate *all* of them. -function add_typedefs!(isrequired, src::CodeInfo, edges::CodeEdges, (typedef_blocks, typedef_names), norequire) +function add_typedefs!( + isrequired, src::CodeInfo, edges::CodeEdges, + typedefs::Tuple{Vector{UnitRange{Int}},Vector{Symbol}}, + norequire + ) changed = false stmts = src.code defaultctors = Tuple{Int,BitSet}[] @@ -1062,7 +1069,7 @@ function add_typedefs!(isrequired, src::CodeInfo, edges::CodeEdges, (typedef_blo stmt = stmts[idx] isrequired[idx] || (idx += 1; continue) intypedef = false - for (typedefr, typedefn) in zip(typedef_blocks, typedef_names) + for (typedefr, typedefn) in zip(typedefs...) if idx ∈ typedefr ireq = view(isrequired, typedefr) if !all(ireq) diff --git a/test/codeedges.jl b/test/codeedges.jl index fc0941d..d847bfd 100644 --- a/test/codeedges.jl +++ b/test/codeedges.jl @@ -297,9 +297,8 @@ module ModSelective end isrequired = fill(false, length(src.code)) targetidx = findlast(stmt -> Meta.isexpr(stmt, :call), src.code) # the second push! isrequired[targetidx] = true - lines_required!(isrequired, (GlobalRef(mod, :branch_value),), src, edges, controller) - selective_eval_fromstart!( - LoweredCodeUtils.RecursiveInterpreter(), frame, isrequired, controller, true) + lines_required!(isrequired, Set((GlobalRef(mod, :branch_value),)), src, edges, controller) + selective_eval_fromstart!(LoweredCodeUtils.RecursiveInterpreter(), frame, isrequired, controller, true) @test @invokelatest(mod.branch_value) == 1 @test @invokelatest(mod.hits) == [2] end From d05aa78b99bbce38b2f1e10e6ce8d19df587e3c9 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 16 Sep 2026 13:33:10 +0900 Subject: [PATCH 5/5] Minor cosmetic changes --- src/signatures.jl | 2 +- test/signatures.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/signatures.jl b/src/signatures.jl index a448267..3161bfa 100644 --- a/src/signatures.jl +++ b/src/signatures.jl @@ -601,7 +601,7 @@ function methoddef!(interp::Interpreter, signatures::Vector{MethodInfoKey}, fram codeloc = codelocation(code, pc) loc = linetable(code, codeloc) ft = Base.unwrap_unionall((Base.unwrap_unionall(sigt)::DataType).parameters[1]) - if !startswith(String((ft.name::Core.TypeName).name), "##") + if !startswith(String((ft.name::Core.TypeName).name), "##") && loc !== nothing @warn "file $(loc.file), line $(loc.line): no method found for $sigt" end if pc == pc3 diff --git a/test/signatures.jl b/test/signatures.jl index 41119a1..3199467 100644 --- a/test/signatures.jl +++ b/test/signatures.jl @@ -502,7 +502,7 @@ end # https://github.com/timholy/Revise.jl/issues/643 module Revise643 -using LoweredCodeUtils, JuliaInterpreter, Test +using JuliaInterpreter, LoweredCodeUtils, Test # make sure to not define `foogr` before macro expansion, # otherwise it will be resolved as `QuoteNode`