wwstage: expand spread variants in match check + size them off flattened members (#209)

The wwstage checker walked a match's raw AST variant list and never expanded a ...inner spread variant, so it rejected fmt's match over field = (...formattable | *mods) ('not a variant of scrutinee'). cstage's resolve_type flattens the spread at type-build. Mirror that in the AST exhaustiveness walk (casevariantin + a recursive checkvariantcovered): when a variant resolves to N_TTAGGED via a spread, recurse into its members. Additive + spread-gated -- typeeqast / casevariantpairmatch (#13) / casecovers untouched, so non-spread matches and 990-997 byte-id are unaffected. Also size a spread N_TTAGGED off each flattened member (mirror cstage check.c), dropping the inner union tag word (field 40B to 32B). Closes the #209 CHECKER reject; full fmt-byte-id still awaits cgen cluster #226 (io.read nominal-remap) + #227 (spread-widen ABI), so fmt tests stay cstage-only with retargeted comments. Adds test 792; regenerates w6c/wwdump combined.ww.
This commit is contained in:
2026-05-31 20:31:40 +09:00
parent 497f1fa0d3
commit 3d44f742a0
10 changed files with 471 additions and 113 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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
* --------------------------+--------------------------------------

View File

@@ -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)

View File

@@ -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

View File

@@ -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.

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
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;
}