issues/0054: generic-struct -> parameterized-protocol erasure traps (canonical xx c)
This commit is contained in:
72
issues/0054-generic-struct-to-param-protocol-erasure.md
Normal file
72
issues/0054-generic-struct-to-param-protocol-erasure.md
Normal file
@@ -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<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) 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.
|
||||
Reference in New Issue
Block a user