diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index e28db632..b5f1778f 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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) diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index d9acfa19..28797fc9 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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. */ diff --git a/internal/wwfixture/types.ww b/internal/wwfixture/types.ww index b642d036..5b9e7e27 100644 --- a/internal/wwfixture/types.ww +++ b/internal/wwfixture/types.ww @@ -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, diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 9616d705..5afae19d 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -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; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 414b519a..b75c8f8b 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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 `...` diff --git a/test/wcc/data/match_yield_in_switch/case.ww b/test/wcc/data/match_yield_in_switch/case.ww new file mode 100644 index 00000000..e19932ca --- /dev/null +++ b/test/wcc/data/match_yield_in_switch/case.ww @@ -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; +}; diff --git a/test/wcc/data/mlet_tuple_alias/case.ww b/test/wcc/data/mlet_tuple_alias/case.ww new file mode 100644 index 00000000..eb2dc0fe --- /dev/null +++ b/test/wcc/data/mlet_tuple_alias/case.ww @@ -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; +}; diff --git a/test/wcc/data/r989_cata_massign_alias/case.ww b/test/wcc/data/r989_cata_massign_alias/case.ww index e3da2b1b..aa927559 100644 --- a/test/wcc/data/r989_cata_massign_alias/case.ww +++ b/test/wcc/data/r989_cata_massign_alias/case.ww @@ -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; +}; diff --git a/test/wcc/data/tryprop_spread_return/case.ww b/test/wcc/data/tryprop_spread_return/case.ww new file mode 100644 index 00000000..0bc7854a --- /dev/null +++ b/test/wcc/data/tryprop_spread_return/case.ww @@ -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; +}; diff --git a/test/wcc/data/yield_outside_match/case.ww b/test/wcc/data/yield_outside_match/case.ww new file mode 100644 index 00000000..fae21127 --- /dev/null +++ b/test/wcc/data/yield_outside_match/case.ww @@ -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; +};