Files
sx/issues/0054-generic-struct-to-param-protocol-erasure.md
agra d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00

4.0 KiB

FIXED (1f6e27d, examples/212). Two root causes:

  1. instantiateGenericStruct now binds the template name to the concrete instance (tb.put(tmpl.name, id)), so an impl method self: *Combined resolves self.field to the instance, not the 0-field generic stub. (This was a general pre-existing bug — self.x failed on any generic-struct impl method.)
  2. createProtocolThunk monomorphizes the template method for a generic-struct instance (Combined.getCombined__i64_i64.get with the instance bindings), so the erasure vtable dispatches instead of an unreachable thunk.

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: i64; }
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => 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(i64) {
    c : Combined(i64, ..sources.T) = ---;
    c.value = 99;
    c.sources = (..sources);
    return xx c;                 // Combined__i64_i64 -> VL(i64)
}
main :: () -> i32 {
    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__i64_i64. Erasing Combined__i64_i64 → VL(i64) looks up (VL, i64, Combined__i64_i64), 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__i64_i64.get (which returns self.value as $R=i64). 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_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(i64)); 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.