diff --git a/Makefile b/Makefile index 07ce1dd8..c12ec375 100644 --- a/Makefile +++ b/Makefile @@ -598,7 +598,12 @@ DATABYTEID_EXPECTED_MIN = 915 # wwstage spills a wwi-decl'd tagged call-result arg through a 16B # scratch pair where cstage PUSHQes AX/DX (frame 64 vs 80); runtime- # equivalent, sibling of the closed #211 name-keyed family. -DATABYTEID_DIVERGED = r700_strings_byteindex +# r839_xmod_nominal_match: latent, exposed by the 839 migration (the +# carrier waived byte-id silently) — same spill family as r700: wwstage +# routes the cross-module tagged call-result through a 16B scratch pair +# where cstage PUSHQes AX/DX (frame 16 vs 32); runtime-equivalent +# (both stages run exit 42). +DATABYTEID_DIVERGED = r700_strings_byteindex r839_xmod_nominal_match $(if $(DATABYTEID_FILES),,$(error test-byteid: empty data corpus)) test-data-byteid: $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww @work=$$(mktemp -d "$(CURDIR)/$(DATABYTEID_DIR).XXXXXX") || exit 1; \ diff --git a/internal/wwfixture/types.ww b/internal/wwfixture/types.ww index 0ffa7b09..f9e857b1 100644 --- a/internal/wwfixture/types.ww +++ b/internal/wwfixture/types.ww @@ -1,13 +1,13 @@ package wwfixture; def protocolversion: i32 = 1; -def corpuscount: i32 = 1669; -def errorcount: i32 = 339; +def corpuscount: i32 = 1673; +def errorcount: i32 = 341; def compilecount: i32 = 18; -def runcount: i32 = 206; -def runexitcount: i32 = 1106; -def nativecount: i32 = 3338; -def corpushash: str = "2c2ef7841c6da289337a72816041bdc594fa84da18d4f1e11327db3d578e1d14"; +def runcount: i32 = 207; +def runexitcount: i32 = 1107; +def nativecount: i32 = 3346; +def corpushash: str = "a071db5169fe671d40b72d9967caaea053b505f300e64f787f3627ca0f340f5c"; type directive = enum i32 { ERROR = 0, diff --git a/test/wcc/839_xmod_nominal_typeeqast.c b/test/wcc/839_xmod_nominal_typeeqast.c deleted file mode 100644 index 6568c354..00000000 --- a/test/wcc/839_xmod_nominal_typeeqast.c +++ /dev/null @@ -1,302 +0,0 @@ -/* - * 839_xmod_nominal_typeeqast — B-full Layer 1 (#14): typeeqast's N_TNAME - * arm now compares by RESOLVED-decl identity (aliassym pointer-equality), - * not surface spelling. A cross-module type referenced bare inside its - * defining module (`myerr`) and qualified by a consumer (`e.myerr`) used - * to mis-compare unequal (streq), so a nominal forward / variant match was - * silently mis-identified. cstage already equates them (type.c:278 - * `TY_NAMED: a == b` on resolved Type); harec equates them - * (types.c:579 `STORAGE_ALIAS: ident_equal`). ww now matches at the AST - * level via the existing #51/#53 resolver — NO type-interning layer. - * - * The headline discriminator for this fix is the lib/shlex graduation - * (989 #59.15: M_WWREJECT -> M_ID). That flip needs shlex's full - * `(size | io.error)` match pipeline and is NOT reproducible standalone, - * so the gate that actually distinguishes pre-fix surface-streq from - * post-fix nominal-aliassym is 989_lib_byteid, not this test. - * - * scenario | shape | gate - * ------------------+---------------------------------------------+------ - * nominal_match | match e.res whose variants are spelled BARE | 42 - * | inside e, with QUALIFIED case patterns | - * | (`case e.myerr`) — nominal cover | - * a6_concrete | concrete `e.myerr` returned into `e.res` | 0 - * | (A6 concrete->tagged, bare-vs-qualified) | - * incompat_concrete | `str` into `(i64 | e.myerr)` — NO str | FAIL - * | variant: confident reject (no over-accept)| - * distinct_alias | distinct alias `other = !i64` into `e.res` | FAIL - * | — rejected at CGEN (#95 structural | - * | ambiguity: two i64-underlying variants), | - * | symmetric both stages | - * - * SCOPE / no-teeth disclosure: all four rows decide IDENTICALLY against - * the pre-fix surface-streq HEAD (nominal_match->42, a6_concrete->0, the - * two NEG rows reject) on BOTH stages — the nominal-identity change does - * not flip any of them, because each is reached through a sibling - * mechanism that already handled bare-vs-qualified: nominal_match via - * casevariantpairmatch (#205 match-cover resolve), a6_concrete via the - * concrete->tagged arm's underlying-i64 resolvealias, and distinct_alias - * via the cgen #95 ambiguity reject (the CHECKER accepts it — `other`'s - * underlying i64 matches a variant; the reject is structural, not - * nominal). So this test does NOT guard a revert of typeeqast to streq; - * that is 989 #59.15's job. What it DOES pin is that the four - * cross-module nominal accept/reject decisions stay correct AND symmetric - * across cstage/wwstage (rule-10) — a forward guard against either stage - * drifting on cross-module nominal forwards. - * - * Both stages must agree (rule-10): the POS rows build+run to the same - * exit on cstage and wwstage; the NEG rows build-FAIL on both. Byte-id of - * the emitted asm is NOT asserted here — receiving a cross-module tagged - * return and re-passing it crosses a pre-existing, unrelated cgen frame- - * layout divergence; B-full Layer 1's byte-id-neutrality is gated by - * 989_lib_byteid + 990-997 (the bootstrap forwards). - */ -#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 file { const char *name; const char *src; }; - -struct scenario { - const char *label; - const struct file *files; /* name==NULL terminates */ - int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */ - int want_exit; -}; - -/* ---- nominal_match: bare union variants, qualified case patterns ---- */ -static const struct file nominal_match_files[] = { - { "e.ww", - "package e;\n" - "\n" - "export type myerr = !i64;\n" - "export type res = (i64 | myerr);\n" - "\n" - "export fn ok(v: i64) res = { return v; };\n" - "export fn bad() res = { return (-7: myerr); };\n" }, - { "main.ww", - "package main;\n" - "\n" - "import e;\n" - "\n" - "fn unwrap(r: e.res) i64 = {\n" - " match (r) {\n" - " case let n: i64 => return n;\n" - " case let x: e.myerr => return -1;\n" - " };\n" - "};\n" - "\n" - "export fn main() i32 = {\n" - " let a = unwrap(e.ok(40));\n" - " let b = unwrap(e.bad());\n" - " if (a != 40) { return 11; };\n" - " if (b != -1) { return 12; };\n" - " return 42;\n" - "};\n" }, - { NULL, NULL } -}; - -/* ---- a6_concrete: concrete e.myerr into e.res return (A6) ---------- */ -static const struct file a6_concrete_files[] = { - { "e.ww", - "package e;\n" - "\n" - "export type myerr = !i64;\n" - "export type res = (i64 | myerr);\n" }, - { "main.ww", - "package main;\n" - "\n" - "import e;\n" - "\n" - "fn wrap(x: e.myerr) e.res = { return x; };\n" - "\n" - "export fn main() i32 = { return 0; };\n" }, - { NULL, NULL } -}; - -/* ---- incompat_concrete: str into (i64|e.myerr) — must reject ------- */ -static const struct file incompat_concrete_files[] = { - { "e.ww", - "package e;\n" - "\n" - "export type myerr = !i64;\n" }, - { "main.ww", - "package main;\n" - "\n" - "import e;\n" - "\n" - "type u = (i64 | e.myerr);\n" - "\n" - "fn bad(s: str) u = { return s; };\n" - "\n" - "export fn main() i32 = { return 0; };\n" }, - { NULL, NULL } -}; - -/* ---- distinct_alias: distinct alias over same underlying — reject -- */ -static const struct file distinct_alias_files[] = { - { "e.ww", - "package e;\n" - "\n" - "export type myerr = !i64;\n" - "export type res = (i64 | myerr);\n" }, - { "main.ww", - "package main;\n" - "\n" - "import e;\n" - "\n" - "type other = !i64;\n" - "\n" - "fn f(x: other) e.res = { return x; };\n" - "\n" - "export fn main() i32 = { return 0; };\n" }, - { NULL, NULL } -}; - -static const struct scenario scenarios[] = { - { "nominal_match", nominal_match_files, 1, 42 }, - { "a6_concrete", a6_concrete_files, 1, 0 }, - { "incompat_concrete", incompat_concrete_files, 0, 0 }, - { "distinct_alias", distinct_alias_files, 0, 0 }, -}; - -struct driver { const char *name; char path[2100]; int gated; }; - -static int -run_one(const struct driver *drv, const struct scenario *sc) -{ - char dir[] = "/tmp/ww839_XXXXXX"; - if (mkdtemp(dir) == NULL) { - fprintf(stderr, "839[%s][%s]: mkdtemp failed\n", - drv->name, 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, "839[%s][%s]: write %s\n", - drv->name, sc->label, sc->files[i].name); - rc = -1; goto done; - } - int writebad = fputs(sc->files[i].src, f) == EOF || ferror(f); - if (fclose(f) != 0) writebad = 1; - if (writebad) { - fprintf(stderr, "839[%s][%s]: write %s\n", - drv->name, sc->label, sc->files[i].name); - rc = -1; goto done; - } - } - - snprintf(cmd, sizeof cmd, "cd %s && %s build -I %s %s/main.ww " - ">/dev/null 2>&1", dir, drv->path, dir, dir); - int brc = runwait(cmd); - - if (!sc->expect_build) { - /* NEG: the build must FAIL on this stage. */ - if (brc == 0) { - fprintf(stderr, "839[%s][%s]: built ok, expected a " - "confident reject\n", drv->name, sc->label); - rc = -1; - } - goto done; - } - - if (brc != 0) { - fprintf(stderr, "839[%s][%s]: build failed, expected ok\n", - drv->name, sc->label); - rc = -1; goto done; - } - snprintf(path, sizeof path, "%s/main", dir); - int got = runwait(path); - if (got != sc->want_exit) { - fprintf(stderr, "839[%s][%s]: exit %d, want %d\n", - drv->name, sc->label, got, sc->want_exit); - rc = -1; - } - -done: - { - int cleanfail = 0; - for (int i = 0; sc->files[i].name; i++) { - snprintf(path, sizeof path, "%s/%s", dir, - sc->files[i].name); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - } - snprintf(path, sizeof path, "%s/main", dir); - if (unlink(path) != 0 && errno != ENOENT) cleanfail = 1; - snprintf(path, sizeof path, "%s/main.sepwork", dir); - snprintf(cmd, sizeof cmd, "rm -rf %s", path); - if (runwait(cmd) != 0) cleanfail = 1; - if (rmdir(dir) != 0) cleanfail = 1; - if (cleanfail) { - fprintf(stderr, "839[%s][%s]: workspace cleanup failed\n", - drv->name, sc->label); - if (rc == 0) rc = -1; - } - } - 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; - } - - struct driver drivers[2]; - snprintf(drivers[0].path, sizeof drivers[0].path, "%s/ww", bin); - drivers[0].name = "cstage"; - drivers[0].gated = 0; - snprintf(drivers[1].path, sizeof drivers[1].path, "%s/ww_ww", bin); - drivers[1].name = "wwstage"; - drivers[1].gated = 1; - - int n = (int)(sizeof scenarios / sizeof scenarios[0]); - int total = 0, fail = 0; - - for (int d = 0; d < 2; d++) { - if (drivers[d].gated && access(drivers[d].path, X_OK) != 0) { - fprintf(stderr, "839: skip %s (no %s)\n", - drivers[d].name, drivers[d].path); - continue; - } - for (int i = 0; i < n; i++) { - total++; - if (run_one(&drivers[d], &scenarios[i]) != 0) - fail++; - } - } - - if (fail) { - fprintf(stderr, "839 xmod_nominal_typeeqast: %d/%d failed\n", - fail, total); - return 1; - } - printf("xmod_nominal_typeeqast: %d/%d ok (cross-module nominal " - "accept + reject, both stages)\n", total, total); - return 0; -} diff --git a/test/wcc/data/r839_xmod_a6_concrete/case.ww b/test/wcc/data/r839_xmod_a6_concrete/case.ww new file mode 100644 index 00000000..9dbac414 --- /dev/null +++ b/test/wcc/data/r839_xmod_a6_concrete/case.ww @@ -0,0 +1,9 @@ +//ww:run +// migrated from test/wcc/839_xmod_nominal_typeeqast.c: A6 — concrete e.myerr returned into e.res (bare-vs-qualified nominal forward), both stages accept. +package e; +export type myerr = !i64; +export type res = (i64 | myerr); +package main; +import e; +fn wrap(x: e.myerr) e.res = { return x; }; +export fn main() i32 = { return 0; }; diff --git a/test/wcc/data/r839_xmod_distinct_alias/case.ww b/test/wcc/data/r839_xmod_distinct_alias/case.ww new file mode 100644 index 00000000..2d5fd240 --- /dev/null +++ b/test/wcc/data/r839_xmod_distinct_alias/case.ww @@ -0,0 +1,10 @@ +//ww:error "ambiguous without nominal layout" +// migrated from test/wcc/839_xmod_nominal_typeeqast.c: a distinct local alias over the same underlying i64 into e.res — #95 structural-ambiguity reject, both stages. +package e; +export type myerr = !i64; +export type res = (i64 | myerr); +package main; +import e; +type other = !i64; +fn f(x: other) e.res = { return x; }; +export fn main() i32 = { return 0; }; diff --git a/test/wcc/data/r839_xmod_incompat_concrete/case.ww b/test/wcc/data/r839_xmod_incompat_concrete/case.ww new file mode 100644 index 00000000..a4cd2834 --- /dev/null +++ b/test/wcc/data/r839_xmod_incompat_concrete/case.ww @@ -0,0 +1,9 @@ +//ww:error "not assignable" +// migrated from test/wcc/839_xmod_nominal_typeeqast.c: str into (i64 | e.myerr) has no variant — confident reject, both stages. +package e; +export type myerr = !i64; +package main; +import e; +type u = (i64 | e.myerr); +fn bad(s: str) u = { return s; }; +export fn main() i32 = { return 0; }; diff --git a/test/wcc/data/r839_xmod_nominal_match/case.ww b/test/wcc/data/r839_xmod_nominal_match/case.ww new file mode 100644 index 00000000..62a3a148 --- /dev/null +++ b/test/wcc/data/r839_xmod_nominal_match/case.ww @@ -0,0 +1,22 @@ +//ww:run-exit 42 +// migrated from test/wcc/839_xmod_nominal_typeeqast.c: bare-spelled union variants matched via QUALIFIED case patterns (`case let x: e.myerr`) — cross-module nominal cover. +package e; +export type myerr = !i64; +export type res = (i64 | myerr); +export fn ok(v: i64) res = { return v; }; +export fn bad() res = { return (-7: myerr); }; +package main; +import e; +fn unwrap(r: e.res) i64 = { + match (r) { + case let n: i64 => return n; + case let x: e.myerr => return -1; + }; +}; +export fn main() i32 = { + let a = unwrap(e.ok(40)); + let b = unwrap(e.bad()); + if (a != 40) { return 11; }; + if (b != -1) { return 12; }; + return 42; +};