wcc+w6c_ww: loud-gate try-propagation over multi-success unions (F8/F9 interim)

? and ! assume ONE success member end-to-end: the checker collapses
the result to the first non-error variant (check.c tagged_success_type
/ check.ww exprtype) and cgen emits a single tag compare, so any other
success member is silently mistaken for an error — ? propagates it to
the caller (p11h: []capture read back as nomem, exit 21), ! aborts on
it. Until the honest subset-union result typing lands (task #14, harec
check.c:2759-2835), both stages loud-reject |success| > 1 at the
checker choke-points (one per stage), identical diagnostic, both ops
per rob's one-class ruling (#133 precedent). (T|err1|err2) — one
success, many errors — stays legal (925 canary + new accept rows).

F9 rides along (task #12): wwstage scruttype only resolves IDENT/DOT,
so the direct forms f()? is T / match(f()?) / f()! is T slipped its
lenient-miss contract and were silently ACCEPTED where cstage rejects
(cs!=ww, gate-blind). checkisas/checkmatchexhaust now resolve the
try-result via exprtype, keyed on the RESOLVED success type — a named
tagged success ((ab|nomem)? is i32) keeps being accepted, matching
cstage's verdict empirically.

test/wcc/806: 11 rows x dual driver + byte-id accepts (26 fixtures);
reject rows pin exact per-stage diagnostic text; p11h + q_card2_unw
graduated to rejects; call-arg-position reject + void-success accept
pin position-independence and the dominant lib/ (void|err)? shape.
Tasks #5 + #12; #14 lifts both gates together.
This commit is contained in:
2026-06-04 10:10:35 +09:00
parent cfc2985c61
commit 48df04a8ca
6 changed files with 686 additions and 20 deletions

View File

@@ -10642,6 +10642,7 @@ fn resolvewalk(c: *checker, n: *node) void = {
// is examined before the arm bodies install new bindings.
if (k == nkind.N_MATCH) { checkmatchexhaust(c, n); };
if (k == nkind.N_TRYPROP) { checktryprop(c, n); };
if (k == nkind.N_TRYUNW) { checktryprop(c, n); };
if (k == nkind.N_TYPETEST) { checkisas(c, n); };
if (k == nkind.N_TYPEASSERT) { checkisas(c, n); };
if (k == nkind.N_LET) { checkletassign(c, n); };
@@ -14029,9 +14030,30 @@ fn checkmatchexhaust(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
let st: *node = scruttype(c, n.lhs);
// F9 (task #12): direct `match (expr?)` / `match (expr!)` — cstage
// types the try-result as the success variant and rejects when it
// is not itself a tagged union (check.c "match on non-tagged-
// union"); scruttype's IDENT/DOT-only resolution let the form slip
// through silently (cs≠ww). The reject below is gated on the
// try-form so the lenient-miss contract for other unresolvable
// scrutinees is untouched; a tagged success keeps flowing into the
// normal exhaustiveness walk, matching cstage's accept.
let istry: bool = false;
if (st == nil) {
if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) {
istry = true;
st = exprtype(c, n.lhs, nil);
};
};
let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; };
if (u.kind != nkind.N_TTAGGED) { return; };
if (u.kind != nkind.N_TTAGGED) {
if (istry) {
cerr("match on non-tagged-union try-result\n");
c.errs += 1;
};
return;
};
// #13: the union's defining module, so a bare body variant can be
// matched against a module-qualified cross-module case pattern (and
// a foreign-qualifier pattern correctly rejected). See
@@ -14610,6 +14632,19 @@ fn checkisas(c: *checker, n: *node) void = {
if (n == nil) { return; };
// e is in n.lhs (value), T is in n.rhs (type expr).
let st: *node = scruttype(c, n.lhs);
// F9 (task #12): direct `expr? is T` / `expr! is T` — cstage cexpr
// types the ?/! result as the success variant and the non-tagged
// gate below then rejects (check.c "is on non-tagged-union").
// scruttype only resolves IDENT/DOT, so the direct try-form slipped
// through the lenient-miss contract and wwstage silently ACCEPTED
// (cs≠ww). Resolve the try-result here; a tagged success (named
// union variant) flows on into the variant checks, matching
// cstage's accept.
if (st == nil && n.lhs != nil) {
if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) {
st = exprtype(c, n.lhs, nil);
};
};
let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; };
// #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`).
@@ -14666,8 +14701,10 @@ fn checkisas(c: *checker, n: *node) void = {
//
// For `expr?`, the operand's error subset must be a subset of the
// enclosing fn's return-type variants. Mirrors C check.c. Operand
// is nkind.N_TRYPROP; its lhs is the value-bearing expr; we look at the
// expr's *declared* type for nkind.N_IDENT/nkind.N_CALL cases.
// is nkind.N_TRYPROP or nkind.N_TRYUNW (the F8 cardinality gate covers
// both; the subset walk is ?-only); its lhs is the value-bearing expr;
// we look at the expr's *declared* type for nkind.N_IDENT/nkind.N_CALL
// cases.
fn exprtypeoftry(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
@@ -14702,13 +14739,30 @@ fn checktryprop(c: *checker, n: *node) void = {
let u: *node = resolvealias(c, unwrapbang(t));
if (u == nil) { return; };
if (u.kind != nkind.N_TTAGGED) { return; };
// Does the operand have any error variants?
// F8 interim gate (task #5): try-propagation assumes ONE success
// member end-to-end — exprtype collapses to the first non-error
// variant and cgen emits a single tag compare, so any OTHER
// success member is silently mistaken for an error (? propagates
// it; ! aborts on it). One class, both ops (#133 precedent).
// Until the honest subset-union result typing lands (task #14,
// harec check.c:2759-2835), reject loud. Mirrors cstage check.c
// N_TRYPROP/N_TRYUNW.
let haserr: bool = false;
let nsucc: int = 0;
let v: *node = u.list;
for (v != nil) {
if (iserrvariant(c, u, v)) { haserr = true; };
if (iserrvariant(c, u, v)) { haserr = true; } else { nsucc += 1; };
v = v.next;
};
if (nsucc > 1) {
if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); };
cerr(": multi-success union unwired (task #14): bind and match instead\n");
c.errs += 1;
return;
};
// `!` has no propagation, so no error-subset check (mirrors
// cstage's N_TRYPROP-only guard on the subset walk).
if (n.kind != nkind.N_TRYPROP) { return; };
if (!haserr) { return; };
// Enclosing fn must return a tagged union with each operand
// error variant present.