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

@@ -15,6 +15,8 @@ type checker = struct {
errs: i32,
loops: i32, // break/continue loop-nesting guard; cstage
// 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 +
// synth the entry; loud-reject a user main.
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");
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;
return;
};
@@ -754,7 +760,12 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
if (n.rhs != nil) {
// `rt` would shadow the imported lib/rt module
// (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);
if (rty != nil) { rty = resolvealias(c, unwrapbang(rty)); };
if (rty != nil) { if (rty.kind == syntax.nkind.N_TTUPLE) {
pt = rty.list;
}; };
@@ -775,20 +786,19 @@ fn resolvewalk(c: *checker, n: *syntax.node) void = {
let pt: *syntax.node = nil;
// #242: consume the rhs tuple type for ANY rhs (see N_MLET).
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);
if (rty != nil) { rty = resolvealias(c, unwrapbang(rty)); };
if (rty != nil && rty.kind == syntax.nkind.N_TTUPLE) {
pt = rty.list;
} else {
// #38/F2 (review item 39): the multi-assign rhs must be a
// tuple. cstage check.c:2562-2568 errors "multi-assign rhs
// is not a tuple (got %s)" when cexpr(rhs)->kind != TY_TUPLE
// — and, like ww's exprtype (N_CALL returns the decl's bare
// return tnode, :3319), it does NOT chase the NAMED wrapper,
// so a tuple-ALIAS return (`fn f() pair`) is rejected too.
// 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.
// tuple. cstage check.c errors "multi-assign rhs is not a
// tuple (got %s)". 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");
};
};
@@ -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_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 ||
k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT ||
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) {
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;
};
@@ -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 = {
if (n == nil) { return; };
let t: *syntax.node = exprtypeoftry(c, n.lhs);
@@ -6833,24 +6912,7 @@ fn checktryprop(c: *checker, n: *syntax.node) void = {
c.errs += 1;
return;
};
let ev: *syntax.node = u.list;
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;
};
trysubseterrs(c, u, u.list, r, 0i32);
};
// hascvariadic — true iff the param list ends in a bare C-style `...`