4.0 KiB
FIXED (1f6e27d, examples/212). Two root causes:
instantiateGenericStructnow binds the template name to the concrete instance (tb.put(tmpl.name, id)), so an impl methodself: *Combinedresolvesself.fieldto the instance, not the 0-field generic stub. (This was a general pre-existing bug —self.xfailed on any generic-struct impl method.)createProtocolThunkmonomorphizes the template method for a generic-struct instance (Combined.get→Combined__s64_s64.getwith the instance bindings), so the erasure vtable dispatches instead of anunreachablethunk.
xx c (Combined → VL($R)) now dispatches correctly. The full canonical map
additionally needs mapper closure-pack typing + $R inference at the call site
(a separate piece) — tracked separately.
Symptom
xx c where c is a generic-struct instance and the target is a parameterized
protocol — via a generic impl P($R) for Combined($R, ..$Ts) — compiles
cleanly but traps at runtime (exit 133) when a method is then called on the
erased value. The protocol value is built with a wrong/empty vtable, so the
dispatch jumps to a bad fn-ptr.
This is the last piece of the canonical map (return xx c;).
Reproduction
#import "modules/std.sx";
VL :: protocol(T: Type) { get :: () -> T; }
IntCell :: struct { v: s64; }
impl VL(s64) for IntCell { get :: (self: *IntCell) -> s64 => self.v; }
Combined :: struct($R: Type, ..$Ts: []Type) { sources: (..VL(Ts)); value: $R; }
impl VL($R) for Combined($R, ..$Ts) { get :: (self: *Combined) -> $R => self.value; }
make :: (..sources: VL) -> VL(s64) {
c : Combined(s64, ..sources.T) = ---;
c.value = 99;
c.sources = (..sources);
return xx c; // Combined__s64_s64 -> VL(s64)
}
main :: () -> s32 {
r := make(IntCell.{ v = 1 });
print("{}\n", r.get()); // expect 99; instead traps
0;
}
sx ir produces clean, verifier-passing IR (no "no visible xx conversion"
diagnostic — so an impl was matched), but the JIT traps on r.get().
Root cause (suspected)
param_impl_map is keyed by concrete (protocol, target_args_mangled, source_mangled). The impl impl VL($R) for Combined($R, ..$Ts) is generic on
both sides — its source mangles to a generic Combined (with $R/$Ts), not
the concrete Combined__s64_s64. Erasing Combined__s64_s64 → VL(s64) looks up
(VL, s64, Combined__s64_s64), which doesn't key-match the generic impl; some
looser path still produces a protocol value, but its vtable slot for get
isn't bound to the monomorphized Combined__s64_s64.get (which returns
self.value as $R=s64). Calling through it traps.
The fix needs generic-impl matching + per-instance monomorphization for protocol
erasure: when erasing a concrete generic-struct instance to a parameterized
protocol, find the generic impl whose source template matches the instance's
template (binding $R/$Ts from the instance's recorded bindings —
struct_instance_bindings), monomorphize the impl methods for those bindings,
and fill the vtable with the resulting fn-ptrs. Compare:
buildProtocolValue/buildProtocolErasure(src/ir/lower.zig) — the vtable construction + impl-method lookup.param_impl_mapkeying (Proto\x00<args>\x00<src_mangled>) and how a generic source template is (or isn't) matched against a concrete instance.instantiateParamProtocol(the dst side already works) andinstantiateGenericStruct'sstruct_instance_bindings(the source bindings).
Verification
The reproduction should print 99. Plain (non-generic) struct → parameterized
protocol erasure already works (examples/206: xx IntCell -> VL(s64)); the gap
is specifically a generic-struct source matched via a generic impl.
Status
Everything else in the canonical map works: Combined($R, ..sources.T)
instantiation (examples/209), c.sources = (..sources) materialization with
per-element erasure (examples/210), and mapper(..sources.value) projection +
spread (examples/211). This erasure is the final blocker.