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)
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;
}
@@ -2193,7 +2203,9 @@ cexpr(Checker *c, Node *n)
scope_define(c->cur, cs->str, SK_VAR, vt, cs);
}
}
c->matcharms++;
cstmt(c, cs->body);
c->matcharms--;
c->cur = saved;
}
/* Exhaustiveness: every variant must be handled. A default arm
@@ -2678,7 +2690,10 @@ cstmt(Checker *c, Node *n)
}
case N_MLET: {
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) {
err(c, n->pos, "multi-let rhs is not a tuple (got %s)",
type_name(c->a, rt));
@@ -2691,8 +2706,8 @@ cstmt(Checker *c, Node *n)
(elem ? type_default(elem) : ty_err);
if (declared && elem && !type_assignable(declared, elem))
err(c, l->pos, "let %s: %s not assignable from %s",
l->str, type_name(c->a, elem),
type_name(c->a, declared));
l->str, type_name(c->a, declared),
type_name(c->a, elem));
l->type = t;
if (l->str && l->str[0]) {
check_module_shadow(c, l->str, l->pos, "let");
@@ -2711,7 +2726,9 @@ cstmt(Checker *c, Node *n)
}
case N_MASSIGN: {
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) {
err(c, n->pos, "multi-assign rhs is not a tuple (got %s)",
type_name(c->a, rt));
@@ -2733,7 +2750,14 @@ cstmt(Checker *c, Node *n)
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_CONTINUE:
if (c->loops == 0)