From ccce89ca254b0cebef00341d2a4c7353849eef62 Mon Sep 17 00:00:00 2001 From: Keno Fischer Date: Tue, 21 Jul 2026 04:23:40 +0000 Subject: [PATCH] Support typegroup-based struct lowering On Julia versions where ordinary `struct` definitions lower through the typegroup mechanism, the type-creating statement is a call to `Core.resolve_typegroup` rather than `Core._structtype`/`Core._typebody!`. Teach `istypedef`/`typedef_range`/`find_typedefs` to recognize this form, including multi-type `typegroup` blocks, which have no leading `global` marker and define several names from a single statement range. Co-Authored-By: Claude Fable 5 --- src/codeedges.jl | 28 ++++++++++++++++++++++------ src/utils.jl | 40 ++++++++++++++++++++++++++++++++++++++++ test/codeedges.jl | 29 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/codeedges.jl b/src/codeedges.jl index 9fc03c8..cf6500c 100644 --- a/src/codeedges.jl +++ b/src/codeedges.jl @@ -1007,13 +1007,29 @@ function find_typedefs(src::CodeInfo) if istypedef(stmt) && !isanonymous_typedef(stmt::Expr) stmt = stmt::Expr r = typedef_range(src, i) - push!(typedef_blocks, r) - name = stmt.head === :call ? stmt.args[3] : stmt.args[1] - if isa(name, QuoteNode) - name = name.value + if is_resolve_typegroup_call(stmt) + # A typegroup defines one type per `declare_const` in the block; + # record each name against the same statement range. + for j in r + s = src.code[j] + is_declare_const(s) || continue + name = (s::Expr).args[3] + if isa(name, QuoteNode) + name = name.value + end + isa(name, Symbol) || continue + push!(typedef_blocks, r) + push!(typedef_names, name) + end + else + push!(typedef_blocks, r) + name = stmt.head === :call ? stmt.args[3] : stmt.args[1] + if isa(name, QuoteNode) + name = name.value + end + isa(name, Symbol) || @show src i r stmt + push!(typedef_names, name::Symbol) end - isa(name, Symbol) || @show src i r stmt - push!(typedef_names, name::Symbol) i = last(r)+1 else i += 1 diff --git a/src/utils.jl b/src/utils.jl index 7e6b652..e521d60 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -199,6 +199,20 @@ function isanonymous_typedef(@nospecialize stmt) return false end +# Recognize the `Core.resolve_typegroup` call that creates the types of a type +# group. On Julia versions where `struct` definitions lower through the +# typegroup mechanism, ordinary structs also produce this form (with a +# single-element group); `typegroup` blocks produce multi-element groups. +function is_resolve_typegroup_call(@nospecialize(stmt)) + isexpr(stmt, :call) || return false + f = (stmt::Expr).args[1] + is_global_ref(f, Core, :resolve_typegroup) && return true + @static if isdefined(Core, :resolve_typegroup) + is_quotenode_egal(f, Core.resolve_typegroup) && return true + end + return false +end + function istypedef(stmt) isa(stmt, Expr) || return false stmt = getrhs(stmt) @@ -215,6 +229,7 @@ function istypedef(stmt) end end end + is_resolve_typegroup_call(stmt) && return true isanonymous_typedef(stmt) && return true return false end @@ -256,6 +271,31 @@ function typedef_range(src::CodeInfo, idx) is_declare_global(s) && break istart -= 1 end + if is_resolve_typegroup_call(getrhs(stmt)) + # Typegroup form: `TypeVar` bindings and struct-info svecs, then + # `resolve_typegroup(mod, typevars, infos, olds)`, then `getfield` + # extractions and one `declare_const` per type, closed by `latestworld`. + # Ordinary struct definitions open with a `global` marker; `typegroup` + # blocks do not, so if none was found fall back to extending the range + # backwards to the previous statement that cannot be part of the group. + if istart < 1 + istart = idx + for j = idx-1:-1:1 + s = src.code[j] + (isexpr(s, :latestworld) || isexpr(s, :method) || isexpr(s, :thunk) || + is_return(s)) && break + istart = j + end + end + iend, n = idx, length(src.code) + while iend <= n + s = src.code[iend] + (isexpr(s, :latestworld) || isexpr(s, :global) || is_return(s)) && break + iend += 1 + end + iend <= n || error("no final latestworld found for typegroup") + return istart:iend-1 + end istart >= 1 || error("no initial :global or declare_global found") iend, n = idx, length(src.code) have_typebody = have_equivtypedef = false diff --git a/test/codeedges.jl b/test/codeedges.jl index a507a09..e666ceb 100644 --- a/test/codeedges.jl +++ b/test/codeedges.jl @@ -584,4 +584,33 @@ end end end +# Typegroup blocks (Julia versions where `Core.resolve_typegroup` exists). +# Ordinary structs also lower through the typegroup mechanism on Julia ≥ 1.14 +# and are covered by the cases above; this checks a multi-type group, which has +# no leading `global` marker and defines several names from one statement range. +@static if isdefined(Core, :resolve_typegroup) + @testset "typegroup blocks" begin + m = Module(:TypegroupMock) + ex = Expr(:typegroup, Expr(:block, + :(struct TGA; b::Union{TGB,Nothing}; end), + :(struct TGB; a::TGA; end))) + lwr = Meta.lower(m, ex) + if lwr isa Expr && lwr.head === :thunk && + any(LoweredCodeUtils.is_resolve_typegroup_call, (first(lwr.args)::Core.CodeInfo).code) + src = first(lwr.args)::Core.CodeInfo + edges = CodeEdges(m, src) + idx = findfirst(LoweredCodeUtils.is_resolve_typegroup_call, src.code) + @test istypedef(src.code[idx]) + blocks, names = LoweredCodeUtils.find_typedefs(src) + @test names == [:TGA, :TGB] + @test length(blocks) == 2 && blocks[1] == blocks[2] + isrq = lines_required!(istypedef.(src.code), src, edges) + selective_eval_fromstart!(Frame(m, src), isrq, #=istoplevel=#true) + @test @invokelatest(isdefined(m, :TGA)) + @test @invokelatest(isdefined(m, :TGB)) + @test @invokelatest(fieldtype(m.TGB, :a)) === @invokelatest(m.TGA) + end + end +end + end # module codeedges