/* * 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 *cdrv, const char *wdrv, const struct scenario *sc) { (void)bin; 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); } if (sc->expect_reject) { /* #94 sep layout: there is no combined unit to re-check. The * checker reject now fires inside each driver's --sep w6c pass, * so BOTH driver builds MUST fail. w6c_ww (via ww_ww) accepting * here is the #13 false-ACCEPT regression. */ snprintf(cmd, sizeof cmd, "cd %s && WW_PKGCACHE=%s/pkgc_c %s build --sep -I %s " "-o %s/main %s/main.ww >/dev/null 2>&1", dir, dir, cdrv, dir, dir, dir); if (runwait(cmd) == 0) { fprintf(stderr, "787[%s]: cstage build SUCCEEDED, " "expected reject\n", sc->label); rc = -1; goto done; } snprintf(cmd, sizeof cmd, "cd %s && WW_PKGCACHE=%s/pkgc_w %s build --sep -I %s " "-o %s/mainww %s/main.ww >/dev/null 2>&1", dir, dir, wdrv, dir, dir, dir); if (runwait(cmd) == 0) { fprintf(stderr, "787[%s]: ww_ww ACCEPTED foreign variant " "(#13 false-accept), expected reject\n", sc->label); rc = -1; goto done; } goto done; } /* cstage driver --sep build + run: pins runtime routing. Pin * WW_PKGCACHE under the scratch dir so out/.pkgcache is untouched. */ snprintf(cmd, sizeof cmd, "cd %s && WW_PKGCACHE=%s/pkgc_c %s build --sep -I %s -o %s/main %s/main.ww", dir, dir, cdrv, dir, 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: the wwstage driver's --sep build runs the * wwstage checker over the same units. Pre-fix it REJECTED the * cross-module variant arms (build fails); post-fix it accepts AND * its per-package w6c_ww asm is byte-id with cstage's. The root + * each imported pkg compile to separate .sepwork/.s; * concat (sorted glob, identical set both stages) for the compare. */ snprintf(cmd, sizeof cmd, "cd %s && WW_PKGCACHE=%s/pkgc_w %s build --sep -I %s -o %s/mainww %s/main.ww", dir, dir, wdrv, dir, dir, dir); if (runwait(cmd) != 0) { fprintf(stderr, "787[%s]: ww_ww build failed " "(#13 cross-module variant reject?)\n", sc->label); rc = -1; goto done; } char cs_s[1024], ws_s[1024]; snprintf(cs_s, sizeof cs_s, "%s/all_cs.s", dir); snprintf(ws_s, sizeof ws_s, "%s/all_ww.s", dir); snprintf(cmd, sizeof cmd, "cat %s/main.sepwork/*.s > %s 2>/dev/null", dir, cs_s); if (system(cmd)) {} snprintf(cmd, sizeof cmd, "cat %s/mainww.sepwork/*.s > %s 2>/dev/null", dir, ws_s); if (system(cmd)) {} 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 cdrv[2100], wdrv[2100]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); if (access(wdrv, X_OK) != 0) { fprintf(stderr, "787: ww_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, cdrv, wdrv, &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; }