Skip to content

Commit 8aa7d23

Browse files
Reference translation: scale to Piccolissimo — type_content/method descriptors, health-gated canonicalize!, multi-private load chains
Completes the full production flow (translated-load of the whole bundle purely from an emitted sidecar) discovered while proving the svec descriptors at Piccolissimo scale. Two more order-independent descriptor kinds (the remaining anonymous targets in Piccolissimo's 244; same content-match philosophy as :svec_content): - :type_content — an anonymous Union / un-named UnionAll / parameterization, located by structural type equality (mutual subtyping) over the dep blob's type objects, rank-disambiguated. - :method — a Method object, resolved by (defining-module path, name, structural signature) via _methods_by_ftype. emit_sidecar now describes Piccolissimo 244/244 (was 239/244) and Altissimo 43/43, zero failures. canonicalize! — idempotent AND correct. The leg5 type-hash repair must rebuild a broken method sig's non-interned Tuple so its baked hash is recomputed in the consumer's nonce universe. The previous conditional-rebuild left a broken sig whose params were pointer-correct but whose baked hash was stale (Piccolissimo's AltissimoOptions solve! → paradoxical MethodError). Fix: gate repair on brokenness detected by a fresh-hash dispatch probe — _methods_by_ftype on the RECONSTRUCTED sig (probing with the method's own stale-hashed sig falsely reports health by matching the stale-stored table entry). Repaired methods become findable under their consumer-hash sig, so the next pass skips them → idempotent; only the 36 genuinely-broken Piccolissimo entries are re-inserted (= leg5). Multi-private load chains: _remap_to_loaded! and translate!'s dep lookup now consult Base.loaded_precompiles (not just loaded_modules), so a downstream private (Piccolissimo) sees an upstream private (a translated Altissimo, loaded via load_package_image → registered only in loaded_precompiles) and remaps its header to Altissimo's NEW restamped build-id, avoiding a mixed-lineage closure-check failure. Added loader helper _loaded_module_by_pid. Full production flow validated (julia 1.12.6, pure private-free consumer depot, ZERO builder-depot dependency access at consume time): emit one 43 KB sidecar for both privates (11.8 s); load_translated both from that sidecar alone (translate Altissimo 1.7 s / Piccolissimo 3.3 s; 18 + 1012 words rewritten = leg5); H-gate smoke solve Ipopt f1=0.9761, Altissimo-backend f1=0.9921 — bit-for-bit leg1/leg5. Test item 6 updated for the new _resolve_new_offset(t, root, ctx) arity; full suite green (336/336). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9jVcSYDNXtbBGfcAEPwvh
1 parent 21dcc6f commit 8aa7d23

3 files changed

Lines changed: 187 additions & 27 deletions

File tree

src/loader.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ function _find_loaded_module(name::AbstractString, build_id::UInt128)
5959
return nothing
6060
end
6161

62+
# The loaded module for a `PkgId`, searching `loaded_precompiles` (where
63+
# `load_package_image` registers restored modules) as well as `loaded_modules` and
64+
# the well-known roots. A private image loaded via `load_package_image` lands in
65+
# `loaded_precompiles` but NOT `loaded_modules`, so a plain `root_module`/
66+
# `loaded_modules` lookup misses it — this finds it (used when translating a
67+
# downstream private that depends on an already-loaded private, and when remapping
68+
# headers to the loaded universe).
69+
function _loaded_module_by_pid(pid::Base.PkgId)
70+
m = get(Base.loaded_modules, pid, nothing)
71+
m === nothing || return m
72+
for (p, mods) in Base.loaded_precompiles
73+
(p.uuid === pid.uuid && p.name == pid.name) || continue
74+
isempty(mods) || return last(mods)
75+
end
76+
for mod in (Core, Base, Main)
77+
(pid.name == String(nameof(mod))) && return mod
78+
end
79+
return nothing
80+
end
81+
6282
# All 128-bit build-ids under which a module `name` is currently loaded (across
6383
# `loaded_precompiles` and `loaded_modules`). Used to distinguish a genuinely
6484
# `:absent` dependency from a `:mixed_lineage` one (name present, wrong build-id).

src/translate.jl

Lines changed: 165 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,10 @@ function _describe_target(root::Module, tbl, blob_lo::UInt, blob_hi::UInt,
523523
return _describe_svec_content(obj, dep_ctx, byte_offset, dep_name)
524524
elseif obj isa String
525525
return _describe_const_data(obj, dep_ctx, byte_offset, dep_name)
526+
elseif obj isa Method
527+
return _describe_method(obj, dep_ctx, byte_offset, dep_name, root)
528+
elseif obj isa Type
529+
return _describe_type_content(obj, dep_ctx, byte_offset, dep_name)
526530
end
527531
end
528532
error("emit_sidecar: cannot describe target in $dep_name at offset $byte_offset " *
@@ -731,6 +735,83 @@ function _describe_const_data(obj, ctx::_DepCtx, boff::Int, dep_name::AbstractSt
731735
Tuple{Symbol, Any}[], payload, r, length(offs))
732736
end
733737

738+
# Structural (interning-independent) type equality via mutual subtyping.
739+
function _type_eq(a, b)
740+
(a isa Type && b isa Type) || return false
741+
return try
742+
a <: b && b <: a
743+
catch
744+
false
745+
end
746+
end
747+
748+
# Offsets of every in-blob TYPE object structurally equal to `T` (a Union / an
749+
# anonymous UnionAll / an un-named DataType parameterization — content-describable
750+
# by its own structure, but with no name and no build-stable anchor path).
751+
function _match_type_offsets(ctx::_DepCtx, T)
752+
offs = Int[]
753+
for gp in ctx.img.gctags
754+
boff = gp + 8
755+
ptr = ctx.lo + UInt(boff)
756+
(ctx.lo <= ptr < ctx.hi) || continue
757+
obj = try
758+
unsafe_pointer_to_objref(Ptr{Cvoid}(ptr))
759+
catch
760+
continue
761+
end
762+
(obj isa Type && _type_eq(obj, T)) && push!(offs, boff)
763+
end
764+
return sort!(offs)
765+
end
766+
767+
function _describe_type_content(obj, ctx::_DepCtx, boff::Int, dep_name::AbstractString)
768+
payload = _pack(obj)
769+
T2 = _unpack(payload)
770+
offs = _match_type_offsets(ctx, T2)
771+
r = findfirst(==(boff), offs)
772+
r === nothing &&
773+
error("emit_sidecar: $dep_name type@$boff — content self-check failed " *
774+
"(round-tripped type did not re-locate; matches=$offs). type=" * _safe_repr(obj))
775+
return RefDescriptor(:type_content, Symbol[], Symbol(""), nothing,
776+
Tuple{Symbol, Any}[], payload, r, length(offs))
777+
end
778+
779+
# Resolve a method by (defining-module path, name, structural signature): among the
780+
# world's methods whose ftype-signature admits `sig`, the one defined in `ownermod`
781+
# as `name` with a structurally-identical `sig`. Returns its blob byte offset.
782+
function _resolve_method_offset(sig, root::Module, mp::Vector{Symbol}, name::Symbol, ctx::_DepCtx)
783+
ownermod = _resolve_module(root, mp)
784+
cands = Base._methods_by_ftype(sig, -1, Base.get_world_counter())
785+
cands === false && error("method: _methods_by_ftype failed for $(_safe_repr(sig))")
786+
hits = Method[]
787+
for mm in cands
788+
meth = mm.method
789+
(meth.name === name && meth.module === ownermod) || continue
790+
(meth.sig <: sig && sig <: meth.sig) || continue
791+
push!(hits, meth)
792+
end
793+
isempty(hits) && error("method: no live method matches $(name) in $(nameof(ownermod)) with sig $(_safe_repr(sig))")
794+
length(hits) > 1 && error("method: ambiguous ($(length(hits)) live methods match) for $(name)")
795+
off = Int(_vptr(hits[1]) - ctx.lo)
796+
(0 <= off < Int(ctx.hi - ctx.lo)) ||
797+
error("method: resolved method offset $off out of blob for $(name)")
798+
return off
799+
end
800+
801+
function _describe_method(m::Method, ctx::_DepCtx, boff::Int, dep_name::AbstractString,
802+
root::Module)
803+
mp = _module_path(root, m.module)
804+
mp === nothing &&
805+
error("emit_sidecar: $dep_name method@$boff — defining module $(m.module) " *
806+
"not under dep root $(nameof(root))")
807+
payload = _pack(m.sig)
808+
off = _resolve_method_offset(_unpack(payload), root, mp, m.name, ctx)
809+
off == boff ||
810+
error("emit_sidecar: $dep_name method@$boff — self-check found offset $off " *
811+
"(method $(m.name), sig $(_safe_repr(m.sig)))")
812+
return RefDescriptor(:method, mp, m.name, nothing, Tuple{Symbol, Any}[], payload, 0, 0)
813+
end
814+
734815
# ── Descriptor resolution (consumer side) ────────────────────────────
735816

736817
function _resolve_module(root::Module, modpath::Vector{Symbol})
@@ -767,7 +848,8 @@ function _resolve_descriptor(d::RefDescriptor, root::Module)
767848
cur = _walk_step(cur, op, arg)
768849
end
769850
return cur
770-
elseif d.kind === :svec_content || d.kind === :const_data
851+
elseif d.kind === :svec_content || d.kind === :const_data ||
852+
d.kind === :type_content || d.kind === :method
771853
error("translate!: $(d.kind) is a content descriptor — resolve it against a " *
772854
"dep blob with `_resolve_new_offset`, not `_resolve_descriptor` " *
773855
"(it has no live object to return, only a matched offset)")
@@ -791,7 +873,7 @@ end
791873
# resolve to a live object whose offset is `jl_value_ptr - blob_base`; content kinds
792874
# must instead SEARCH the consumer blob, since the object has no name and no stable
793875
# path — see `_match_svec_offsets` / `_match_const_string_offsets`.)
794-
function _resolve_new_offset(t::RefTarget, ctx::_DepCtx)
876+
function _resolve_new_offset(t::RefTarget, root::Module, ctx::_DepCtx)
795877
d = t.descriptor
796878
tag = "$(t.dep_name)@$(t.old_offset)"
797879
if d.kind === :svec_content
@@ -802,6 +884,12 @@ function _resolve_new_offset(t::RefTarget, ctx::_DepCtx)
802884
s = _unpack(d.payload)
803885
offs = _match_const_string_offsets(ctx, s)
804886
return _pick_ranked(offs, d.rank, d.cohort, "const_data $tag")
887+
elseif d.kind === :type_content
888+
T = _unpack(d.payload)
889+
offs = _match_type_offsets(ctx, T)
890+
return _pick_ranked(offs, d.rank, d.cohort, "type_content $tag")
891+
elseif d.kind === :method
892+
return _resolve_method_offset(_unpack(d.payload), root, d.modpath, d.name, ctx)
805893
else
806894
error("translate!: _resolve_new_offset called on non-content descriptor $(d.kind)")
807895
end
@@ -1007,6 +1095,9 @@ function translate!(image_path::String, sidecar; depot = nothing,
10071095
catch
10081096
nothing
10091097
end
1098+
# Fall back to loaded_precompiles for an upstream private loaded via
1099+
# `load_package_image` (not registered in loaded_modules / root_module).
1100+
m === nothing && (m = _loaded_module_by_pid(pid))
10101101
m === nothing && return (m = nothing, lo = UInt(0), ctx = nothing)
10111102
bl = _dep_blob(tbl, m)
10121103
bl === nothing && return (m = m, lo = UInt(0), ctx = nothing)
@@ -1035,15 +1126,18 @@ function translate!(image_path::String, sidecar; depot = nothing,
10351126
push!(failed, "$(t.dep_name)@$(t.old_offset): dep not loaded")
10361127
continue
10371128
end
1038-
is_content = t.descriptor.kind === :svec_content || t.descriptor.kind === :const_data
1129+
is_content = t.descriptor.kind === :svec_content ||
1130+
t.descriptor.kind === :const_data ||
1131+
t.descriptor.kind === :type_content ||
1132+
t.descriptor.kind === :method
10391133
if is_content && info.ctx === nothing
10401134
push!(failed, "$(t.dep_name)@$(t.old_offset): content descriptor but " *
10411135
"dep package image not found for blob enumeration")
10421136
continue
10431137
end
10441138
newoff = try
10451139
if is_content
1046-
_resolve_new_offset(t, info.ctx::_DepCtx)
1140+
_resolve_new_offset(t, info.m::Module, info.ctx::_DepCtx)
10471141
else
10481142
obj = _resolve_descriptor(t.descriptor, info.m::Module)
10491143
Int(_vptr(obj) - info.lo)
@@ -1200,6 +1294,45 @@ signature.
12001294
12011295
Returns a [`CanonicalizeReport`](@ref).
12021296
"""
1297+
# Whether dispatch can find `meth` when queried with signature `probe`. A
1298+
# nonce-salted (stale-hash) method is the pathology leg5 wall #5 describes: `===` /
1299+
# subtyping still hold (structural), but the hash-consulting cache probe inside
1300+
# `_methods_by_ftype` MISSES. Crucially the query must use a **freshly-reconstructed**
1301+
# (consumer-hash) signature: probing with the method's OWN stale-hashed sig would
1302+
# still match the stale-stored table entry and falsely report health. Probing with a
1303+
# fresh-hash sig reveals the mismatch — and makes the repair idempotent (a re-inserted
1304+
# method is stored under the fresh hash, so the next pass's fresh-hash probe finds it).
1305+
function _sig_findable(meth::Method, probe)
1306+
return try
1307+
found = false
1308+
for mm in Base._methods_by_ftype(probe, -1, Base.get_world_counter())
1309+
if mm.method === meth
1310+
found = true
1311+
break
1312+
end
1313+
end
1314+
found
1315+
catch
1316+
true # cannot probe (odd sig) → treat as healthy, leave it alone
1317+
end
1318+
end
1319+
1320+
# Reconstruct a signature so its (and its components') type hashes are recomputed in
1321+
# the CONSUMER's nonce universe. Interned parametric components go through
1322+
# `_canonsig` (idempotent — returns the same pointer once canonical); the sig's own
1323+
# `Tuple` wrapper is rebuilt UNCONDITIONALLY, because Tuple types are not interned so
1324+
# the only way to refresh a stale baked hash is to construct a fresh one.
1325+
function _recanon_sig(t)
1326+
if t isa UnionAll
1327+
return UnionAll(t.var, _recanon_sig(t.body))
1328+
elseif t isa DataType && t.name.name === :Tuple
1329+
ps = Any[(p isa Type || p isa Core.TypeofVararg) ? _canonsig(p) : p for p in t.parameters]
1330+
return Tuple{ps...}
1331+
else
1332+
return _canonsig(t)
1333+
end
1334+
end
1335+
12031336
function canonicalize!(mods::Module...)
12041337
allmods = Set{Module}()
12051338
for r in mods
@@ -1225,31 +1358,34 @@ function canonicalize!(mods::Module...)
12251358
meth.module in allmods || continue
12261359
objectid(meth) in visited && continue
12271360
push!(visited, objectid(meth)); nseen += 1
1361+
# Reconstruct the sig with consumer-universe hashes, then gate strictly on
1362+
# brokenness (idempotent: a healthy/repaired method is found by the
1363+
# fresh-hash probe and skipped). Unconditional re-interning of every sig
1364+
# is slow and — for re-insertion — dangerous (duplicate dispatch entries →
1365+
# spurious ambiguities).
12281366
cs = try
1229-
_canonsig(meth.sig)
1367+
_recanon_sig(meth.sig)
12301368
catch
1231-
meth.sig
1369+
continue
1370+
end
1371+
_sig_findable(meth, cs) && continue # dispatch already finds it → healthy
1372+
same = try # alpha-equivalent same signature
1373+
cs <: meth.sig && meth.sig <: cs
1374+
catch
1375+
false
12321376
end
1233-
_vptr(cs) == _vptr(meth.sig) && continue # already canonical (idempotent)
1234-
cs == meth.sig || continue # structure must be identical
1377+
same || continue
12351378
try
1236-
ccall(:jl_set_nth_field, Cvoid, (Any, Csize_t, Any), meth, sigidx - 1, cs)
1237-
nfix += 1
1238-
broken = true
1239-
try
1240-
for mm in Base._methods_by_ftype(cs, -1, Base.get_world_counter())
1241-
if mm.method === meth
1242-
broken = false; break
1243-
end
1244-
end
1245-
catch
1379+
if _vptr(cs) != _vptr(meth.sig)
1380+
ccall(:jl_set_nth_field, Cvoid, (Any, Csize_t, Any), meth, sigidx - 1, cs)
1381+
nfix += 1
12461382
end
1247-
if broken
1248-
mt = ccall(:jl_method_table_for, Any, (Any,), cs)
1249-
if mt !== nothing
1250-
ccall(:jl_method_table_insert, Cvoid, (Any, Any, Ptr{Cvoid}), mt, meth, C_NULL)
1251-
nreins += 1
1252-
end
1383+
# Broken ⇒ the stored dispatch entry is under a stale hash; re-insert
1384+
# so the method is reachable under its consumer-hash signature.
1385+
mt = ccall(:jl_method_table_for, Any, (Any,), cs)
1386+
if mt !== nothing
1387+
ccall(:jl_method_table_insert, Cvoid, (Any, Any, Ptr{Cvoid}), mt, meth, C_NULL)
1388+
nreins += 1
12531389
end
12541390
catch e
12551391
@debug "canonicalize!: sig-fix failed" file = meth.file line = meth.line exception = e
@@ -1271,7 +1407,11 @@ function _remap_to_loaded!(image_path::String)
12711407
for dep in hdr.required_modules
12721408
_is_sysimage_dep(dep) && continue
12731409
pid = Base.PkgId(dep.uuid, dep.name)
1274-
m = get(Base.loaded_modules, pid, nothing)
1410+
# Consult loaded_precompiles too: an upstream private already loaded via
1411+
# `load_package_image` (e.g. a translated Altissimo) lands there, not in
1412+
# loaded_modules — and a downstream private's header MUST be remapped to
1413+
# its NEW (restamped) build-id or the closure check flags mixed lineage.
1414+
m = _loaded_module_by_pid(pid)
12751415
m === nothing && continue
12761416
bid = Base.module_build_id(m)
12771417
cur = (UInt128(dep.build_id_hi) << 64) | UInt128(dep.build_id_lo)

test/test_translate.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ end
653653
d.kind === :svec_content || continue
654654
n_svec[] += 1
655655
t = J.RefTarget(1, "Dep", UInt128(0), boff, d, UInt64(0), Int[])
656-
newoff = try J._resolve_new_offset(t, ctx) catch; -1 end
656+
newoff = try J._resolve_new_offset(t, Dep, ctx) catch; -1 end
657657
newoff == boff && (n_svec_ok[] += 1)
658658
end
659659
println("SVEC content targets=", n_svec[], " roundtrip_ok=", n_svec_ok[])
@@ -663,7 +663,7 @@ end
663663
sboff = Int(vp(s) - lo)
664664
dc = J._describe_target(Dep, tbl, lo, hi, sboff, "Dep"; dep_ctx = ctx)
665665
tc = J.RefTarget(1, "Dep", UInt128(0), sboff, dc, UInt64(0), Int[])
666-
cnew = J._resolve_new_offset(tc, ctx)
666+
cnew = J._resolve_new_offset(tc, Dep, ctx)
667667
println("CONST kind=", dc.kind, " roundtrip_ok=", cnew == sboff,
668668
" inconst=", ctx.img.const_lo <= sboff < ctx.img.const_hi)
669669
println("CONTENT PROBE DONE")

0 commit comments

Comments
 (0)