wcc: checker-seam drain — spreads, tuple aliases, yield scoping

Four seams from one review cluster, all landing on the same checker
files; gates ran on the union (rule 11 body).

?-subset: the wwstage walk compared `...spread` ALIAS nodes on both
sides of the error-subset check, falsely rejecting a spread-carried
error return (`(i64 | ...errs)`, cs-accept/ww-reject) — both sides
now flatten like trycountvariants (cstage Tparams are pre-flattened
at type level).

Multi-let/multi-assign: the tuple gates keyed on the RAW rhs kind,
rejecting a NAMED tuple alias (`type pair = (i64,i64)`; cs clean-
reject, ww asserttyped stop) — both stages chase per #99 alias
transparency, wwstage rettupleof peels the alias so cgmassign keeps
the str element's len/cap stores, and the catA massign-alias error
pin re-rules to a run fixture. The N_MLET diagnostic also printed
its operands swapped (elem/declared reversed vs its wording).

Yield: match_yield_type didn't descend into N_SWITCH, so a yield
inside a switch arm typed the match void and dropped the value
(both stages); and a stray yield outside any match arm reached cgen
unchecked — both stages now gate on a match-arm counter (the
c->loops discipline).
This commit is contained in:
2026-08-09 01:51:39 +09:00
parent 503d1ab01e
commit 411515a83b
10 changed files with 207 additions and 39 deletions

View File

@@ -187,6 +187,16 @@ match_yield_type(Node *body)
} }
if (body->kind == N_FOR || body->kind == N_FORRANGE) if (body->kind == N_FOR || body->kind == N_FORRANGE)
return match_yield_type(body->body); return match_yield_type(body->body);
/* a yield inside a switch arm yields from the enclosing MATCH
* (switch is a statement, not a yield scope) — invisible here,
* the match typed void and the yielded value was dropped. */
if (body->kind == N_SWITCH) {
for (Node *cs = body->list; cs; cs = cs->next) {
Type *t = match_yield_type(cs->body);
if (t) return t;
}
return NULL;
}
return NULL; return NULL;
} }
@@ -2193,7 +2203,9 @@ cexpr(Checker *c, Node *n)
scope_define(c->cur, cs->str, SK_VAR, vt, cs); scope_define(c->cur, cs->str, SK_VAR, vt, cs);
} }
} }
c->matcharms++;
cstmt(c, cs->body); cstmt(c, cs->body);
c->matcharms--;
c->cur = saved; c->cur = saved;
} }
/* Exhaustiveness: every variant must be handled. A default arm /* Exhaustiveness: every variant must be handled. A default arm
@@ -2678,7 +2690,10 @@ cstmt(Checker *c, Node *n)
} }
case N_MLET: { case N_MLET: {
Type *rt = cexpr(c, n->rhs); Type *rt = cexpr(c, n->rhs);
Type *u = (rt && rt->kind == TY_TUPLE) ? rt : NULL; /* #99 alias transparency: a NAMED tuple alias rhs
* (`type pair = (i64,i64)`) destructures like its base. */
Type *ru = type_chase_named(rt);
Type *u = (ru && ru->kind == TY_TUPLE) ? ru : NULL;
if (u == NULL) { if (u == NULL) {
err(c, n->pos, "multi-let rhs is not a tuple (got %s)", err(c, n->pos, "multi-let rhs is not a tuple (got %s)",
type_name(c->a, rt)); type_name(c->a, rt));
@@ -2691,8 +2706,8 @@ cstmt(Checker *c, Node *n)
(elem ? type_default(elem) : ty_err); (elem ? type_default(elem) : ty_err);
if (declared && elem && !type_assignable(declared, elem)) if (declared && elem && !type_assignable(declared, elem))
err(c, l->pos, "let %s: %s not assignable from %s", err(c, l->pos, "let %s: %s not assignable from %s",
l->str, type_name(c->a, elem), l->str, type_name(c->a, declared),
type_name(c->a, declared)); type_name(c->a, elem));
l->type = t; l->type = t;
if (l->str && l->str[0]) { if (l->str && l->str[0]) {
check_module_shadow(c, l->str, l->pos, "let"); check_module_shadow(c, l->str, l->pos, "let");
@@ -2711,7 +2726,9 @@ cstmt(Checker *c, Node *n)
} }
case N_MASSIGN: { case N_MASSIGN: {
Type *rt = cexpr(c, n->rhs); Type *rt = cexpr(c, n->rhs);
Type *u = (rt && rt->kind == TY_TUPLE) ? rt : NULL; /* #99 alias transparency — the N_MLET chase's twin. */
Type *ru = type_chase_named(rt);
Type *u = (ru && ru->kind == TY_TUPLE) ? ru : NULL;
if (u == NULL) { if (u == NULL) {
err(c, n->pos, "multi-assign rhs is not a tuple (got %s)", err(c, n->pos, "multi-assign rhs is not a tuple (got %s)",
type_name(c->a, rt)); type_name(c->a, rt));
@@ -2733,7 +2750,14 @@ cstmt(Checker *c, Node *n)
break; break;
} }
case N_DEFER: (void)cexpr(c, n->lhs); break; case N_DEFER: (void)cexpr(c, n->lhs); break;
case N_YIELD: if (n->lhs) (void)cexpr(c, n->lhs); break; case N_YIELD:
/* break/continue get the c->loops gate; a stray yield
* outside any match arm reached cgen unchecked and its
* value silently vanished. */
if (c->matcharms == 0)
err(c, n->pos, "yield outside match");
if (n->lhs) (void)cexpr(c, n->lhs);
break;
case N_BREAK: case N_BREAK:
case N_CONTINUE: case N_CONTINUE:
if (c->loops == 0) if (c->loops == 0)

View File

@@ -584,6 +584,7 @@ struct Checker {
* directives when refusing param/let names that * directives when refusing param/let names that
* would shadow an imported module bareword. */ * would shadow an imported module bareword. */
int loops; /* nesting count for break/continue */ int loops; /* nesting count for break/continue */
int matcharms; /* nesting count for yield */
int errs; int errs;
int is_test; /* #15: `w6c -T` — collect @test fns + synth int is_test; /* #15: `w6c -T` — collect @test fns + synth
* the entry; loud-reject a user main. */ * the entry; loud-reject a user main. */

View File

@@ -1,13 +1,13 @@
package wwfixture; package wwfixture;
def protocolversion: i32 = 1; def protocolversion: i32 = 1;
def corpuscount: i32 = 1754; def corpuscount: i32 = 1758;
def errorcount: i32 = 351; def errorcount: i32 = 351;
def compilecount: i32 = 21; def compilecount: i32 = 21;
def runcount: i32 = 209; def runcount: i32 = 209;
def runexitcount: i32 = 1173; def runexitcount: i32 = 1177;
def nativecount: i32 = 3508; def nativecount: i32 = 3516;
def corpushash: str = "67a119b41fd78b1f2900c096e5b5670e98935d2965573f67bb21ab817931dffa"; def corpushash: str = "ab5ae796c10e476af0c1f40e0d0b216b3eb708f62b6ee17aafcf79d5e1dd8a69";
type directive = enum i32 { type directive = enum i32 {
ERROR = 0, ERROR = 0,

View File

@@ -181,6 +181,14 @@ fn rettupleof(c: *cgen, rhs: *syntax.node) *syntax.node = {
if (cnm.len == 0) { return nil; }; if (cnm.len == 0) { return nil; };
let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod); let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod);
if (rtyp == nil) { return nil; }; if (rtyp == nil) { return nil; };
// #99 alias transparency: a NAMED tuple alias return (`fn f()
// pair`) destructures like its base — peel to the N_TTUPLE (the
// nodetuplearg alias-peel; without it the element walk saw no
// tuple and the str len/cap stores were dropped).
for (rtyp != nil && rtyp.kind == syntax.nkind.N_TNAME) {
rtyp = aliaslookup(c, rtyp.str);
};
if (rtyp == nil) { return nil; };
if (rtyp.kind != syntax.nkind.N_TTUPLE) { return nil; }; if (rtyp.kind != syntax.nkind.N_TTUPLE) { return nil; };
return rtyp; return rtyp;
}; };

View File

@@ -15,6 +15,8 @@ type checker = struct {
errs: i32, errs: i32,
loops: i32, // break/continue loop-nesting guard; cstage loops: i32, // break/continue loop-nesting guard; cstage
// twin cmd/wcc/check.c:598 (c->loops) // twin cmd/wcc/check.c:598 (c->loops)
matcharms: i32, // yield match-arm-nesting guard; cstage
// twin (c->matcharms)
istest: i32, // #15: `w6c_ww -T` — collect @test fns + istest: i32, // #15: `w6c_ww -T` — collect @test fns +
// synth the entry; loud-reject a user main. // synth the entry; loud-reject a user main.
verbose: i32, // when non-zero, log each unresolved name verbose: i32, // when non-zero, log each unresolved name
@@ -692,7 +694,11 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
checkmoduleshadow(c, nm, "binding"); checkmoduleshadow(c, nm, "binding");
syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n); syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n);
}; };
if (n.body != nil) { resolvewalk(c, n.body); }; if (n.body != nil) {
c.matcharms += 1;
resolvewalk(c, n.body);
c.matcharms -= 1;
};
c.cur = outer; c.cur = outer;
return; return;
}; };
@@ -754,7 +760,12 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (n.rhs != nil) { if (n.rhs != nil) {
// `rt` would shadow the imported lib/rt module // `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it. // (checkmoduleshadow errors); `rty` avoids it.
// #99 alias transparency: a NAMED tuple alias rhs
// (`type pair = (i64,i64)`) destructures like its
// base — resolvealias mirrors cstage's
// type_chase_named at the N_MLET gate.
let rty: *syntax.node = exprtype(c, n.rhs, nil); let rty: *syntax.node = exprtype(c, n.rhs, nil);
if (rty != nil) { rty = resolvealias(c, unwrapbang(rty)); };
if (rty != nil) { if (rty.kind == syntax.nkind.N_TTUPLE) { if (rty != nil) { if (rty.kind == syntax.nkind.N_TTUPLE) {
pt = rty.list; pt = rty.list;
}; }; }; };
@@ -775,20 +786,19 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
let pt: *syntax.node = nil; let pt: *syntax.node = nil;
// #242: consume the rhs tuple type for ANY rhs (see N_MLET). // #242: consume the rhs tuple type for ANY rhs (see N_MLET).
if (n.rhs != nil) { if (n.rhs != nil) {
// #99 alias transparency — the N_MLET chase's twin
// (both stages now chase the NAMED wrapper here).
let rty: *syntax.node = exprtype(c, n.rhs, nil); let rty: *syntax.node = exprtype(c, n.rhs, nil);
if (rty != nil) { rty = resolvealias(c, unwrapbang(rty)); };
if (rty != nil && rty.kind == syntax.nkind.N_TTUPLE) { if (rty != nil && rty.kind == syntax.nkind.N_TTUPLE) {
pt = rty.list; pt = rty.list;
} else { } else {
// #38/F2 (review item 39): the multi-assign rhs must be a // #38/F2 (review item 39): the multi-assign rhs must be a
// tuple. cstage check.c:2562-2568 errors "multi-assign rhs // tuple. cstage check.c errors "multi-assign rhs is not a
// is not a tuple (got %s)" when cexpr(rhs)->kind != TY_TUPLE // tuple (got %s)". Without this ww distributed nil element
// — and, like ww's exprtype (N_CALL returns the decl's bare // widths and cgmassign silently dropped the str len/cap
// return tnode, :3319), it does NOT chase the NAMED wrapper, // stores. ww's piecewise cerr can't splice the type
// so a tuple-ALIAS return (`fn f() pair`) is rejected too. // spelling, so the "(got %s)" tail is omitted.
// Without this ww distributed nil element widths and
// cgmassign silently dropped the str len/cap stores. ww's
// piecewise cerr can't splice the type spelling, so the
// "(got %s)" tail is omitted.
deffolderr(c, n, "multi-assign rhs is not a tuple"); deffolderr(c, n, "multi-assign rhs is not a tuple");
}; };
}; };
@@ -919,6 +929,15 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (k == syntax.nkind.N_CALL) { desugarcallargs(c, n); }; if (k == syntax.nkind.N_CALL) { desugarcallargs(c, n); };
if (k == syntax.nkind.N_ASSIGN) { checkassign(c, n); }; if (k == syntax.nkind.N_ASSIGN) { checkassign(c, n); };
// break/continue get the loops gate; a stray yield outside any
// match arm reached cgen unchecked and its value silently
// vanished. Mirror cstage cstmt N_YIELD.
if (k == syntax.nkind.N_YIELD) {
if (c.matcharms == 0) {
cerr("error: yield outside match\n");
c.errs += 1;
};
};
if (k == syntax.nkind.N_INTLIT || k == syntax.nkind.N_FLOATLIT || if (k == syntax.nkind.N_INTLIT || k == syntax.nkind.N_FLOATLIT ||
k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT || k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT ||
k == syntax.nkind.N_TRUE || k == syntax.nkind.N_FALSE || k == syntax.nkind.N_TRUE || k == syntax.nkind.N_FALSE ||
@@ -1555,6 +1574,18 @@ fn matchyieldtype(c: *checker, body: *syntax.node, bname: str, btype: *syntax.no
if (k == syntax.nkind.N_FOR || k == syntax.nkind.N_FORRANGE) { if (k == syntax.nkind.N_FOR || k == syntax.nkind.N_FORRANGE) {
return matchyieldtype(c, body.body, bname, btype, nodeout); return matchyieldtype(c, body.body, bname, btype, nodeout);
}; };
// a yield inside a switch arm yields from the enclosing MATCH
// (switch is a statement, not a yield scope) — invisible here,
// the match typed void and the yielded value was dropped.
if (k == syntax.nkind.N_SWITCH) {
let cs: *syntax.node = body.list;
for (cs != nil) {
let t: *syntax.tinfo = matchyieldtype(c, cs.body, bname, btype, nodeout);
if (t != nil) { return t; };
cs = cs.next;
};
return nil;
};
return nil; return nil;
}; };
@@ -6791,6 +6822,54 @@ fn trycountvariants(c: *checker, outer: *syntax.node, v: *syntax.node, nsuccp: *
}; };
}; };
// tryerrinreturn — is error variant `ev` present in the return union's
// variant list, flattening `...inner` spreads? cstage compares over
// PRE-FLATTENED Tparams (resolve_type inlines spreads, #61a), so its
// per-param walk sees spliced members natively; the wwstage AST walk
// compared the spread ALIAS node and falsely rejected a spread-carried
// error (`(i64 | ...errs)` return, cs-accept/ww-reject).
fn tryerrinreturn(c: *checker, rvlist: *syntax.node, ev: *syntax.node, depth: i32) bool = {
let rv: *syntax.node = rvlist;
for (rv != nil) {
if (rv.op == syntax.tkind.TK_ELLIPSIS && depth < 8i32) {
let inner: *syntax.node = resolvealias(c, unwrapbang(rv));
if (inner != nil && inner.kind == syntax.nkind.N_TTAGGED) {
if (tryerrinreturn(c, inner.list, ev, depth + 1i32)) { return true; };
rv = rv.next;
continue;
};
};
if (typeeqast(c, rv, ev)) { return true; };
rv = rv.next;
};
return false;
};
// trysubseterrs — the ?-subset walk over the OPERAND union's error
// variants, flattening its `...inner` spreads the same way (the
// trycountvariants pattern; outer `u` rides through for iserrvariant
// exactly as there).
fn trysubseterrs(c: *checker, u: *syntax.node, evlist: *syntax.node, r: *syntax.node, depth: i32) void = {
let ev: *syntax.node = evlist;
for (ev != nil) {
if (ev.op == syntax.tkind.TK_ELLIPSIS && depth < 8i32) {
let inner: *syntax.node = resolvealias(c, unwrapbang(ev));
if (inner != nil && inner.kind == syntax.nkind.N_TTAGGED) {
trysubseterrs(c, u, inner.list, r, depth + 1i32);
ev = ev.next;
continue;
};
};
if (iserrvariant(c, u, ev)) {
if (!tryerrinreturn(c, r.list, ev, 0i32)) {
cerr("?: error variant not in enclosing return\n");
c.errs += 1;
};
};
ev = ev.next;
};
};
fn checktryprop(c: *checker, n: *syntax.node) void = { fn checktryprop(c: *checker, n: *syntax.node) void = {
if (n == nil) { return; }; if (n == nil) { return; };
let t: *syntax.node = exprtypeoftry(c, n.lhs); let t: *syntax.node = exprtypeoftry(c, n.lhs);
@@ -6833,24 +6912,7 @@ fn checktryprop(c: *checker, n: *syntax.node) void = {
c.errs += 1; c.errs += 1;
return; return;
}; };
let ev: *syntax.node = u.list; trysubseterrs(c, u, u.list, r, 0i32);
for (ev != nil) {
if (iserrvariant(c, u, ev)) {
let found: bool = false;
let rv: *syntax.node = r.list;
for (rv != nil) {
if (typeeqast(c, rv, ev)) {
found = true;
rv = nil;
} else { rv = rv.next; };
};
if (!found) {
cerr("?: error variant not in enclosing return\n");
c.errs += 1;
};
};
ev = ev.next;
};
}; };
// hascvariadic — true iff the param list ends in a bare C-style `...` // hascvariadic — true iff the param list ends in a bare C-style `...`

View File

@@ -0,0 +1,21 @@
//ww:run-exit 0
// A yield inside a switch arm yields from the enclosing MATCH; the
// match-yield walk didn't descend into N_SWITCH, so the match typed
// void and the yielded value was dropped.
package main;
fn pick(v: (i32 | str)) i32 = {
let r: i32 = match (v) {
case let x: i32 =>
switch (x) {
case 2: yield 42;
case: yield 7;
};
case let s: str => yield 0;
};
return r;
};
export fn main() i32 = {
let u: (i32 | str) = 2;
if (pick(u) == 42) { return 0; };
return 1;
};

View File

@@ -0,0 +1,12 @@
//ww:run-exit 0
// #99 alias transparency at multi-let: a NAMED tuple alias rhs
// destructures like its base (was cs-reject "not a tuple" + ww
// asserttyped stop on unstamped binds).
package main;
type pair = (i64, i64);
fn mk() pair = { return (3, 4); };
export fn main() i32 = {
let (a, b) = mk();
if (a + b == 7) { return 0; };
return 1;
};

View File

@@ -1,5 +1,16 @@
//ww:error "multi-assign rhs is not a tuple" //ww:run-exit 0
// #99 alias transparency at multi-assign (re-ruled from the catA
// error pin): a NAMED tuple alias return destructures like its base;
// rettupleof peels the alias so the str element keeps len/cap.
package main; package main;
type pair = (i64, str); type pair = (i64, str);
fn f() pair = { return (7i64, "hey"); }; fn f() pair = { return (7i64, "hey"); };
export fn main() i32 = { let a: i64 = 0i64; let s: str = ""; a, s = f(); return 0; }; export fn main() i32 = {
let a: i64 = 0i64;
let s: str = "";
a, s = f();
if (a != 7i64) { return 1; };
if (s.len != 3) { return 2; };
if (s[0] != 'h': u8) { return 3; };
return 0;
};

View File

@@ -0,0 +1,21 @@
//ww:run-exit 0
// ?-subset walk flattens ...spread members on BOTH sides; the raw
// AST walk compared the spread alias node and falsely rejected this
// (ww-only; cstage Tparams are pre-flattened at type level).
package main;
type ea = !i32;
type eb = !i64;
type errs = (ea | eb);
fn inner(sel: i32) (i64 | ea | eb) = {
if (sel == 0) { return 7i64; };
return 5: ea;
};
fn outer() (i64 | ...errs) = {
let v: i64 = inner(1)?;
return v;
};
export fn main() i32 = {
let r: (i64 | ...errs) = outer();
if (r is ea) { return 0; };
return 1;
};

View File

@@ -0,0 +1,8 @@
//ww:error "yield outside match"
// break/continue get the loops gate; a stray yield reached cgen
// unchecked and its value silently vanished.
package main;
export fn main() i32 = {
yield 5;
return 0;
};