fix: diagnose non-conforming protocol erasure instead of unreachable-thunk SIGABRT (issue 0176)
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).
This commit is contained in:
@@ -1,5 +1,25 @@
|
||||
# 0176 — calling a method through a protocol-typed struct field aborts (exit 133, no diagnostic)
|
||||
|
||||
> **RESOLVED** (root cause differs from the title's hypothesis). The crash had
|
||||
> nothing to do with struct fields: erasing a type to a protocol when the type
|
||||
> conforms only via a FREE FUNCTION (`speak :: (self: *Dog)`) rather than an
|
||||
> explicit `impl Speaker for Dog { ... }` built a vtable of `unreachable` thunks
|
||||
> → SIGABRT on dispatch. Per specs.md §"Storage and protocol conformance"
|
||||
> (erasure is impl-driven, not structural), the repro was never valid. Fix
|
||||
> (`src/ir/lower/protocol.zig`): a conformance gate `firstUnimplementedMethod` in
|
||||
> `buildProtocolValue` emits a located diagnostic (missing impl, or a
|
||||
> signature-mismatch when an impl method introduces its own `$T`) instead of
|
||||
> building unreachable thunks; 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 by 3+1 adversarial
|
||||
> reviews; suite 788/0. Regressions:
|
||||
> `examples/protocols/0419-protocols-struct-field-dispatch.sx` (positive),
|
||||
> `examples/diagnostics/1197-diagnostics-protocol-erasure-no-impl.sx` +
|
||||
> `1198-diagnostics-protocol-erasure-generic-method.sx` (negative). Updated
|
||||
> `examples/memory/0808-*.sx` (it relied on a non-conforming erasure that never
|
||||
> dispatched). (Adjacent pre-existing bug found + filed: 0178 — protocol impl
|
||||
> method with a mismatched return/param TYPE silently miscompiles.)
|
||||
|
||||
## Symptom
|
||||
|
||||
A struct field whose type is a PROTOCOL holds an erased value fine, but calling a
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# 0178 — protocol impl method with a mismatched return/param TYPE silently miscompiles
|
||||
|
||||
## Symptom
|
||||
|
||||
An `impl P for T` whose method has the right NAME but a mismatched return type or
|
||||
parameter type is accepted (it satisfies the issue-0176 conformance gate, which
|
||||
is name-based), and dispatch through the erased protocol silently produces the
|
||||
WRONG result (exit 0). No diagnostic. (Arity mismatch and `#builtin`-body
|
||||
mismatch fail loudly — exit 1 — and are not this bug; the TYPE-mismatch cases are
|
||||
silent.)
|
||||
|
||||
## Reproduction
|
||||
|
||||
```sx
|
||||
#import "modules/std.sx";
|
||||
P :: protocol { val :: (self: *Self) -> i64; }
|
||||
T :: struct { n: i64 = 7; }
|
||||
impl P for T { val :: (self: *T) -> bool { return true; } } // return type bool ≠ i64
|
||||
main :: () {
|
||||
t := T.{ n = 7 };
|
||||
p : P = t;
|
||||
print("{}\n", p.val()); // prints "1" (the bool), silently wrong — no diagnostic
|
||||
}
|
||||
```
|
||||
|
||||
A parameter-type mismatch (`x: bool` where the protocol declares `x: i64`)
|
||||
similarly dispatches silently wrong.
|
||||
|
||||
## Investigation prompt
|
||||
|
||||
The issue-0176 conformance gate (`firstUnimplementedMethod` in
|
||||
`src/ir/lower/protocol.zig`) checks method PRESENCE (and rejects `type_params >
|
||||
0`), but does NOT check that the impl method's SIGNATURE (parameter types,
|
||||
arity, return type) matches the protocol method's declared signature. A
|
||||
mismatched-type impl builds a thunk that calls the impl with the wrong ABI,
|
||||
silently miscompiling. Add signature validation when registering / gating an
|
||||
impl method against its protocol method: compare the impl method's params
|
||||
(after the erased `self`) and return type against the protocol declaration, and
|
||||
emit a located diagnostic on mismatch (arity, param type, or return type). The
|
||||
protocol method declaration is in `protocol_decl_map`; the impl FnDecl is in
|
||||
`fn_ast_map`. Decide whether this lives in the conformance gate or in
|
||||
`ProtocolResolver.registerImplBlock` (`src/ir/protocols.zig`). Follow the
|
||||
no-silent-fallback rule. Verify: the repro is now a clean diagnostic (exit 1);
|
||||
a correctly-typed impl still works; add an `examples/diagnostics/11xx-...`
|
||||
negative regression. (Found during adversarial review of issue 0176.)
|
||||
Reference in New Issue
Block a user