diff --git a/Makefile b/Makefile index 1e144c5a..6c17b6ff 100644 --- a/Makefile +++ b/Makefile @@ -336,6 +336,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_fieldfn_leaf_collide_run \ $(BIN)/test_amp_fn_assign_run \ $(BIN)/test_xmod_alias_struct_collide_run \ + $(BIN)/test_xmod_variant_match \ $(BIN)/test_structvariant_largeunion_return \ $(BIN)/test_narrow_alias_deref_store \ $(BIN)/test_bufio_vstream_run \ @@ -743,6 +744,17 @@ $(BIN)/test_xmod_alias_struct_collide_run: test/wcc/784_xmod_alias_struct_collid $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #13: cross-module decomposition of an imported union's variants +# (`case pkg.a`). Pre-fix the wwstage checker rejected it; cstage built +# it. cstage driver build + run pins routing; raw w6c_ww on the +# combined.ww (must now accept) + cs==ww byte-id is the discriminator. +# Builds its own 2-module fixtures in a private mktemp dir. +$(BIN)/test_xmod_variant_match: test/wcc/787_xmod_variant_match.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #9: returning a STRUCT variant of a LARGE (>4-eightbyte) tagged union. # Both-stage byte-id + runtime (reads tag AND the widened &fn field, so a # dropped-store regression can't hide behind self-consistent byte-id). diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e3eaee42..f8fe69f0 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13389,13 +13389,94 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // tagged-union type is handled by some case (or a default arm // exists). Multi-pattern `case A | B =>` covers all alts. -fn casecovers(c: *checker, cs: *node, want: *node) bool = { +// qualleaf — rightmost dotted segment of a (possibly module-qualified) +// type name; the whole name when unqualified. +fn qualleaf(nm: str) str = { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < nm.len) { + if (nm[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx < 0) { return nm; }; + let leaf: str; + leaf.ptr = nm.ptr + ((dotidx + 1): u64); + leaf.len = nm.len - dotidx - 1; + return leaf; +}; + +// qualmod — module qualifier of a type name (segment before the +// rightmost '.'), or `defmod` when unqualified. +fn qualmod(nm: str, defmod: str) str = { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < nm.len) { + if (nm[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx < 0) { return defmod; }; + let head: str; + head.ptr = nm.ptr; + head.len = dotidx; + return head; +}; + +// casevariantpairmatch — a case pattern names variant `v` of the tagged +// union iff their (module, leaf) PAIRS match. Each name reduces to +// (qualmod, qualleaf): a DOTTED name keeps its own qualifier; a BARE +// name is attributed `unionmod`, the union's defining module. So a +// cross-module `case errors.unsupported` matches the bare `unsupported` +// in errors.error's body, while a foreign `othermod.unsupported` is +// REJECTED (qualifier differs) and Shape A's dotted `errors.error` +// variant of io.error keeps matching `case errors.error` (defmod is +// NOT forced onto a dotted variant — drew #13). Approximates harec's +// resolved-Type variant identity (cmd/wcc/check.c:1651) at the AST +// level via the existing nmod/aliassym machinery (cf. #51/#53); the +// precise Type-identity form is #10 (tinfo SSoT). typeeqast handles the +// exact-string cases first, so this fires only on the qualified-vs-bare +// mix. +fn casevariantpairmatch(v: *node, pat: *node, unionmod: str) bool = { + let vv: *node = unwrapbang(v); + let pp: *node = unwrapbang(pat); + if (vv == nil) { return false; }; + if (pp == nil) { return false; }; + if (vv.kind != nkind.N_TNAME) { return false; }; + if (pp.kind != nkind.N_TNAME) { return false; }; + if (!streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; }; + return streq(qualmod(vv.str, unionmod), qualmod(pp.str, unionmod)); +}; + +// taggeddefmod — defining module of the typedecl whose body IS the +// tagged union (follows alias hops via aliassym, the #51/#53 machinery). +// Bare variants in that body are defined here: io.error = +// !(errors.error | underread | nomem) (module io) -> "io"; errors.error +// -> "errors". Falls back to c.curmod when the chain can't resolve. +fn taggeddefmod(c: *checker, st: *node) str = { + let defmod: str = c.curmod; + let cur: *node = unwrapbang(st); + for (cur != nil) { + if (cur.kind != nkind.N_TNAME) { return defmod; }; + let s: *sym = aliassym(c, cur); + if (s == nil) { return defmod; }; + if (s.decl == nil) { return defmod; }; + if (s.decl.nmod.len > 0) { defmod = s.decl.nmod; }; + let body: *node = unwrapbang(s.decl.lhs); + if (body == nil) { return defmod; }; + if (body.kind == nkind.N_TTAGGED) { return defmod; }; + cur = body; + }; + return defmod; +}; + +fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = { if (cs.lhs != nil) { if (typeeqast(cs.lhs, want)) { return true; }; + if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; }; }; let alt: *node = cs.list; for (alt != nil) { if (typeeqast(alt, want)) { return true; }; + if (casevariantpairmatch(want, alt, unionmod)) { return true; }; alt = alt.next; }; return false; @@ -13417,10 +13498,11 @@ 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) bool = { +fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = { let v: *node = tagged.list; for (v != nil) { if (typeeqast(v, pat)) { return true; }; + if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; }; return false; @@ -13446,18 +13528,23 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; if (u.kind != nkind.N_TTAGGED) { return; }; + // #13: the union's defining module, so a bare body variant can be + // matched against a module-qualified cross-module case pattern (and + // a foreign-qualifier pattern correctly rejected). See + // casevariantpairmatch. + let unionmod: str = taggeddefmod(c, st); // Validity: every `case T` pattern (and multi-pattern alts) // must name a variant of u. Catches typos and dead arms that // the dispatch would never reach. let cs0: *node = n.list; for (cs0 != nil) { if (cs0.lhs != nil) { - if (!casevariantin(u, cs0.lhs)) { + if (!casevariantin(u, cs0.lhs, unionmod)) { errbadcase(c, cs0.lhs); }; let alt: *node = cs0.list; for (alt != nil) { - if (!casevariantin(u, alt)) { + if (!casevariantin(u, alt, unionmod)) { errbadcase(c, alt); }; alt = alt.next; @@ -13477,7 +13564,7 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let covered: bool = false; let cs2: *node = n.list; for (cs2 != nil) { - if (casecovers(c, cs2, v)) { + if (casecovers(c, cs2, v, unionmod)) { covered = true; cs2 = nil; } else { @@ -13729,7 +13816,7 @@ fn checkisas(c: *checker, n: *node) void = { if (utinfo != nil) { if (wanttinfo != nil) { if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; }; }; - if (casevariantin(u, want)) { return; }; + if (casevariantin(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 a10f64b3..0c7c3948 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -3288,13 +3288,94 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // tagged-union type is handled by some case (or a default arm // exists). Multi-pattern `case A | B =>` covers all alts. -fn casecovers(c: *checker, cs: *node, want: *node) bool = { +// qualleaf — rightmost dotted segment of a (possibly module-qualified) +// type name; the whole name when unqualified. +fn qualleaf(nm: str) str = { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < nm.len) { + if (nm[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx < 0) { return nm; }; + let leaf: str; + leaf.ptr = nm.ptr + ((dotidx + 1): u64); + leaf.len = nm.len - dotidx - 1; + return leaf; +}; + +// qualmod — module qualifier of a type name (segment before the +// rightmost '.'), or `defmod` when unqualified. +fn qualmod(nm: str, defmod: str) str = { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < nm.len) { + if (nm[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx < 0) { return defmod; }; + let head: str; + head.ptr = nm.ptr; + head.len = dotidx; + return head; +}; + +// casevariantpairmatch — a case pattern names variant `v` of the tagged +// union iff their (module, leaf) PAIRS match. Each name reduces to +// (qualmod, qualleaf): a DOTTED name keeps its own qualifier; a BARE +// name is attributed `unionmod`, the union's defining module. So a +// cross-module `case errors.unsupported` matches the bare `unsupported` +// in errors.error's body, while a foreign `othermod.unsupported` is +// REJECTED (qualifier differs) and Shape A's dotted `errors.error` +// variant of io.error keeps matching `case errors.error` (defmod is +// NOT forced onto a dotted variant — drew #13). Approximates harec's +// resolved-Type variant identity (cmd/wcc/check.c:1651) at the AST +// level via the existing nmod/aliassym machinery (cf. #51/#53); the +// precise Type-identity form is #10 (tinfo SSoT). typeeqast handles the +// exact-string cases first, so this fires only on the qualified-vs-bare +// mix. +fn casevariantpairmatch(v: *node, pat: *node, unionmod: str) bool = { + let vv: *node = unwrapbang(v); + let pp: *node = unwrapbang(pat); + if (vv == nil) { return false; }; + if (pp == nil) { return false; }; + if (vv.kind != nkind.N_TNAME) { return false; }; + if (pp.kind != nkind.N_TNAME) { return false; }; + if (!streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; }; + return streq(qualmod(vv.str, unionmod), qualmod(pp.str, unionmod)); +}; + +// taggeddefmod — defining module of the typedecl whose body IS the +// tagged union (follows alias hops via aliassym, the #51/#53 machinery). +// Bare variants in that body are defined here: io.error = +// !(errors.error | underread | nomem) (module io) -> "io"; errors.error +// -> "errors". Falls back to c.curmod when the chain can't resolve. +fn taggeddefmod(c: *checker, st: *node) str = { + let defmod: str = c.curmod; + let cur: *node = unwrapbang(st); + for (cur != nil) { + if (cur.kind != nkind.N_TNAME) { return defmod; }; + let s: *sym = aliassym(c, cur); + if (s == nil) { return defmod; }; + if (s.decl == nil) { return defmod; }; + if (s.decl.nmod.len > 0) { defmod = s.decl.nmod; }; + let body: *node = unwrapbang(s.decl.lhs); + if (body == nil) { return defmod; }; + if (body.kind == nkind.N_TTAGGED) { return defmod; }; + cur = body; + }; + return defmod; +}; + +fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = { if (cs.lhs != nil) { if (typeeqast(cs.lhs, want)) { return true; }; + if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; }; }; let alt: *node = cs.list; for (alt != nil) { if (typeeqast(alt, want)) { return true; }; + if (casevariantpairmatch(want, alt, unionmod)) { return true; }; alt = alt.next; }; return false; @@ -3316,10 +3397,11 @@ 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) bool = { +fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = { let v: *node = tagged.list; for (v != nil) { if (typeeqast(v, pat)) { return true; }; + if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; }; return false; @@ -3345,18 +3427,23 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; if (u.kind != nkind.N_TTAGGED) { return; }; + // #13: the union's defining module, so a bare body variant can be + // matched against a module-qualified cross-module case pattern (and + // a foreign-qualifier pattern correctly rejected). See + // casevariantpairmatch. + let unionmod: str = taggeddefmod(c, st); // Validity: every `case T` pattern (and multi-pattern alts) // must name a variant of u. Catches typos and dead arms that // the dispatch would never reach. let cs0: *node = n.list; for (cs0 != nil) { if (cs0.lhs != nil) { - if (!casevariantin(u, cs0.lhs)) { + if (!casevariantin(u, cs0.lhs, unionmod)) { errbadcase(c, cs0.lhs); }; let alt: *node = cs0.list; for (alt != nil) { - if (!casevariantin(u, alt)) { + if (!casevariantin(u, alt, unionmod)) { errbadcase(c, alt); }; alt = alt.next; @@ -3376,7 +3463,7 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let covered: bool = false; let cs2: *node = n.list; for (cs2 != nil) { - if (casecovers(c, cs2, v)) { + if (casecovers(c, cs2, v, unionmod)) { covered = true; cs2 = nil; } else { @@ -3628,7 +3715,7 @@ fn checkisas(c: *checker, n: *node) void = { if (utinfo != nil) { if (wanttinfo != nil) { if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; }; }; - if (casevariantin(u, want)) { return; }; + if (casevariantin(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 243bdf3a..e4612eab 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13389,13 +13389,94 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // tagged-union type is handled by some case (or a default arm // exists). Multi-pattern `case A | B =>` covers all alts. -fn casecovers(c: *checker, cs: *node, want: *node) bool = { +// qualleaf — rightmost dotted segment of a (possibly module-qualified) +// type name; the whole name when unqualified. +fn qualleaf(nm: str) str = { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < nm.len) { + if (nm[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx < 0) { return nm; }; + let leaf: str; + leaf.ptr = nm.ptr + ((dotidx + 1): u64); + leaf.len = nm.len - dotidx - 1; + return leaf; +}; + +// qualmod — module qualifier of a type name (segment before the +// rightmost '.'), or `defmod` when unqualified. +fn qualmod(nm: str, defmod: str) str = { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < nm.len) { + if (nm[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx < 0) { return defmod; }; + let head: str; + head.ptr = nm.ptr; + head.len = dotidx; + return head; +}; + +// casevariantpairmatch — a case pattern names variant `v` of the tagged +// union iff their (module, leaf) PAIRS match. Each name reduces to +// (qualmod, qualleaf): a DOTTED name keeps its own qualifier; a BARE +// name is attributed `unionmod`, the union's defining module. So a +// cross-module `case errors.unsupported` matches the bare `unsupported` +// in errors.error's body, while a foreign `othermod.unsupported` is +// REJECTED (qualifier differs) and Shape A's dotted `errors.error` +// variant of io.error keeps matching `case errors.error` (defmod is +// NOT forced onto a dotted variant — drew #13). Approximates harec's +// resolved-Type variant identity (cmd/wcc/check.c:1651) at the AST +// level via the existing nmod/aliassym machinery (cf. #51/#53); the +// precise Type-identity form is #10 (tinfo SSoT). typeeqast handles the +// exact-string cases first, so this fires only on the qualified-vs-bare +// mix. +fn casevariantpairmatch(v: *node, pat: *node, unionmod: str) bool = { + let vv: *node = unwrapbang(v); + let pp: *node = unwrapbang(pat); + if (vv == nil) { return false; }; + if (pp == nil) { return false; }; + if (vv.kind != nkind.N_TNAME) { return false; }; + if (pp.kind != nkind.N_TNAME) { return false; }; + if (!streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; }; + return streq(qualmod(vv.str, unionmod), qualmod(pp.str, unionmod)); +}; + +// taggeddefmod — defining module of the typedecl whose body IS the +// tagged union (follows alias hops via aliassym, the #51/#53 machinery). +// Bare variants in that body are defined here: io.error = +// !(errors.error | underread | nomem) (module io) -> "io"; errors.error +// -> "errors". Falls back to c.curmod when the chain can't resolve. +fn taggeddefmod(c: *checker, st: *node) str = { + let defmod: str = c.curmod; + let cur: *node = unwrapbang(st); + for (cur != nil) { + if (cur.kind != nkind.N_TNAME) { return defmod; }; + let s: *sym = aliassym(c, cur); + if (s == nil) { return defmod; }; + if (s.decl == nil) { return defmod; }; + if (s.decl.nmod.len > 0) { defmod = s.decl.nmod; }; + let body: *node = unwrapbang(s.decl.lhs); + if (body == nil) { return defmod; }; + if (body.kind == nkind.N_TTAGGED) { return defmod; }; + cur = body; + }; + return defmod; +}; + +fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = { if (cs.lhs != nil) { if (typeeqast(cs.lhs, want)) { return true; }; + if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; }; }; let alt: *node = cs.list; for (alt != nil) { if (typeeqast(alt, want)) { return true; }; + if (casevariantpairmatch(want, alt, unionmod)) { return true; }; alt = alt.next; }; return false; @@ -13417,10 +13498,11 @@ 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) bool = { +fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = { let v: *node = tagged.list; for (v != nil) { if (typeeqast(v, pat)) { return true; }; + if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; }; return false; @@ -13446,18 +13528,23 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let u: *node = resolvealias(c, unwrapbang(st)); if (u == nil) { return; }; if (u.kind != nkind.N_TTAGGED) { return; }; + // #13: the union's defining module, so a bare body variant can be + // matched against a module-qualified cross-module case pattern (and + // a foreign-qualifier pattern correctly rejected). See + // casevariantpairmatch. + let unionmod: str = taggeddefmod(c, st); // Validity: every `case T` pattern (and multi-pattern alts) // must name a variant of u. Catches typos and dead arms that // the dispatch would never reach. let cs0: *node = n.list; for (cs0 != nil) { if (cs0.lhs != nil) { - if (!casevariantin(u, cs0.lhs)) { + if (!casevariantin(u, cs0.lhs, unionmod)) { errbadcase(c, cs0.lhs); }; let alt: *node = cs0.list; for (alt != nil) { - if (!casevariantin(u, alt)) { + if (!casevariantin(u, alt, unionmod)) { errbadcase(c, alt); }; alt = alt.next; @@ -13477,7 +13564,7 @@ fn checkmatchexhaust(c: *checker, n: *node) void = { let covered: bool = false; let cs2: *node = n.list; for (cs2 != nil) { - if (casecovers(c, cs2, v)) { + if (casecovers(c, cs2, v, unionmod)) { covered = true; cs2 = nil; } else { @@ -13729,7 +13816,7 @@ fn checkisas(c: *checker, n: *node) void = { if (utinfo != nil) { if (wanttinfo != nil) { if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; }; }; - if (casevariantin(u, want)) { return; }; + if (casevariantin(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/787_xmod_variant_match.c b/test/wcc/787_xmod_variant_match.c new file mode 100644 index 00000000..0fad7751 --- /dev/null +++ b/test/wcc/787_xmod_variant_match.c @@ -0,0 +1,368 @@ +/* + * 787_xmod_variant_match — project #13 close. Pins that a `package main` + * can decompose an IMPORTED union's individual VARIANTS cross-module + * (`match (e: pkg.u) { case pkg.a => ...; case let o: pkg.b => ... }`), + * on BOTH stages, byte-identically (rule-10). + * + * THE BUG (wwstage-CHECKER-only, cs!=ww): wwstage's match-arm validity + * (selfhost/cmd/wcc/check.ww casevariantin) and exhaustiveness + * (casecovers) compared via typeeqast, whose N_TNAME arm is a raw + * streq. A union's variant is written UNQUALIFIED in its defining + * module (`a` in pkg.u's body); the cross-module case pattern is the + * dotted `pkg.a` (one N_TNAME). streq("a","pkg.a") -> false -> wwstage + * rejected "case: not a variant of scrutinee". cstage compares + * resolved-Type identity (variant_match, cmd/wcc/check.c:1651), so the + * qualifier is irrelevant and it built+routed correctly. Hare allows + * cross-module variant decomposition; align UP to cstage (a too-strict + * checker, NOT a down-align that would forbid the feature). + * + * THE FIX (#13): casevariantpairmatch reduces BOTH pattern and variant + * to a (module, leaf) pair — a dotted name keeps its own qualifier, a + * bare name is attributed the union's defining module (taggeddefmod via + * aliassym .decl.nmod) — and matches the pairs. This accepts a + * cross-module `case errors.unsupported` (vs bare `unsupported`) while + * REJECTING a foreign `othermod.unsupported`, and keeps a dotted body + * variant matching its own qualifier (Shape A). Wired into BOTH + * casevariantin (validity) and casecovers (exhaustiveness). #10-family + * (AST-name vs cstage tinfo); precise Type identity is the #10 endgame. + * + * The PRE-FIX failure mode is a wwstage CHECKER REJECT, so the + * discriminator is the `w6c_ww` build of the combined.ww succeeding at + * all — pre-fix it errored out; post-fix it succeeds AND is byte-id + * with cstage. Self-contained 2-module fixtures (no lib coupling). + * + * scenario | shape | exit | byte-id + * ------------------+----------------------------------------+------+-------- + * no_default | match pkg.u=!(a|b), bare arms, NO | 21 | cs==ww + * | default — exercises casecovers | | + * | exhaustiveness cross-module | | + * bind_and_default | match pkg.u2=!(a|b|c): bare arm + a | 42 | cs==ww + * | `case let o: pkg.b` BOUND arm + a | | + * | `case =>` default; if-checks each arm | | + * foreign_qualifier | `case o.a` vs pkg.u (o.a is a DIFFERENT | both | (reject + * | module's same-leaf type) — BOTH |reject| net) + * | stages must REJECT; guards the fix | | + * | against a false-ACCEPT (bare leaf- | | + * | strip would wrongly accept) | | + * shape_a_dotted_var| union BODY holds dotted variant | 12 | cs==ww + * | pkg.e=!(errs.bad|local); case errs.bad| | + * | keeps OWN qualifier (drew Shape A) — | | + * | sole positive lock on qualmod's | | + * | dotted-variant branch | | + * + * GATE POLARITY: must stay GREEN. Red means wwstage rejected a valid + * cross-module variant match again (w6c_ww build fails) or the routing + * diverged (byte-id / runtime). + */ +#include +#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; +} + +struct file { const char *name; const char *src; }; + +struct scenario { + const char *label; + const struct file *files; /* name==NULL terminates */ + int want_exit; + int expect_reject; /* 1 = BOTH stages must REJECT (checker) */ +}; + +/* ---- no_default: bare cross-module arms, no default (exhaustiveness) */ +static const struct file no_default_files[] = { + { "pkg.ww", + "package pkg;\n" + "export type a = !void;\n" + "export type b = !void;\n" + "export type u = !(a | b);\n" + "export fn mka() u = { let x: a; return x; };\n" + "export fn mkb() u = { let y: b; return y; };\n" }, + { "main.ww", + "package main;\n" + "import pkg;\n" + "fn classify(e: pkg.u) i32 = {\n" + " match (e) {\n" + " case pkg.a => return 1;\n" + " case pkg.b => return 2;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " return classify(pkg.mkb()) * 10 + classify(pkg.mka());\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- bind_and_default: bound arm + default cross-module ----------- */ +static const struct file bind_default_files[] = { + { "pkg.ww", + "package pkg;\n" + "export type a = !void;\n" + "export type b = !void;\n" + "export type c = !void;\n" + "export type u2 = !(a | b | c);\n" + "export fn mka() u2 = { let x: a; return x; };\n" + "export fn mkb() u2 = { let y: b; return y; };\n" + "export fn mkc() u2 = { let z: c; return z; };\n" }, + { "main.ww", + "package main;\n" + "import pkg;\n" + "fn classify(e: pkg.u2) i32 = {\n" + " match (e) {\n" + " case pkg.a => return 1;\n" + " case let o: pkg.b => return 2;\n" + " case => return 9;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (classify(pkg.mka()) != 1) { return 11; };\n" + " if (classify(pkg.mkb()) != 2) { return 12; };\n" + " if (classify(pkg.mkc()) != 9) { return 13; };\n" + " return 42;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- foreign_qualifier: a same-leaf variant from a DIFFERENT module + * is NOT a variant of the scrutinee — BOTH stages must REJECT. Guards + * the #13 fix against trading the false-reject for a false-ACCEPT: a + * bare leaf-strip would wrongly accept `o.a` (leaf "a" matches pkg's + * variant `a`); the (module,leaf)-pair match rejects it (o != pkg). */ +static const struct file foreign_qualifier_files[] = { + { "pkg.ww", + "package pkg;\n" + "export type a = !void;\n" + "export type b = !void;\n" + "export type u = !(a | b);\n" + "export fn mka() u = { let x: a; return x; };\n" }, + { "o.ww", + "package o;\n" + "export type a = !void;\n" }, + { "main.ww", + "package main;\n" + "import pkg;\n" + "import o;\n" + "fn classify(e: pkg.u) i32 = {\n" + " match (e) {\n" + " case o.a => return 1;\n" + " case => return 0;\n" + " };\n" + "};\n" + "export fn main() i32 = { return classify(pkg.mka()); };\n" }, + { NULL, NULL } +}; + +/* ---- shape_a_dotted_variant: the union's BODY itself holds a dotted + * cross-module variant (`pkg.e = !(errs.bad | local)`), matched with a + * dotted `case errs.bad` AND a bare-variant `case pkg.local`. This is + * drew's Shape A (cf. io.error nesting errors.error): the dotted body + * variant `errs.bad` must keep ITS OWN qualifier (errs), NOT be forced + * onto the union's defining module (pkg) — else `case errs.bad` would + * false-REJECT. The other three scenarios carry only BARE body variants, + * so this is the sole positive lock on qualmod's dotted-variant branch; + * a regression that attributes unionmod to every variant breaks here + * alone. exit = classify(mkbad())*10 + classify(mklocal()) = 1*10+2. */ +static const struct file shape_a_files[] = { + { "errs.ww", + "package errs;\n" + "export type bad = !void;\n" }, + { "pkg.ww", + "package pkg;\n" + "import errs;\n" + "export type local = !void;\n" + "export type e = !(errs.bad | local);\n" + "export fn mkbad() e = { let x: errs.bad; return x; };\n" + "export fn mklocal() e = { let y: local; return y; };\n" }, + { "main.ww", + "package main;\n" + "import pkg;\n" + "import errs;\n" + "fn classify(x: pkg.e) i32 = {\n" + " match (x) {\n" + " case errs.bad => return 1;\n" + " case pkg.local => return 2;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " return classify(pkg.mkbad()) * 10 + classify(pkg.mklocal());\n" + "};\n" }, + { NULL, NULL } +}; + +static const struct scenario scenarios[] = { + { "no_default", no_default_files, 21, 0 }, + { "bind_and_default", bind_default_files, 42, 0 }, + { "foreign_qualifier", foreign_qualifier_files, 0, 1 }, + { "shape_a_dotted_var", shape_a_files, 12, 0 }, +}; + +static int +run_scenario(const char *bin, const char *w6c, const char *w6c_ww, + const struct scenario *sc) +{ + char dir[] = "/tmp/ww787_XXXXXX"; + if (mkdtemp(dir) == NULL) { + fprintf(stderr, "787[%s]: mkdtemp failed\n", sc->label); + return -1; + } + + char path[1024], cmd[4096]; + int rc = 0; + + for (int i = 0; sc->files[i].name; i++) { + snprintf(path, sizeof path, "%s/%s", dir, sc->files[i].name); + FILE *f = fopen(path, "wb"); + if (!f) { fprintf(stderr, "787[%s]: write %s\n", sc->label, + sc->files[i].name); rc = -1; goto done; } + fputs(sc->files[i].src, f); + fclose(f); + } + + char comb[1024]; + snprintf(comb, sizeof comb, "%s/main.combined.ww", dir); + + if (sc->expect_reject) { + /* The driver build MUST fail (cstage's checker rejects the + * foreign-qualifier variant), but expand() still writes the + * combined unit before the checker runs — so re-check it with + * BOTH backends and require BOTH to reject. w6c_ww accepting + * here is the #13 false-ACCEPT regression. */ + char xs[1024]; + snprintf(xs, sizeof xs, "%s/x.s", dir); + snprintf(cmd, sizeof cmd, + "cd %s && %s/ww build -I %s %s/main.ww >/dev/null 2>&1", + dir, bin, dir, dir); + if (runwait(cmd) == 0) { + fprintf(stderr, "787[%s]: cstage build SUCCEEDED, " + "expected reject\n", sc->label); + rc = -1; goto done; + } + if (access(comb, 0) != 0) { + fprintf(stderr, "787[%s]: no combined.ww to re-check\n", + sc->label); + rc = -1; goto done; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, xs, comb); + if (runwait(cmd) == 0) { + fprintf(stderr, "787[%s]: w6c ACCEPTED foreign variant, " + "expected reject\n", sc->label); + rc = -1; goto done; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, xs, comb); + if (runwait(cmd) == 0) { + fprintf(stderr, "787[%s]: w6c_ww ACCEPTED foreign variant " + "(#13 false-accept), expected reject\n", sc->label); + rc = -1; goto done; + } + goto done; + } + + /* cstage driver build + run: pins runtime routing. */ + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build -I %s %s/main.ww", + dir, bin, dir, dir); + if (runwait(cmd) != 0) { + fprintf(stderr, "787[%s]: cstage build failed\n", sc->label); + rc = -1; goto done; + } + snprintf(path, sizeof path, "%s/main", dir); + int got = runwait(path); + if (got != sc->want_exit) { + fprintf(stderr, "787[%s]: cstage exit %d, want %d\n", + sc->label, got, sc->want_exit); + rc = -1; + } + + /* The #13 discriminator: raw w6c_ww on the driver-produced + * combined.ww. Pre-fix the wwstage checker REJECTED the + * cross-module variant arms (non-zero exit here). Post-fix it + * accepts AND is byte-id with cstage's w6c. */ + char cs_s[1024], ws_s[1024]; + 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, "787[%s]: w6c on combined failed\n", sc->label); + 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, "787[%s]: w6c_ww on combined failed " + "(#13 cross-module variant reject?)\n", sc->label); + rc = -1; goto done; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "787[%s]: cs.s/ww.s DIFFER (rule-10 byte-id " + "violation)\n", sc->label); + rc = -1; + } + +done: + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + (void)runwait(cmd); + return rc; +} + +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, "787: w6c_ww missing — cannot run the cs==ww " + "byte-id gate (the whole point of this test)\n"); + return 1; + } + + int n = (int)(sizeof scenarios / sizeof scenarios[0]); + int fail = 0; + for (int i = 0; i < n; i++) { + if (run_scenario(bin, w6c, w6c_ww, &scenarios[i]) != 0) + fail++; + } + + if (fail) { + fprintf(stderr, "787 xmod_variant_match: %d/%d scenarios failed\n", + fail, n); + return 1; + } + printf("xmod_variant_match: %d/%d ok (cstage run + cs==ww byte-id)\n", + n, n); + return 0; +}