Erasing a type to a protocol when it conforms only via a free function (not an explicit impl P for T) built a vtable of unreachable thunks -> SIGABRT on first dispatch, with no diagnostic. Per specs.md erasure is impl-driven, not structural, so the erasure was never valid. Add a conformance gate (firstUnimplementedMethod in buildProtocolValue, src/ir/lower/protocol.zig): emit a located diagnostic when a protocol method has no reachable impl, or when an impl method introduces its own type params (signature mismatch — it bails lazyLowerFunction and would reach the unreachable thunk). A std.debug.panic tripwire guards the diagnostics==null path so a non-conforming erasure can never silently ship as undef. Gate<->thunk equivalence verified bidirectional. Regressions: protocols/0419 (positive struct-field dispatch), diagnostics/1197 (no-impl) + 1198 (generic-method signature mismatch). Updated memory/0808 (it erased a non-conforming type that never dispatched). Verified by 3+1 adversarial reviews, suite 788/0. Filed adjacent bug 0178 (protocol impl method type-mismatch silent miscompile).
50 lines
2.1 KiB
Plaintext
50 lines
2.1 KiB
Plaintext
// Dispatch a protocol method THROUGH a protocol-typed struct field.
|
|
// Regression (issue 0176): the issue's repro used a plain free function to
|
|
// "satisfy" the protocol — which is NOT impl-driven conformance, so erasure
|
|
// built a vtable of unreachable thunks and the first dispatch SIGABRT'd with no
|
|
// diagnostic. With a real `impl` block the field dispatch must work; the
|
|
// non-conforming case is now a loud diagnostic (see the negative example).
|
|
//
|
|
// Covers: struct-literal init, field-assign init, reassigning a protocol field
|
|
// to a DIFFERENT concrete impl, a protocol method with args + non-void return,
|
|
// a protocol field beside a non-protocol field (offsets), a nested struct
|
|
// holding a struct holding a protocol field, and passing the holder by value
|
|
// into another function before dispatching.
|
|
|
|
#import "modules/std.sx";
|
|
|
|
Speaker :: protocol { greet :: (self: *Self, x: i64) -> i64; }
|
|
|
|
Dog :: struct { n: i64 = 0; }
|
|
impl Speaker for Dog { greet :: (self: *Dog, x: i64) -> i64 { return self.n + x; } }
|
|
|
|
Cat :: struct { m: i64 = 0; }
|
|
impl Speaker for Cat { greet :: (self: *Cat, x: i64) -> i64 { return self.m * x; } }
|
|
|
|
Holder :: struct { s: Speaker; b: i64 = 0; t: Speaker; }
|
|
Outer :: struct { h: Holder; }
|
|
|
|
run :: (h: Holder) -> i64 { return h.s.greet(10) + h.t.greet(2); }
|
|
|
|
main :: () {
|
|
d := Dog.{ n = 42 };
|
|
c := Cat.{ m = 5 };
|
|
|
|
// struct-literal init: two protocol fields straddling a non-protocol one.
|
|
h : Holder = .{ s = d, b = 7, t = c };
|
|
print("lit: {} {} {}\n", h.s.greet(10), h.b, h.t.greet(3)); // 52 7 15
|
|
|
|
// field-assign init, then reassign each field to a DIFFERENT concrete impl.
|
|
h2 : Holder = .{ s = d, b = 0, t = c };
|
|
h2.s = c;
|
|
h2.t = d;
|
|
print("reassign: {} {}\n", h2.s.greet(4), h2.t.greet(8)); // 20 50
|
|
|
|
// nested struct holding a struct holding a protocol field.
|
|
o : Outer = .{ h = .{ s = d, b = 1, t = c } };
|
|
print("nested: {} {}\n", o.h.s.greet(0), o.h.t.greet(2)); // 42 10
|
|
|
|
// pass the holder by value into a function, dispatch there.
|
|
print("passed: {}\n", run(h)); // 52 + 10 = 62
|
|
}
|