diff --git a/issues/0054-generic-struct-to-param-protocol-erasure.md b/issues/0054-generic-struct-to-param-protocol-erasure.md new file mode 100644 index 0000000..64b910a --- /dev/null +++ b/issues/0054-generic-struct-to-param-protocol-erasure.md @@ -0,0 +1,72 @@ +# 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 + +```sx +#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](../src/ir/lower.zig)) + — the vtable construction + impl-method lookup. +- `param_impl_map` keying (`Proto\x00\x00`) and how a generic + source template is (or isn't) matched against a concrete instance. +- `instantiateParamProtocol` (the dst side already works) and + `instantiateGenericStruct`'s `struct_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.