@@ -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))
732736end
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
736817function _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)" )
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
12011295Returns 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+
12031336function 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)
0 commit comments