diff --git a/Makefile b/Makefile index 65191627..9b790a70 100644 --- a/Makefile +++ b/Makefile @@ -244,6 +244,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arr_tagged_elem \ $(BIN)/test_arr_infer_len \ $(BIN)/test_arr_cap_reject \ + $(BIN)/test_tagged_subset_reject \ $(BIN)/test_arr_ptr_global \ $(BIN)/test_def_arr_infer_len \ $(BIN)/test_def_arr_len \ @@ -663,6 +664,12 @@ $(BIN)/test_arr_cap_reject: test/wcc/817_arr_cap_reject.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tagged_subset_reject: test/wcc/989_tagged_subset_reject.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_arr_ptr_global: test/wcc/818_arr_ptr_global.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 27325f33..9f8cd675 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14243,7 +14243,57 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { }; v = v.next; }; - *confident = false; + // Spread `...` member on either side: keep the lenient escape. + // cstage flattens spreads at resolve_type so its subset loop + // never sees one; wwstage stays AST-keyed (#115, check.ww:921), + // so a TK_ELLIPSIS variant can reach here. Routing it through + // the strict typeeqast subset loop would over-reject a valid + // spread-widen cstage accepts → new cs≠ww divergence. Spread + // decl-form layout is #199b, deferred. + let hasspread: bool = false; + for (let p: *node = du.list; p != nil; p = p.next) { + if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; }; + }; + for (let p: *node = su.list; p != nil; p = p.next) { + if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; }; + }; + if (hasspread) { + *confident = false; + return true; + }; + // Structural subset loop — wwstage was align-DOWN-missing this; + // cstage type.c type_assignable tagged→tagged arm (type.c:360-367). + // Every src variant must appear in dst, else a loud reject; a + // genuine subset accepts. + // + // Per-variant cover is the HONEST FLOOR (drew ruling A): typeeqast + // FIRST for the exact / structural variants (e.g. []u8 slices, + // nested unions — bufio scanbytes/scanline forward `[]u8`), THEN a + // LEAF-ONLY name bridge (qualleaf, module IGNORED) for the bare-vs- + // qualified spelling mix typeeqast cannot span. A callee returning + // an INLINE union spells its variants BARE (utf8.next: + // (rune|done|more|invalid)) while the consumer annotates them + // QUALIFIED (utf8.done); the module qualifier is unrecoverable for + // an inline-union return, so leaf alone decides. Mirrors casecovers' + // typeeqast-then-leaf composition (check.ww:4136). qualmod is NOT + // compared (cf casevariantpairmatch): module identity is the #10 + // gap. Sound for the bootstrap — no two of its variants share a leaf + // (drew); the cross-module same-leaf collision (a.foo vs b.foo) is a + // known over-accept deferred to #10 / filed in the #4 census. + for (let sp: *node = su.list; sp != nil; sp = sp.next) { + let ok: bool = false; + for (let dp: *node = du.list; dp != nil; dp = dp.next) { + if (typeeqast(c, dp, sp)) { ok = true; break; }; + let su2: *node = unwrapbang(sp); + let du2: *node = unwrapbang(dp); + if (su2 != nil && du2 != nil && + su2.kind == nkind.N_TNAME && du2.kind == nkind.N_TNAME && + streq(qualleaf(su2.str), qualleaf(du2.str))) { + ok = true; break; + }; + }; + if (!ok) { return false; }; + }; return true; }; // tagged → non-tagged: requires `?` / `!` / match to project a diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index b2d88895..dd5481b8 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -3962,7 +3962,57 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { }; v = v.next; }; - *confident = false; + // Spread `...` member on either side: keep the lenient escape. + // cstage flattens spreads at resolve_type so its subset loop + // never sees one; wwstage stays AST-keyed (#115, check.ww:921), + // so a TK_ELLIPSIS variant can reach here. Routing it through + // the strict typeeqast subset loop would over-reject a valid + // spread-widen cstage accepts → new cs≠ww divergence. Spread + // decl-form layout is #199b, deferred. + let hasspread: bool = false; + for (let p: *node = du.list; p != nil; p = p.next) { + if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; }; + }; + for (let p: *node = su.list; p != nil; p = p.next) { + if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; }; + }; + if (hasspread) { + *confident = false; + return true; + }; + // Structural subset loop — wwstage was align-DOWN-missing this; + // cstage type.c type_assignable tagged→tagged arm (type.c:360-367). + // Every src variant must appear in dst, else a loud reject; a + // genuine subset accepts. + // + // Per-variant cover is the HONEST FLOOR (drew ruling A): typeeqast + // FIRST for the exact / structural variants (e.g. []u8 slices, + // nested unions — bufio scanbytes/scanline forward `[]u8`), THEN a + // LEAF-ONLY name bridge (qualleaf, module IGNORED) for the bare-vs- + // qualified spelling mix typeeqast cannot span. A callee returning + // an INLINE union spells its variants BARE (utf8.next: + // (rune|done|more|invalid)) while the consumer annotates them + // QUALIFIED (utf8.done); the module qualifier is unrecoverable for + // an inline-union return, so leaf alone decides. Mirrors casecovers' + // typeeqast-then-leaf composition (check.ww:4136). qualmod is NOT + // compared (cf casevariantpairmatch): module identity is the #10 + // gap. Sound for the bootstrap — no two of its variants share a leaf + // (drew); the cross-module same-leaf collision (a.foo vs b.foo) is a + // known over-accept deferred to #10 / filed in the #4 census. + for (let sp: *node = su.list; sp != nil; sp = sp.next) { + let ok: bool = false; + for (let dp: *node = du.list; dp != nil; dp = dp.next) { + if (typeeqast(c, dp, sp)) { ok = true; break; }; + let su2: *node = unwrapbang(sp); + let du2: *node = unwrapbang(dp); + if (su2 != nil && du2 != nil && + su2.kind == nkind.N_TNAME && du2.kind == nkind.N_TNAME && + streq(qualleaf(su2.str), qualleaf(du2.str))) { + ok = true; break; + }; + }; + if (!ok) { return false; }; + }; return true; }; // tagged → non-tagged: requires `?` / `!` / match to project a diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 5f12d2ab..639a2868 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14243,7 +14243,57 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { }; v = v.next; }; - *confident = false; + // Spread `...` member on either side: keep the lenient escape. + // cstage flattens spreads at resolve_type so its subset loop + // never sees one; wwstage stays AST-keyed (#115, check.ww:921), + // so a TK_ELLIPSIS variant can reach here. Routing it through + // the strict typeeqast subset loop would over-reject a valid + // spread-widen cstage accepts → new cs≠ww divergence. Spread + // decl-form layout is #199b, deferred. + let hasspread: bool = false; + for (let p: *node = du.list; p != nil; p = p.next) { + if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; }; + }; + for (let p: *node = su.list; p != nil; p = p.next) { + if (p.op == tkind.TK_ELLIPSIS) { hasspread = true; }; + }; + if (hasspread) { + *confident = false; + return true; + }; + // Structural subset loop — wwstage was align-DOWN-missing this; + // cstage type.c type_assignable tagged→tagged arm (type.c:360-367). + // Every src variant must appear in dst, else a loud reject; a + // genuine subset accepts. + // + // Per-variant cover is the HONEST FLOOR (drew ruling A): typeeqast + // FIRST for the exact / structural variants (e.g. []u8 slices, + // nested unions — bufio scanbytes/scanline forward `[]u8`), THEN a + // LEAF-ONLY name bridge (qualleaf, module IGNORED) for the bare-vs- + // qualified spelling mix typeeqast cannot span. A callee returning + // an INLINE union spells its variants BARE (utf8.next: + // (rune|done|more|invalid)) while the consumer annotates them + // QUALIFIED (utf8.done); the module qualifier is unrecoverable for + // an inline-union return, so leaf alone decides. Mirrors casecovers' + // typeeqast-then-leaf composition (check.ww:4136). qualmod is NOT + // compared (cf casevariantpairmatch): module identity is the #10 + // gap. Sound for the bootstrap — no two of its variants share a leaf + // (drew); the cross-module same-leaf collision (a.foo vs b.foo) is a + // known over-accept deferred to #10 / filed in the #4 census. + for (let sp: *node = su.list; sp != nil; sp = sp.next) { + let ok: bool = false; + for (let dp: *node = du.list; dp != nil; dp = dp.next) { + if (typeeqast(c, dp, sp)) { ok = true; break; }; + let su2: *node = unwrapbang(sp); + let du2: *node = unwrapbang(dp); + if (su2 != nil && du2 != nil && + su2.kind == nkind.N_TNAME && du2.kind == nkind.N_TNAME && + streq(qualleaf(su2.str), qualleaf(du2.str))) { + ok = true; break; + }; + }; + if (!ok) { return false; }; + }; return true; }; // tagged → non-tagged: requires `?` / `!` / match to project a diff --git a/test/wcc/989_tagged_subset_reject.c b/test/wcc/989_tagged_subset_reject.c new file mode 100644 index 00000000..c467552e --- /dev/null +++ b/test/wcc/989_tagged_subset_reject.c @@ -0,0 +1,488 @@ +/* + * 989_tagged_subset_reject — A7: wwstage align UP to cstage on the + * tagged-union → tagged-union assignability subset check (CHECKER-ONLY). + * + * THE BUG: an implicit (no-cast) WIDE → NARROW tagged assignment + * (`(int|bool|str)` returned where `(int|bool)` is wanted) was SILENTLY + * ACCEPTED by wwstage (its tagged→tagged arm fell through a lenient + * `*confident=false; return true` escape) while cstage correctly REJECTS + * it (structural subset loop, cmd/wcc/type.c:360-367). The accepted + * payload (`str`) was then read through the `int` arm by cgen → garbage + * → exit 1. Pure wwstage-align-UP: cstage gets ZERO touch. + * + * THE FIX (selfhost/cmd/wcc/check.ww, tagged→tagged arm): after the #205 + * NAMED-variant nominal loop, replace the lenient escape with cstage's + * structural subset loop — every src variant must appear in dst (nominal + * identity via Layer-1 typeeqast); a missing variant rejects loud. GUARD: + * the subset loop runs ONLY when neither side carries a `...spread` + * (TK_ELLIPSIS) member; a spread-bearing union keeps the lenient escape + * (cstage flattens spreads in resolve, wwstage stays AST-keyed — routing + * a spread through the strict loop would over-reject a valid spread-widen + * cstage accepts; spread decl-form layout is #199b, deferred). + * + * The fix's predicate is a COMPOSITION (drew ruling A, the honest floor): + * per src variant, typeeqast FIRST (exact / structural — primitives, []u8 + * slices, nested unions, Layer-1 nominal aliases), THEN a leaf-only name + * bridge (qualleaf, module ignored) for the bare-vs-qualified spelling mix + * an inline-union callee return forces (variants spelled BARE in the + * callee, QUALIFIED at the consumer; the module is unrecoverable there). + * The two single-file rows below cannot exercise either half distinctly + * (one module → every name is bare → typeeqast's streq matches), so the + * teeth for the COMPOSITION are the dedicated rows: + * - slice_struct_subset: a []u8 (N_TSLICE, not N_TNAME) src variant is + * matched ONLY by typeeqast — a pure-leaf-bridge regression would + * wrongly reject it. + * - xmod_inline_forward: a bare inline-union variant vs its qualified + * consumer spelling is matched ONLY by the leaf bridge (typeeqast / + * aliassym cannot resolve a bare inline-return variant in the + * consumer's scope) — dropping the bridge regresses cs=0/ww=1. + * + * row | src → dst | verdict + * -------------------------+---------------------------------+----------- + * wide_narrow_nocast | (int|bool|str) → (int|bool) | REJECT [bug] + * flatten_subset | (bool|str) → (int|bool|str) | ok 0 +byteid + * slice_struct_subset | (int|[]u8) → (int|bool|[]u8) | ok 0 +byteid + * nested_wrap | inner → (int|inner) | ok 42 +byteid + * concrete_spread_widen | 42,"hi" → (...formattable|bool) | ok 0 + * spread_named_casematch | case e1 of (str|...e1) | REJECT [#115] + * spread_wider_subset | (str|...e1) → (str|bool|int|f64)| ok 0 [lenient] + * + * Cross-module rows (839-style -I build; wwstage-gated; byte-id NOT + * asserted — receiving + re-passing a cross-module tagged return crosses a + * pre-existing cgen frame-layout divergence, cf. 839): + * xmod_inline_forward | e.next (i64|done) → (i64|e.done)| ok 0 + * xmod_inline_narrow | (i64|done|more) → (i64|e.done) | REJECT + * + * Rows flatten_subset / slice_struct_subset / nested_wrap also assert w6c + * vs w6c_ww asm is BYTE-IDENTICAL (rule-10): the fix only changes the + * REJECT decision and a confident-vs-lenient accept on inputs cstage + * already accepted, so the emitted code is unchanged. The two spread-accept + * rows (concrete_spread_widen / spread_wider_subset) assert accept-on-both- + * stages only — they ride the deferred #199b spread path where the stages + * intentionally diverge (wwstage AST-keyed vs cstage pre-flattened), so + * byte-id is NOT asserted (cf. 839's cross-module-return note). + * spread_named_casematch is a #115 case-match reject (not the subset arm), + * pinned here as a symmetric-both-stage regression guard. + * + * Both stages must agree (rule-10): REJECT rows build-FAIL on both; accept + * rows build+run to the same exit on cstage (`ww`) and wwstage (`ww_ww`). + */ +#include +#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; +} + +struct row { + const char *label; + const char *src; + int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */ + int want_exit; /* meaningful only when expect_build */ + int byteid; /* 1 = also assert w6c vs w6c_ww .s byte-id */ +}; + +static const struct row rows[] = { + /* (1) THE BUG — wide → narrow, no cast → REJECT both stages. + * `str` is a src variant absent from dst → subset loop rejects. */ + { "wide_narrow_nocast", + "package main;\n" + "type big = (int | bool | str);\n" + "type small = (int | bool);\n" + "fn f(b: big) small = { return b; };\n" + "export fn main() int = { return 0; };\n", + 0, 0, 0 }, + + /* (2) D flatten-subset, union→union → ACCEPT + byte-id. + * {bool,str} ⊆ {int,bool,str}; cstage flattens inner + remaps tags. */ + { "flatten_subset", + "package main;\n" + "type inner = (bool | str);\n" + "type flat = (int | bool | str);\n" + "fn f(x: inner) flat = { return x; };\n" + "export fn main() int = { return 0; };\n", + 1, 0, 1 }, + + /* (2b) []u8-bearing structural subset → ACCEPT + byte-id. The `[]u8` + * src variant is N_TSLICE, not N_TNAME, so it is matched ONLY by + * typeeqast — never the leaf bridge. Teeth for the composition: a + * pure-leaf-bridge predicate would drop typeeqast and wrongly reject + * this (cs=0/ww=1). {int,[]u8} ⊆ {int,bool,[]u8}. */ + { "slice_struct_subset", + "package main;\n" + "type a = (int | []u8);\n" + "type b = (int | bool | []u8);\n" + "fn f(x: a) b = { return x; };\n" + "export fn main() int = { return 0; };\n", + 1, 0, 1 }, + + /* (3) B nested-wrap → ACCEPT + byte-id + runtime 42. inner is a + * DIRECT variant of outer → #205 nominal loop accepts (wrap, no + * flatten). Regression pin that the subset loop never reaches it. */ + { "nested_wrap", + "package main;\n" + "type inner = (bool | str);\n" + "type outer = (int | inner);\n" + "fn wrap(x: inner) outer = { return x; };\n" + "export fn main() int = {\n" + " let v: inner = \"hi\";\n" + " let o: outer = wrap(v);\n" + " match (o) {\n" + " case let i: int => { return 1; };\n" + " case let n: inner =>\n" + " match (n) {\n" + " case let b: bool => { return 2; };\n" + " case let s: str => { return 42; };\n" + " };\n" + " };\n" + "};\n", + 1, 42, 1 }, + + /* (4) concrete → spread-field widen → ACCEPT (A6 concrete→tagged + * path, NOT the tagged→tagged subset arm). Pins the spread path is + * untouched; byte-id NOT asserted (#199b spread divergence). */ + { "concrete_spread_widen", + "package main;\n" + "type formattable = (int | str | bool);\n" + "type field = (...formattable | bool);\n" + "fn take(f: field) int = { return 0; };\n" + "export fn main() int = { take(42); take(\"hi\"); return 0; };\n", + 1, 0, 0 }, + + /* (5) `...spread` member vs NAMED case pattern → REJECT both stages + * (#115, check.ww:929 case-match path — NOT the subset arm). A + * symmetric regression pin; the A7 fix must not perturb it. */ + { "spread_named_casematch", + "package main;\n" + "type e1 = (bool | int);\n" + "type sp = (str | ...e1);\n" + "fn f(x: sp) int = {\n" + " match (x) {\n" + " case let s: str => { return 1; };\n" + " case let e: e1 => { return 2; };\n" + " };\n" + "};\n" + "export fn main() int = { return 0; };\n", + 0, 0, 0 }, + + /* (5b) spread → wider subset assign (latent) → KEEP lenient accept. + * sp's src side carries a `...e1` spread → the guard keeps the + * lenient escape (do NOT newly reject); cstage flattens & accepts. + * byte-id NOT asserted (#199b spread divergence). */ + { "spread_wider_subset", + "package main;\n" + "type e1 = (bool | int);\n" + "type sp = (str | ...e1);\n" + "type big = (str | bool | int | f64);\n" + "fn g(x: sp) big = { return x; };\n" + "export fn main() int = { return 0; };\n", + 1, 0, 0 }, +}; + +/* run_build — build+run `src` via `driver`; returns the binary's exit + * code, or -1 on a build failure. */ +static int +run_build(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tsr_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tsr_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -2; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + int brc = runwait(cmd); + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = -1; + if (brc == 0) got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return brc == 0 ? got : -1; +} + +/* build_should_fail — the build must error on `driver`; returns 0 when it + * correctly FAILS, non-zero when it wrongly succeeded. */ +static int +build_should_fail(const char *driver, const char *src, int i) +{ + char s[64], tmpdir[64], cmd[1024]; + snprintf(s, sizeof s, "/tmp/tsrn_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tsrn_%d_d_%d", getpid(), i); + + FILE *f = fopen(s, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, s); + int rc = runwait(cmd); + + unlink(s); + const char *base = strrchr(s, '/'); + base = base ? base + 1 : s; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + unlink(outbin); + rmdir(tmpdir); + return rc == 0 ? -1 : 0; /* build must NOT succeed */ +} + +/* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/tsr_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/tsr_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/tsr_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +/* Cross-module rows — the leaf-bridge teeth. A bare inline-union variant + * (callee) vs its qualified consumer spelling is matched ONLY by the leaf + * bridge, and that mix is intrinsically multi-module (one file → all names + * bare → typeeqast's streq matches). 839-style `-I` build; byte-id NOT + * asserted (cross-module tagged return + re-pass crosses a pre-existing + * cgen frame-layout divergence, cf. 839). */ +struct xrow { + const char *label; + const char *esrc; /* package e */ + const char *msrc; /* package main; import e */ + int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */ + int want_exit; +}; + +static const struct xrow xrows[] = { + /* leaf-bridge ACCEPT: e.next returns the inline union (i64|done) + * spelled BARE; forward into qualified (i64|e.done). typeeqast/ + * aliassym cannot resolve the bare `done` in main's scope, so only + * the leaf bridge equates them. Dropping the bridge → cs=0/ww=1. */ + { "xmod_inline_forward", + "package e;\n" + "export type done = !i64;\n" + "export fn next() (i64 | done) = { return 0; };\n", + "package main;\n" + "import e;\n" + "type myres = (i64 | e.done);\n" + "fn forward() myres = { return e.next(); };\n" + "export fn main() i32 = { return 0; };\n", + 1, 0 }, + + /* named genuine-absence REJECT: src carries `more`, absent from dst + * (i64|e.done) → the subset loop rejects a NAMED variant, both + * stages. Guards the over-accept the lenient escape used to allow. */ + { "xmod_inline_narrow", + "package e;\n" + "export type done = !i64;\n" + "export type more = !i64;\n" + "export fn next() (i64 | done | more) = { return 0; };\n", + "package main;\n" + "import e;\n" + "type myres = (i64 | e.done);\n" + "fn forward() myres = { return e.next(); };\n" + "export fn main() i32 = { return 0; };\n", + 0, 0 }, +}; + +/* run_xmod — write e.ww + main.ww into a fresh dir, `ww build -I dir + * main.ww`, run dir/main. Returns the binary's exit, or -1 on build + * failure (matches the run_build/build_should_fail convention). */ +static int +run_xmod(const char *driver, const struct xrow *x) +{ + char dir[] = "/tmp/tsrx_XXXXXX"; + if (mkdtemp(dir) == NULL) return -2; + + char path[1024], cmd[4096]; + int wrote = 0; + snprintf(path, sizeof path, "%s/e.ww", dir); + FILE *fe = fopen(path, "wb"); + if (fe) { fputs(x->esrc, fe); fclose(fe); wrote++; } + snprintf(path, sizeof path, "%s/main.ww", dir); + FILE *fm = fopen(path, "wb"); + if (fm) { fputs(x->msrc, fm); fclose(fm); wrote++; } + if (wrote != 2) { + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + (void)runwait(cmd); + return -2; + } + + snprintf(cmd, sizeof cmd, "cd %s && %s build -I %s %s/main.ww " + "2>/dev/null", dir, driver, dir, dir); + int brc = runwait(cmd); + + int got = -1; + if (brc == 0) { + snprintf(path, sizeof path, "%s/main", dir); + got = runwait(path); + } + + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + (void)runwait(cmd); + return brc == 0 ? got : -1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + 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 cdrv[1024], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "tagged_subset_reject: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + total++; + if (rows[i].expect_build) { + int got = run_build(drivers[d].path, &rows[i], i); + if (got != rows[i].want_exit) { + fprintf(stderr, + "tagged_subset_reject[%s][%s]: " + "exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want_exit); + fail++; + } + } else { + if (build_should_fail(drivers[d].path, + rows[i].src, 100 + i) != 0) { + fprintf(stderr, + "tagged_subset_reject[%s][%s]: " + "built ok, expected a loud reject\n", + drivers[d].name, rows[i].label); + fail++; + } + } + } + + int xn = (int)(sizeof xrows / sizeof xrows[0]); + for (int i = 0; i < xn; i++) { + total++; + int got = run_xmod(drivers[d].path, &xrows[i]); + if (xrows[i].expect_build) { + if (got != xrows[i].want_exit) { + fprintf(stderr, + "tagged_subset_reject[%s][%s]: " + "exit=%d want=%d\n", + drivers[d].name, xrows[i].label, + got, xrows[i].want_exit); + fail++; + } + } else if (got != -1) { + fprintf(stderr, + "tagged_subset_reject[%s][%s]: " + "built ok, expected a loud reject\n", + drivers[d].name, xrows[i].label); + fail++; + } + } + } + + /* byte-id rows: w6c vs w6c_ww .s must match (rule-10). */ + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + if (!rows[i].byteid) continue; + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "tagged_subset_reject: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("tagged_subset_reject: %d/%d ok\n", total, total); + return 0; +}