diff --git a/Makefile b/Makefile index af44a3f7..40cbb48b 100644 --- a/Makefile +++ b/Makefile @@ -339,6 +339,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_type_value_shadow_run \ $(BIN)/test_xmod_alias_struct_collide_run \ $(BIN)/test_xmod_variant_match \ + $(BIN)/test_spread_variant_match \ $(BIN)/test_named_ptr_alias_variant_widen \ $(BIN)/test_single_field_struct_zeroinit \ $(BIN)/test_structvariant_largeunion_return \ @@ -775,6 +776,20 @@ $(BIN)/test_xmod_variant_match: test/wcc/787_xmod_variant_match.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #209: a `match` over a tagged union with a `...inner` SPREAD variant +# (e.g. fmt's `field = (...formattable | *mods)`). The wwstage checker +# walked the raw AST u.list and false-rejected every flattened member arm +# ("case: not a variant" / "variant not handled"), so any fmt-importing +# unit was wwstage-uncompilable. Fix flattens the spread in the checker's +# match validity + exhaustiveness walk (selfhost/cmd/wcc/check.ww). The +# w6c_ww-accepts + cs==ww byte-id on the driver combined is the +# discriminator. Self-contained single-file probe. +$(BIN)/test_spread_variant_match: test/wcc/792_spread_variant_match.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #15: widening a bare *vtable into a NAMED-alias variant (`stream` = # *vtable) of `(file | stream)` must compute the right tag, not default # to tag 0. Both-stage byte-id + runtime, plus a degenerate-ambiguity diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a89393ab..893bb5b0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11943,10 +11943,6 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let v: *node = n.list; for (v != nil) { let vt: *tinfo = tinfofornode(c, v); - if (vt != nil) { - if (vt.size > maxsz) { maxsz = vt.size; }; - if (vt.align > al) { al = vt.align; }; - }; let isspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *tinfo = vt; if (isspread) { @@ -11955,16 +11951,31 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { }; }; if (isspread && vu != nil && vu.kind == tykind.TY_TAGGED) { - // Spliced variants carry the inner union's - // already-stamped iserror; no re-derivation. + // #209: a `...inner` spread's PAYLOAD is its + // members, not the whole inner union — size/align + // off each spliced member (cstage check.c:660-667 + // st->size), NOT the surface member vt (which would + // over-size by the inner union's own 8B tag word and + // desync the `field` slot from cstage's). Spliced + // variants carry the inner union's already-stamped + // iserror; no re-derivation. let src: *tparam = vu.params; for (src != nil) { + let st: *tinfo = src.type_; + if (st != nil) { + if (st.size > maxsz) { maxsz = st.size; }; + if (st.align > al) { al = st.align; }; + }; let tp: *tparam = alloc(tparam{name="", type_=src.type_, iserror=src.iserror, tnext=nil})!; if (head == nil) { head = tp; } else { tail.tnext = tp; }; tail = tp; src = src.tnext; }; } else { + if (vt != nil) { + if (vt.size > maxsz) { maxsz = vt.size; }; + if (vt.align > al) { al = vt.align; }; + }; let ve: bool = varianterr(c, v); let tp: *tparam = alloc(tparam{name="", type_=vt, iserror=ve, tnext=nil})!; if (head == nil) { head = tp; } else { tail.tnext = tp; }; @@ -13509,9 +13520,30 @@ fn errmatchvariant(c: *checker, n: *node, vname: *node) void = { // casevariantin — true iff `pat` (a `case T` pattern, including // each alt of a multi-pattern) names a variant of the tagged // union `tagged`. -fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = { +// +// #209: a `...inner` spread variant (v.op == TK_ELLIPSIS, parse.ww:284) +// is NOT a variant in itself — its inner tagged union's MEMBERS are. The +// AST `tagged.list` keeps the spread unexpanded (only the #61a tinfo +// build flattens it), so recurse into the inner union's members, +// attributing them the inner union's defining module. Mirrors cstage's +// resolve_type flatten (cmd/wcc/check.c:651-674) at the AST layer; the +// cgen side already dispatches off the flattened tinfo.params (#61a). +fn casevariantin(c: *checker, tagged: *node, pat: *node, unionmod: str) bool = { let v: *node = tagged.list; for (v != nil) { + if (v.op == tkind.TK_ELLIPSIS) { + let inner: *node = resolvealias(c, unwrapbang(v)); + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + if (casevariantin(c, inner, pat, + taggeddefmod(c, v))) { + return true; + }; + v = v.next; + continue; + }; + }; + }; if (typeeqast(v, pat)) { return true; }; if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; @@ -13550,12 +13582,12 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let cs0: *node = n.list; for (cs0 != nil) { if (cs0.lhs != nil) { - if (!casevariantin(u, cs0.lhs, unionmod)) { + if (!casevariantin(c, u, cs0.lhs, unionmod)) { errbadcase(c, cs0.lhs); }; let alt: *node = cs0.list; for (alt != nil) { - if (!casevariantin(u, alt, unionmod)) { + if (!casevariantin(c, u, alt, unionmod)) { errbadcase(c, alt); }; alt = alt.next; @@ -13572,21 +13604,46 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { // For each variant of u, look for a covering case. let v: *node = u.list; for (v != nil) { - let covered: bool = false; - let cs2: *node = n.list; - for (cs2 != nil) { - if (casecovers(c, cs2, v, unionmod)) { - covered = true; - cs2 = nil; - } else { - cs2 = cs2.next; - }; - }; - if (!covered) { errmatchvariant(c, n, v); }; + checkvariantcovered(c, n, v, unionmod); v = v.next; }; }; +// checkvariantcovered — emit "variant not handled" unless some case arm +// covers `v`. #209: a `...inner` spread variant expands to its inner +// union's members (each attributed the inner union's defining module), +// so the phantom spread node is never itself reported uncovered — its +// members are checked instead. Mirrors the casevariantin spread recursion +// + cstage's flattened u->params exhaustiveness walk (cmd/wcc/check.c: +// 1648-1669). Leaf variants keep their NODE so errmatchvariant names them. +fn checkvariantcovered(c: *checker, n: *node, v: *node, unionmod: str) void = { + if (v.op == tkind.TK_ELLIPSIS) { + let inner: *node = resolvealias(c, unwrapbang(v)); + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let im: str = taggeddefmod(c, v); + let m: *node = inner.list; + for (m != nil) { + checkvariantcovered(c, n, m, im); + m = m.next; + }; + return; + }; + }; + }; + let covered: bool = false; + let cs2: *node = n.list; + for (cs2 != nil) { + if (casecovers(c, cs2, v, unionmod)) { + covered = true; + cs2 = nil; + } else { + cs2 = cs2.next; + }; + }; + if (!covered) { errmatchvariant(c, n, v); }; +}; + // ---- let init / return assignability -------------------------------- // // AST-level approximation: when we can infer src's type and dst is @@ -13827,7 +13884,7 @@ fn checkisas(c: *checker, n: *node) void = { if (utinfo != nil) { if (wanttinfo != nil) { if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; }; }; - if (casevariantin(u, want, taggeddefmod(c, st))) { return; }; + if (casevariantin(c, u, want, taggeddefmod(c, st))) { return; }; os.write(2, "is/as: not a variant of operand".ptr, 31u64); if (want.kind == nkind.N_TNAME) { os.write(2, " (".ptr, 2u64); diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 0c7c3948..723c1b11 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -1831,10 +1831,6 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let v: *node = n.list; for (v != nil) { let vt: *tinfo = tinfofornode(c, v); - if (vt != nil) { - if (vt.size > maxsz) { maxsz = vt.size; }; - if (vt.align > al) { al = vt.align; }; - }; let isspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *tinfo = vt; if (isspread) { @@ -1843,16 +1839,31 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { }; }; if (isspread && vu != nil && vu.kind == tykind.TY_TAGGED) { - // Spliced variants carry the inner union's - // already-stamped iserror; no re-derivation. + // #209: a `...inner` spread's PAYLOAD is its + // members, not the whole inner union — size/align + // off each spliced member (cstage check.c:660-667 + // st->size), NOT the surface member vt (which would + // over-size by the inner union's own 8B tag word and + // desync the `field` slot from cstage's). Spliced + // variants carry the inner union's already-stamped + // iserror; no re-derivation. let src: *tparam = vu.params; for (src != nil) { + let st: *tinfo = src.type_; + if (st != nil) { + if (st.size > maxsz) { maxsz = st.size; }; + if (st.align > al) { al = st.align; }; + }; let tp: *tparam = alloc(tparam{name="", type_=src.type_, iserror=src.iserror, tnext=nil})!; if (head == nil) { head = tp; } else { tail.tnext = tp; }; tail = tp; src = src.tnext; }; } else { + if (vt != nil) { + if (vt.size > maxsz) { maxsz = vt.size; }; + if (vt.align > al) { al = vt.align; }; + }; let ve: bool = varianterr(c, v); let tp: *tparam = alloc(tparam{name="", type_=vt, iserror=ve, tnext=nil})!; if (head == nil) { head = tp; } else { tail.tnext = tp; }; @@ -3397,9 +3408,30 @@ fn errmatchvariant(c: *checker, n: *node, vname: *node) void = { // casevariantin — true iff `pat` (a `case T` pattern, including // each alt of a multi-pattern) names a variant of the tagged // union `tagged`. -fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = { +// +// #209: a `...inner` spread variant (v.op == TK_ELLIPSIS, parse.ww:284) +// is NOT a variant in itself — its inner tagged union's MEMBERS are. The +// AST `tagged.list` keeps the spread unexpanded (only the #61a tinfo +// build flattens it), so recurse into the inner union's members, +// attributing them the inner union's defining module. Mirrors cstage's +// resolve_type flatten (cmd/wcc/check.c:651-674) at the AST layer; the +// cgen side already dispatches off the flattened tinfo.params (#61a). +fn casevariantin(c: *checker, tagged: *node, pat: *node, unionmod: str) bool = { let v: *node = tagged.list; for (v != nil) { + if (v.op == tkind.TK_ELLIPSIS) { + let inner: *node = resolvealias(c, unwrapbang(v)); + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + if (casevariantin(c, inner, pat, + taggeddefmod(c, v))) { + return true; + }; + v = v.next; + continue; + }; + }; + }; if (typeeqast(v, pat)) { return true; }; if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; @@ -3438,12 +3470,12 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let cs0: *node = n.list; for (cs0 != nil) { if (cs0.lhs != nil) { - if (!casevariantin(u, cs0.lhs, unionmod)) { + if (!casevariantin(c, u, cs0.lhs, unionmod)) { errbadcase(c, cs0.lhs); }; let alt: *node = cs0.list; for (alt != nil) { - if (!casevariantin(u, alt, unionmod)) { + if (!casevariantin(c, u, alt, unionmod)) { errbadcase(c, alt); }; alt = alt.next; @@ -3460,21 +3492,46 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { // For each variant of u, look for a covering case. let v: *node = u.list; for (v != nil) { - let covered: bool = false; - let cs2: *node = n.list; - for (cs2 != nil) { - if (casecovers(c, cs2, v, unionmod)) { - covered = true; - cs2 = nil; - } else { - cs2 = cs2.next; - }; - }; - if (!covered) { errmatchvariant(c, n, v); }; + checkvariantcovered(c, n, v, unionmod); v = v.next; }; }; +// checkvariantcovered — emit "variant not handled" unless some case arm +// covers `v`. #209: a `...inner` spread variant expands to its inner +// union's members (each attributed the inner union's defining module), +// so the phantom spread node is never itself reported uncovered — its +// members are checked instead. Mirrors the casevariantin spread recursion +// + cstage's flattened u->params exhaustiveness walk (cmd/wcc/check.c: +// 1648-1669). Leaf variants keep their NODE so errmatchvariant names them. +fn checkvariantcovered(c: *checker, n: *node, v: *node, unionmod: str) void = { + if (v.op == tkind.TK_ELLIPSIS) { + let inner: *node = resolvealias(c, unwrapbang(v)); + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let im: str = taggeddefmod(c, v); + let m: *node = inner.list; + for (m != nil) { + checkvariantcovered(c, n, m, im); + m = m.next; + }; + return; + }; + }; + }; + let covered: bool = false; + let cs2: *node = n.list; + for (cs2 != nil) { + if (casecovers(c, cs2, v, unionmod)) { + covered = true; + cs2 = nil; + } else { + cs2 = cs2.next; + }; + }; + if (!covered) { errmatchvariant(c, n, v); }; +}; + // ---- let init / return assignability -------------------------------- // // AST-level approximation: when we can infer src's type and dst is @@ -3715,7 +3772,7 @@ fn checkisas(c: *checker, n: *node) void = { if (utinfo != nil) { if (wanttinfo != nil) { if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; }; }; - if (casevariantin(u, want, taggeddefmod(c, st))) { return; }; + if (casevariantin(c, u, want, taggeddefmod(c, st))) { return; }; os.write(2, "is/as: not a variant of operand".ptr, 31u64); if (want.kind == nkind.N_TNAME) { os.write(2, " (".ptr, 2u64); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 50ea168c..220e2871 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11943,10 +11943,6 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { let v: *node = n.list; for (v != nil) { let vt: *tinfo = tinfofornode(c, v); - if (vt != nil) { - if (vt.size > maxsz) { maxsz = vt.size; }; - if (vt.align > al) { al = vt.align; }; - }; let isspread: bool = (v.op == tkind.TK_ELLIPSIS); let vu: *tinfo = vt; if (isspread) { @@ -11955,16 +11951,31 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { }; }; if (isspread && vu != nil && vu.kind == tykind.TY_TAGGED) { - // Spliced variants carry the inner union's - // already-stamped iserror; no re-derivation. + // #209: a `...inner` spread's PAYLOAD is its + // members, not the whole inner union — size/align + // off each spliced member (cstage check.c:660-667 + // st->size), NOT the surface member vt (which would + // over-size by the inner union's own 8B tag word and + // desync the `field` slot from cstage's). Spliced + // variants carry the inner union's already-stamped + // iserror; no re-derivation. let src: *tparam = vu.params; for (src != nil) { + let st: *tinfo = src.type_; + if (st != nil) { + if (st.size > maxsz) { maxsz = st.size; }; + if (st.align > al) { al = st.align; }; + }; let tp: *tparam = alloc(tparam{name="", type_=src.type_, iserror=src.iserror, tnext=nil})!; if (head == nil) { head = tp; } else { tail.tnext = tp; }; tail = tp; src = src.tnext; }; } else { + if (vt != nil) { + if (vt.size > maxsz) { maxsz = vt.size; }; + if (vt.align > al) { al = vt.align; }; + }; let ve: bool = varianterr(c, v); let tp: *tparam = alloc(tparam{name="", type_=vt, iserror=ve, tnext=nil})!; if (head == nil) { head = tp; } else { tail.tnext = tp; }; @@ -13509,9 +13520,30 @@ fn errmatchvariant(c: *checker, n: *node, vname: *node) void = { // casevariantin — true iff `pat` (a `case T` pattern, including // each alt of a multi-pattern) names a variant of the tagged // union `tagged`. -fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = { +// +// #209: a `...inner` spread variant (v.op == TK_ELLIPSIS, parse.ww:284) +// is NOT a variant in itself — its inner tagged union's MEMBERS are. The +// AST `tagged.list` keeps the spread unexpanded (only the #61a tinfo +// build flattens it), so recurse into the inner union's members, +// attributing them the inner union's defining module. Mirrors cstage's +// resolve_type flatten (cmd/wcc/check.c:651-674) at the AST layer; the +// cgen side already dispatches off the flattened tinfo.params (#61a). +fn casevariantin(c: *checker, tagged: *node, pat: *node, unionmod: str) bool = { let v: *node = tagged.list; for (v != nil) { + if (v.op == tkind.TK_ELLIPSIS) { + let inner: *node = resolvealias(c, unwrapbang(v)); + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + if (casevariantin(c, inner, pat, + taggeddefmod(c, v))) { + return true; + }; + v = v.next; + continue; + }; + }; + }; if (typeeqast(v, pat)) { return true; }; if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; @@ -13550,12 +13582,12 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let cs0: *node = n.list; for (cs0 != nil) { if (cs0.lhs != nil) { - if (!casevariantin(u, cs0.lhs, unionmod)) { + if (!casevariantin(c, u, cs0.lhs, unionmod)) { errbadcase(c, cs0.lhs); }; let alt: *node = cs0.list; for (alt != nil) { - if (!casevariantin(u, alt, unionmod)) { + if (!casevariantin(c, u, alt, unionmod)) { errbadcase(c, alt); }; alt = alt.next; @@ -13572,21 +13604,46 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { // For each variant of u, look for a covering case. let v: *node = u.list; for (v != nil) { - let covered: bool = false; - let cs2: *node = n.list; - for (cs2 != nil) { - if (casecovers(c, cs2, v, unionmod)) { - covered = true; - cs2 = nil; - } else { - cs2 = cs2.next; - }; - }; - if (!covered) { errmatchvariant(c, n, v); }; + checkvariantcovered(c, n, v, unionmod); v = v.next; }; }; +// checkvariantcovered — emit "variant not handled" unless some case arm +// covers `v`. #209: a `...inner` spread variant expands to its inner +// union's members (each attributed the inner union's defining module), +// so the phantom spread node is never itself reported uncovered — its +// members are checked instead. Mirrors the casevariantin spread recursion +// + cstage's flattened u->params exhaustiveness walk (cmd/wcc/check.c: +// 1648-1669). Leaf variants keep their NODE so errmatchvariant names them. +fn checkvariantcovered(c: *checker, n: *node, v: *node, unionmod: str) void = { + if (v.op == tkind.TK_ELLIPSIS) { + let inner: *node = resolvealias(c, unwrapbang(v)); + if (inner != nil) { + if (inner.kind == nkind.N_TTAGGED) { + let im: str = taggeddefmod(c, v); + let m: *node = inner.list; + for (m != nil) { + checkvariantcovered(c, n, m, im); + m = m.next; + }; + return; + }; + }; + }; + let covered: bool = false; + let cs2: *node = n.list; + for (cs2 != nil) { + if (casecovers(c, cs2, v, unionmod)) { + covered = true; + cs2 = nil; + } else { + cs2 = cs2.next; + }; + }; + if (!covered) { errmatchvariant(c, n, v); }; +}; + // ---- let init / return assignability -------------------------------- // // AST-level approximation: when we can infer src's type and dst is @@ -13827,7 +13884,7 @@ fn checkisas(c: *checker, n: *node) void = { if (utinfo != nil) { if (wanttinfo != nil) { if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; }; }; - if (casevariantin(u, want, taggeddefmod(c, st))) { return; }; + if (casevariantin(c, u, want, taggeddefmod(c, st))) { return; }; os.write(2, "is/as: not a variant of operand".ptr, 31u64); if (want.kind == nkind.N_TNAME) { os.write(2, " (".ptr, 2u64); diff --git a/test/wcc/777_fmt_handle_run.c b/test/wcc/777_fmt_handle_run.c index e1cb4574..652e5d84 100644 --- a/test/wcc/777_fmt_handle_run.c +++ b/test/wcc/777_fmt_handle_run.c @@ -16,18 +16,19 @@ * (the io.stream arm) and reads back via memio.string — the two-arm * coverage #5 graduated. * - * Cstage-only per row (no STAGE_WW, no byte_id) — pre-existing wwstage - * bug #209 (sibling of #190): the wwstage checker bails `case: not a - * variant of scrutinee (X)` + `match: variant not handled - * (formattable)` on fmt's match-on-formattable arms whenever a - * downstream probe `import fmt;`s the package. The bug bites identically - * — `fmt.errorln("hi")` from a probe trips the same trace — and is - * pre-existing (reproduces on master). Every fmt test is cstage-only - * (970 fmttest runs the @test fixture under cstage `ww run` only); 995 - * self-rebuild dodges it because no selfhost main pulls err.ww - * transitively, so wwstage never compiles fmt in the bootstrap. Fix = - * one-class checker repair, out of #5 commit-2 scope; byte-id graduates - * with #209 close. + * Cstage-only per row (no STAGE_WW, no byte_id). #209 (the wwstage + * checker bail `case: not a variant of scrutinee` + `match: variant not + * handled (formattable)` on fmt's spread-union match-arms) is now CLOSED + * — wwstage compiles fmt. The residual blocker is a SEPARATE pre-existing + * cgen cluster surfaced once fmt actually codegens under wwstage: + * - #226: io.read's error widening (cgwidentagremap) loses the + * io.eof/io.error NAMED tinfo identity under fmt-presence + * (flatvariantidxt → -1 → tag collapses to 0); cs!=ww whole-file, so + * byte-id fails even though io.read is unused here (it's emitted + * regardless). The #10/#218/tinfo-lossy-nominal family. + * - #227: spread-union widen + return-ABI, broken in BOTH stages. + * 995 self-rebuild dodges all of this because no selfhost main pulls fmt + * transitively. Byte-id graduates when #226 (+#227) land. * * row | what it pins * ---------------------+-------------------------------------- @@ -293,8 +294,8 @@ run_row(const char *driver, const char *cwd, const struct row *r, int seq) /* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so * ww_ww writing intermediates next to the source doesn't clobber the * cstage .s (CLAUDE.md rule 14 phase split). Mirror of 776's. Unused - * while every row is cstage-only (#209), kept for the byte-id - * graduation once #209 closes. */ + * while every row is cstage-only (#226/#227; #209 is closed), kept for + * the byte-id graduation once those cgen folds land. */ static int asm_byte_identical(const char *cdrv, const char *wdrv, const char *cwd, const struct row *r, int seq) @@ -356,7 +357,7 @@ main(void) int total = 0, fail = 0; int wwpresent = (access(wdrv, X_OK) == 0); int seq = 0; - (void)asm_byte_identical; /* cstage-only until #209 closes */ + (void)asm_byte_identical; /* cstage-only until #226/#227 land */ for (int i = 0; i < n; i++) { if (rows[i].stage_mask & STAGE_CS) { diff --git a/test/wcc/778_bufio_vstream_run.c b/test/wcc/778_bufio_vstream_run.c index 811b6d13..a5cbd677 100644 --- a/test/wcc/778_bufio_vstream_run.c +++ b/test/wcc/778_bufio_vstream_run.c @@ -15,9 +15,12 @@ * scanline / finish. * * Each row imports os + bufio + memio + io (NOT fmt — bufio doesn't - * transitively pull fmt, so #209's formattable-match bail doesn't - * propagate; both stages run on every row, cs.s==ww.s — bufio is NOT - * compiler-embedded, so these byte-id rows are its ONLY byte-id cover). + * transitively pull fmt, so it dodges the fmt-presence cgen cluster + * #226 (io.read error-remap nominal-identity) + #227 (spread-union + * widen/return-ABI) that blocks the fmt-importing tests' byte-id; both + * stages run on every row, cs.s==ww.s — bufio is NOT compiler-embedded, + * so these byte-id rows are its ONLY byte-id cover). (#209, the wwstage + * formattable match-arm CHECKER bail, is now closed.) * * row | what it pins * --------------------------+-------------------------------------- diff --git a/test/wcc/779_log_vstream_run.c b/test/wcc/779_log_vstream_run.c index 9b54c550..cd186445 100644 --- a/test/wcc/779_log_vstream_run.c +++ b/test/wcc/779_log_vstream_run.c @@ -12,18 +12,16 @@ * Each row imports os + log + memio + io. log itself imports fmt, * so the formattable / field types flow through transitively. * - * Cstage-only per row (no STAGE_WW, no byte_id) — pre-existing - * wwstage bug #209 (sibling of #190): the wwstage checker bails on - * match-arm-over-formattable when fmt is imported transitively, which - * lib/log does for the logger vtable signatures + the fmt.fprint / - * fprintf dispatch in stdprintln / stdprintfln. The bug bites the - * OLD log surface identically — `log.println("hi")` from a probe - * trips the same trace. Existing 970 logtest runs the @test fixture - * under cstage `ww run` only, so the OLD surface is fine via cstage. - * 995 self-rebuild dodges it because no selfhost cmd imports log - * transitively. Fix-of-the-bug = one-class checker repair, out-of- - * scope for the additive fold-e5; closes the wwstage half here when - * it lands. Byte-id graduates with #209 close. + * Cstage-only per row (no STAGE_WW, no byte_id). #209 (the wwstage + * checker bail on match-arm-over-formattable when fmt is imported + * transitively — which lib/log does for the logger vtable signatures + + * the fmt.fprint / fprintf dispatch in stdprintln / stdprintfln) is now + * CLOSED; wwstage compiles fmt/log. The residual blocker is the + * pre-existing cgen cluster #226 (io.read error-remap nominal-identity) + * + #227 (spread-union widen/return-ABI), surfaced once fmt codegens + * under wwstage. Existing 971 logtest runs the @test fixture under + * cstage `ww run` only. 995 self-rebuild dodges it because no selfhost + * cmd imports log transitively. Byte-id graduates when #226 (+#227) land. * * row | what it pins * --------------------------+-------------------------------------- @@ -70,8 +68,9 @@ * using `os.trywrite(...)?`; same shape memio.ww + fmt.ww + * bufio.ww adopt. * - * - #209 (wwstage formattable match-arm bail): every row is - * STAGE_CS-only; byte-id deferred until #209 lands. + * - #226 (io.read error-remap nominal-identity) + #227 (spread-union + * widen/return-ABI): every row is STAGE_CS-only; byte-id deferred + * until they land. #209 (the checker bail) is already closed. * * BOOTSTRAP-EMBED CHECK: log is NOT embedded in any selfhost * combined.ww (grep `package log\|import log` returns empty), so the @@ -251,8 +250,9 @@ run_row(const char *driver, const char *cwd, const struct row *r, int seq) /* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so * ww_ww writing intermediates next to the source doesn't clobber the * cstage .s (CLAUDE.md rule 14 phase split). Mirror of 778's. Unused - * for fold-e5 — every row is STAGE_CS-only per #209 — but kept - * scaffolded for when #209 closes and byte-id graduates. */ + * for fold-e5 — every row is STAGE_CS-only per #226/#227 (#209, the + * checker bail, is closed) — but kept scaffolded for when those cgen + * folds land and byte-id graduates. */ static int asm_byte_identical(const char *cdrv, const char *wdrv, const char *cwd, const struct row *r, int seq) diff --git a/test/wcc/780_fmt_mods_run.c b/test/wcc/780_fmt_mods_run.c index a95ce070..101b89ef 100644 --- a/test/wcc/780_fmt_mods_run.c +++ b/test/wcc/780_fmt_mods_run.c @@ -11,11 +11,12 @@ * calls fprintf with a {:mods} format string + one arg, and asserts the * exact byte content read back. * - * Cstage-only per row — pre-existing wwstage bug #209 (sibling of - * #190): the wwstage checker bails on fmt.formattable match-arms - * whenever a downstream probe `import fmt;`s the package. Identical - * carve-out to 777_fmt_handle_run (cited there). Byte-id graduates with - * #209 close. + * Cstage-only per row. #209 (the wwstage checker bail on fmt's + * spread-union match-arms) is now CLOSED; the residual blocker is the + * pre-existing cgen cluster #226 (io.read error-remap nominal-identity) + * + #227 (spread-union widen/return-ABI). Identical carve-out to + * 777_fmt_handle_run (cited there). Byte-id graduates when #226 (+#227) + * land. * * row | format | arg | expect | exit * -----------------+------------+--------------+-----------+----- @@ -44,8 +45,10 @@ * * DEFERRALS / RELATED (NOT addressed by these rows): * - * - #209 (wwstage formattable match-arm bail): blocks STAGE_WW here, - * same as 777_fmt_handle_run. Cstage-only until close. + * - #226 (io.read error-remap nominal-identity) + #227 (spread-union + * widen/return-ABI): block STAGE_WW here, same as 777_fmt_handle_run. + * #209 (the checker bail) is closed; these cgen folds are the + * residual. Cstage-only until they land. * * BOOTSTRAP-EMBED CHECK: fmt is NOT embedded in any selfhost * combined.ww (grep confirmed), so the #5 fmt graduation does NOT diff --git a/test/wcc/781_fmt_compositions_run.c b/test/wcc/781_fmt_compositions_run.c index a897b53e..10341443 100644 --- a/test/wcc/781_fmt_compositions_run.c +++ b/test/wcc/781_fmt_compositions_run.c @@ -33,13 +33,13 @@ * | io.close. Asserts the owned str bytes, then * | frees via os.free. * - * Cstage-only per row (no STAGE_WW, no byte_id) — pre-existing wwstage - * bug #209 (sibling of #190): the wwstage checker bails `case: not a - * variant of scrutinee (X)` + `match: variant not handled - * (formattable)` on fmt.formattable match-arms whenever a downstream - * probe `import fmt;`s the package. Identical carve-out to - * 777_fmt_handle_run + 780_fmt_mods_run. Byte-id graduates with #209 - * close. + * Cstage-only per row (no STAGE_WW, no byte_id). #209 (the wwstage + * checker bail `case: not a variant of scrutinee` + `match: variant not + * handled (formattable)` on fmt's spread-union match-arms) is now CLOSED; + * wwstage compiles fmt. The residual blocker is the pre-existing cgen + * cluster #226 (io.read error-remap nominal-identity) + #227 (spread-union + * widen/return-ABI). Identical carve-out to 777_fmt_handle_run + + * 780_fmt_mods_run. Byte-id graduates when #226 (+#227) land. * * IMPORT-ORDER WORKAROUND (sibling task #208, pre-existing): every row * places `import os;` FIRST for the same reason 776/777/780 document @@ -48,8 +48,10 @@ * * DEFERRALS / RELATED (NOT addressed by these rows): * - * - #209 (wwstage formattable match-arm bail): blocks STAGE_WW here, - * same as 777/780. Cstage-only until close. + * - #226 (io.read error-remap nominal-identity) + #227 (spread-union + * widen/return-ABI): block STAGE_WW here, same as 777/780. #209 (the + * checker bail) is closed; these cgen folds are the residual. + * Cstage-only until they land. * - #173 (TRY-on-tagged-return both-stages broken): bsprintf widens * nomem into io.error inline rather than `memio.fixed(buf)?` for * the same reason memio.ww does. diff --git a/test/wcc/792_spread_variant_match.c b/test/wcc/792_spread_variant_match.c new file mode 100644 index 00000000..37116121 --- /dev/null +++ b/test/wcc/792_spread_variant_match.c @@ -0,0 +1,163 @@ +/* + * 792_spread_variant_match — project #209 close. Pins that the wwstage + * checker accepts a `match` over a tagged union with a `...inner` SPREAD + * variant, byte-identically with cstage (rule-10). + * + * THE BUG (wwstage-CHECKER-only, cs!=ww): a spread variant + * `type field = (...inner | str)` flattens its inner union's members + * into `field` (i64|bool|rune|str). cstage's resolve_type performs the + * flatten at type-build (cmd/wcc/check.c:651-674), so its match-arm + * validity + exhaustiveness see the flattened members. wwstage's checker + * (selfhost/cmd/wcc/check.ww checkmatchexhaust) walked the RAW AST + * `u.list`, which keeps the spread unexpanded — every member arm tripped + * "case: not a variant of scrutinee" and the phantom spread node tripped + * "match: variant not handled". So any module that matches over a spread + * union (e.g. fmt's `field = (...formattable | *mods)`) was + * wwstage-uncompilable. cstage compiled it fine. Hare flattens spreads; + * align UP to cstage (a too-strict checker, NOT a down-align). + * + * THE FIX (#209): casevariantin + a checkvariantcovered coverage helper + * recurse into a spread variant's inner-union members (resolvealias → + * N_TTAGGED → walk members), mirroring cstage's flatten at the AST + * layer; the #61a tinfo build additionally sizes the union off the + * flattened MEMBERS (not the whole inner union) so the slot matches + * cstage's. cgen already dispatches off the flattened tinfo.params + * (flatvariantidxt), so no cgen change is needed — checker-only. + * + * The PRE-FIX failure mode is a wwstage CHECKER REJECT, so the + * discriminator is `w6c_ww` on the driver-produced combined.ww + * succeeding AT ALL — pre-fix it errored out; post-fix it succeeds AND + * is byte-id with cstage's w6c. The probe stays on the param+match shape + * (a fn taking the spread union and matching it, exhaustive, no + * default): that is exactly what #209's checker fix covers and is + * byte-id. (Spread-union CONSTRUCTION/return-ABI is a separate, + * pre-existing both-stage cgen path — out of #209 scope — so it is + * deliberately NOT exercised here.) Self-contained single-file probe. + */ +#define _GNU_SOURCE +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +static const char SRC[] = + "package main;\n" + "\n" + "type inner = (i64 | bool | rune);\n" + "type field = (...inner | str);\n" + "\n" + "fn classify(f: field) i64 = {\n" + " match (f) {\n" + " case let n: i64 => return n;\n" + " case let b: bool => return 1;\n" + " case let r: rune => return 2;\n" + " case let s: str => return (s.len: i64);\n" + " };\n" + "};\n" + "\n" + "export fn main() i32 = { return 0; };\n"; + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2048]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[2100], w6c_ww[2100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "792: w6c_ww missing — cannot run the cs==ww " + "byte-id gate (the whole point of this test)\n"); + return 1; + } + + char dir[] = "/tmp/ww792_XXXXXX"; + if (mkdtemp(dir) == NULL) { fprintf(stderr, "792: mkdtemp\n"); return 1; } + + char path[1024], cmd[4096], comb[1024], cs_s[1024], ws_s[1024]; + int rc = 0; + + snprintf(path, sizeof path, "%s/main.ww", dir); + FILE *f = fopen(path, "wb"); + if (!f) { fprintf(stderr, "792: write main.ww\n"); rc = 1; goto done; } + fputs(SRC, f); + fclose(f); + + snprintf(comb, sizeof comb, "%s/main.combined.ww", dir); + + /* cstage driver build: produces the combined.ww + must accept. */ + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build -I %s %s/main.ww", + dir, bin, dir, dir); + if (runwait(cmd) != 0) { + fprintf(stderr, "792: cstage build failed\n"); + rc = 1; goto done; + } + if (access(comb, 0) != 0) { + fprintf(stderr, "792: no combined.ww produced\n"); + rc = 1; goto done; + } + + /* The #209 discriminator: raw w6c_ww on the combined. Pre-fix the + * wwstage checker REJECTED the spread-union match arms (non-zero + * exit). Post-fix it accepts AND is byte-id with cstage's w6c. */ + snprintf(cs_s, sizeof cs_s, "%s/cs.s", dir); + snprintf(ws_s, sizeof ws_s, "%s/ww.s", dir); + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, comb); + if (runwait(cmd) != 0) { + fprintf(stderr, "792: w6c on combined failed\n"); + rc = 1; goto done; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, comb); + if (runwait(cmd) != 0) { + fprintf(stderr, "792: w6c_ww on combined failed " + "(#209 spread-union match reject?)\n"); + rc = 1; goto done; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "792: cs.s/ww.s DIFFER (rule-10 byte-id " + "violation)\n"); + rc = 1; goto done; + } + + printf("spread_variant_match: ok (w6c_ww accepts spread match + " + "cs==ww byte-id)\n"); + +done: + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + (void)runwait(cmd); + return rc; +}