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:
@@ -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)
|
||||
|
||||
@@ -584,6 +584,7 @@ struct Checker {
|
||||
* directives when refusing param/let names that
|
||||
* would shadow an imported module bareword. */
|
||||
int loops; /* nesting count for break/continue */
|
||||
int matcharms; /* nesting count for yield */
|
||||
int errs;
|
||||
int is_test; /* #15: `w6c -T` — collect @test fns + synth
|
||||
* the entry; loud-reject a user main. */
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package wwfixture;
|
||||
|
||||
def protocolversion: i32 = 1;
|
||||
def corpuscount: i32 = 1754;
|
||||
def corpuscount: i32 = 1758;
|
||||
def errorcount: i32 = 351;
|
||||
def compilecount: i32 = 21;
|
||||
def runcount: i32 = 209;
|
||||
def runexitcount: i32 = 1173;
|
||||
def nativecount: i32 = 3508;
|
||||
def corpushash: str = "67a119b41fd78b1f2900c096e5b5670e98935d2965573f67bb21ab817931dffa";
|
||||
def runexitcount: i32 = 1177;
|
||||
def nativecount: i32 = 3516;
|
||||
def corpushash: str = "ab5ae796c10e476af0c1f40e0d0b216b3eb708f62b6ee17aafcf79d5e1dd8a69";
|
||||
|
||||
type directive = enum i32 {
|
||||
ERROR = 0,
|
||||
|
||||
@@ -181,6 +181,14 @@ fn rettupleof(c: *cgen, rhs: *syntax.node) *syntax.node = {
|
||||
if (cnm.len == 0) { return nil; };
|
||||
let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod);
|
||||
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; };
|
||||
return rtyp;
|
||||
};
|
||||
|
||||
@@ -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 `...`
|
||||
|
||||
21
test/wcc/data/match_yield_in_switch/case.ww
Normal file
21
test/wcc/data/match_yield_in_switch/case.ww
Normal 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;
|
||||
};
|
||||
12
test/wcc/data/mlet_tuple_alias/case.ww
Normal file
12
test/wcc/data/mlet_tuple_alias/case.ww
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
type pair = (i64, str);
|
||||
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;
|
||||
};
|
||||
|
||||
21
test/wcc/data/tryprop_spread_return/case.ww
Normal file
21
test/wcc/data/tryprop_spread_return/case.ww
Normal 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;
|
||||
};
|
||||
8
test/wcc/data/yield_outside_match/case.ww
Normal file
8
test/wcc/data/yield_outside_match/case.ww
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user