/* * 806_tryprop_multisuccess — the `?` operator's interim single-success * gate (F8, task #5) and the direct try-form is/match reject parity * (F9, task #12). * * F8: ww's `?` assumes ONE success member end-to-end — the checker * collapses the result to the first non-error variant and cgen emits a * single tag compare — so `f()?` over (A|B|err) silently PROPAGATED * the other success member to the caller as if it were an error * (scratch/fold2b_probes/p11h, exit 21; graduated below as * reject_success2). Until the honest subset-union result typing lands * (task #14, harec check.c:2759-2835), BOTH stages loud-reject * |success| > 1 at the checker; (T|err1|err2) — one success, many * errors — stays legal (runtime canary: 925_tryprop_tag_remap_run). * * F9: cstage types `f()?` as the success variant, so the direct forms * `f()? is T` / `match (f()?)` hit its non-tagged is/match gates and * reject — while wwstage's scruttype (IDENT/DOT-only) silently * ACCEPTED the same files (cs≠ww, gate-blind). Align-richer-DOWN: * wwstage gains the same verdicts (text differs per per-stage diag * conventions, asserted exactly per stage below). A success variant * that is itself a named tagged union must KEEP being accepted * (accept_nested_tagged — guards the mirror against over-rejecting). * * row | shape | want * ----------------------+--------------------------------------+------ * reject_success2 | (void|[]capture|nomem)? — p11h | BUILD_FAIL * reject_success3 | (i32|bool|u64|nomem)? | BUILD_FAIL * reject_try_is | f()? is i32, success = i32 | BUILD_FAIL * reject_try_match | match (f()?), success = i32 | BUILD_FAIL * reject_unw_success2 | (void|[]capture|nomem)! — `!` is the | BUILD_FAIL * | same class (rob ruling, #133) | * reject_tryunw_is | f()! is i32 — kind-agnostic sibling | BUILD_FAIL * reject_callarg | g(f()?) — gate in call-arg position | BUILD_FAIL * accept_two_member | (i32|nomem)? unwrap runtime | 0 * accept_unw_multi_error| (i32|e1|e2)! unwrap runtime | 0 * accept_void_success | (void|nomem)? statement runtime | 0 * accept_nested_tagged | (ab|nomem)? is i32, ab = (i32|bool) | 0 * * BUILD_FAIL rows assert the diagnostic TEXT per stage (a build that * fails for any other reason is a vacuous reject and fails the row). * Accept rows also assert cstage/wwstage asm byte-id. */ #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; } /* want == BUILD_FAIL: the row must FAIL to build on both stages AND * emit the per-stage expected diagnostic on stderr (rule 7 — never a * silent acceptance). */ #define BUILD_FAIL (-2147483647 - 1) struct row { const char *label; const char *src; int want; const char *expect_cs; /* BUILD_FAIL: required cstage stderr substring */ const char *expect_ww; /* BUILD_FAIL: required wwstage stderr substring */ }; #define MULTISUCC_DIAG \ "?: multi-success union unwired (task #14): bind and match instead" static const struct row rows[] = { /* p11h graduated: pre-gate this BUILT and exited 21 (the []capture * success wrongly propagated and read back as nomem). */ { "reject_success2", "package main;\n" "type capture = struct { content: str, start: size, end: size };\n" "fn search2(k: i32) (void | []capture | nomem) = {\n" "\tif (k == 0) { return; };\n" "\tlet caps: []capture = [];\n" "\tappend(caps, capture { content = \"xy\", start = 1, end = 3 });\n" "\treturn caps;\n" "};\n" "fn tb(k: i32) (i32 | nomem) = {\n" "\tlet r: (void | []capture) = search2(k)?;\n" "\tif (r is []capture) { return 7; };\n" "\treturn 8;\n" "};\n" "export fn main() i32 = {\n" "\tlet x: (i32 | nomem) = tb(1);\n" "\tif (x is nomem) { return 21; };\n" "\tif (x is i32) { return 22; };\n" "\treturn 23;\n" "};\n", BUILD_FAIL, MULTISUCC_DIAG, MULTISUCC_DIAG }, { "reject_success3", "package main;\n" "fn f(k: i32) (i32 | bool | u64 | nomem) = { return 7; };\n" "fn g() (i32 | nomem) = {\n" "\tlet v: i32 = f(1)?;\n" "\treturn v;\n" "};\n" "export fn main() i32 = {\n" "\tlet x: (i32 | nomem) = g();\n" "\tif (x is i32) { return 0; };\n" "\treturn 1;\n" "};\n", BUILD_FAIL, MULTISUCC_DIAG, MULTISUCC_DIAG }, /* F9 graduation rows: the ?-result is bare i32, so `is`/`match` * over it must reject on BOTH stages (pre-fix wwstage accepted). */ { "reject_try_is", "package main;\n" "fn f(k: i32) (i32 | nomem) = { return 7; };\n" "fn g() (i32 | nomem) = {\n" "\tif (f(1)? is i32) { return 1; };\n" "\treturn 0;\n" "};\n" "export fn main() i32 = {\n" "\tlet x: (i32 | nomem) = g();\n" "\tif (x is i32) { return 0; };\n" "\treturn 1;\n" "};\n", BUILD_FAIL, "is on non-tagged-union", "is/as: operand is not a tagged union" }, { "reject_try_match", "package main;\n" "fn f(k: i32) (i32 | nomem) = { return 7; };\n" "fn g() (i32 | nomem) = {\n" "\tmatch (f(1)?) {\n" "\tcase i32 => return 1;\n" "\t};\n" "\treturn 0;\n" "};\n" "export fn main() i32 = {\n" "\tlet x: (i32 | nomem) = g();\n" "\tif (x is i32) { return 0; };\n" "\treturn 1;\n" "};\n", BUILD_FAIL, "match on non-tagged-union", "match on non-tagged-union try-result" }, /* `!` shares the single-success collapse — one class, both ops * (rob ruling, #133 precedent): pre-gate this BUILT on BOTH stages * and silently aborted-on-success at runtime (probe q_card2_unw, * graduated). */ { "reject_unw_success2", "package main;\n" "type capture = struct { content: str, start: size, end: size };\n" "fn search2(k: i32) (void | []capture | nomem) = {\n" "\tif (k == 0) { return; };\n" "\tlet caps: []capture = [];\n" "\tappend(caps, capture { content = \"xy\", start = 1, end = 3 });\n" "\treturn caps;\n" "};\n" "fn tb(k: i32) i32 = {\n" "\tlet r: (void | []capture) = search2(k)!;\n" "\tif (r is []capture) { return 7; };\n" "\treturn 8;\n" "};\n" "export fn main() i32 = { return tb(1); };\n", BUILD_FAIL, "!: multi-success union unwired (task #14): bind and match instead", "!: multi-success union unwired (task #14): bind and match instead" }, /* cstage's reject derives from the typed result, not the operator * kind — `!` collapses identically, so its direct form must match * verdicts too (pre-fix wwstage built this and misbehaved). */ { "reject_tryunw_is", "package main;\n" "fn f(k: i32) (i32 | nomem) = { return 7; };\n" "fn g() i32 = {\n" "\tif (f(1)! is i32) { return 1; };\n" "\treturn 0;\n" "};\n" "export fn main() i32 = { return g() - 1; };\n", BUILD_FAIL, "is on non-tagged-union", "is/as: operand is not a tagged union" }, /* `?` nested in a CALL-ARG: the gate keys on the operand's type at * the checker expression walk, so position must not matter — guards * against a walker path that types call args without visiting the * try node. */ { "reject_callarg", "package main;\n" "fn f(k: i32) (i32 | bool | nomem) = { return 7; };\n" "fn g(v: i32) i32 = { return v; };\n" "fn h() (i32 | nomem) = {\n" "\treturn g(f(1)?);\n" "};\n" "export fn main() i32 = {\n" "\tlet x: (i32 | nomem) = h();\n" "\tif (x is i32) { return 0; };\n" "\treturn 1;\n" "};\n", BUILD_FAIL, MULTISUCC_DIAG, MULTISUCC_DIAG }, /* |success| == 1: the gate must not fire and the unwrap must keep * working at runtime. The 3-member single-success shape * (i32|e1|e2) is pinned by 925_tryprop_tag_remap_run. */ { "accept_two_member", "package main;\n" "fn f(k: i32) (i32 | nomem) = { return 7; };\n" "fn g() (i32 | nomem) = {\n" "\tlet v: i32 = f(1)?;\n" "\treturn v + 1;\n" "};\n" "export fn main() i32 = {\n" "\tmatch (g()) {\n" "\tcase let v: i32 => return v - 8;\n" "\tcase nomem => return 2;\n" "\t};\n" "\treturn 3;\n" "};\n", 0, NULL, NULL }, /* |success| == 1 with MULTIPLE errors under `!`: the gate must not * fire and the success unwrap must keep working at runtime (the * `!` twin of 925's ? canary shape). */ { "accept_unw_multi_error", "package main;\n" "type e1 = !void;\n" "type e2 = !void;\n" "fn g(which: i32) (i32 | e1 | e2) = {\n" "\tif (which == 1) { return e1{}; };\n" "\tif (which == 2) { return e2{}; };\n" "\treturn 100;\n" "};\n" "export fn main() i32 = {\n" "\tlet v: i32 = g(0)!;\n" "\treturn v - 100;\n" "};\n", 0, NULL, NULL }, /* VOID success: (void|nomem)? as an expression-statement is the * dominant lib/ shape (io writes etc.) — void counts as the one * success member, nsucc == 1, the gate must not fire. */ { "accept_void_success", "package main;\n" "fn f(k: i32) (void | nomem) = {\n" "\treturn;\n" "};\n" "fn g() (i32 | nomem) = {\n" "\tf(1)?;\n" "\treturn 5;\n" "};\n" "export fn main() i32 = {\n" "\tlet x: (i32 | nomem) = g();\n" "\tmatch (x) {\n" "\tcase let v: i32 => return v - 5;\n" "\tcase nomem => return 2;\n" "\t};\n" "\treturn 3;\n" "};\n", 0, NULL, NULL }, /* Success variant is itself a NAMED tagged union: `f()? is i32` * is `is` over (i32|bool) — tagged — and cstage ACCEPTS. The * wwstage F9 mirror must key on the resolved success type, not * on the try syntax, or this over-rejects. */ { "accept_nested_tagged", "package main;\n" "type ab = (i32 | bool);\n" "fn f(k: i32) (ab | nomem) = {\n" "\tlet v: ab = 7;\n" "\treturn v;\n" "};\n" "fn g() (i32 | nomem) = {\n" "\tif (f(1)? is i32) { return 0; };\n" "\treturn 1;\n" "};\n" "export fn main() i32 = {\n" "\tlet x: (i32 | nomem) = g();\n" "\tmatch (x) {\n" "\tcase let v: i32 => return v;\n" "\tcase nomem => return 2;\n" "\t};\n" "\treturn 3;\n" "};\n", 0, NULL, NULL }, }; /* errlog_has — the build-failure stderr must carry the row's expected * diagnostic; any other failure (parse error, crash) is a vacuous * reject and must not pass. */ static int errlog_has(const char *path, const char *needle) { FILE *f = fopen(path, "rb"); if (!f) return 0; char buf[8192]; size_t got = fread(buf, 1, sizeof buf - 1, f); fclose(f); buf[got] = '\0'; return strstr(buf, needle) != NULL; } static int run_driver(const char *driver, const char *expect, const struct row *r, int i) { char tmpdir[64], src[128], outbin[128], errlog[128], rmcmd[160], cmd[1200]; snprintf(tmpdir, sizeof tmpdir, "/tmp/tpms_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); snprintf(src, sizeof src, "%s/tpms_%d_%d.ww", tmpdir, getpid(), i); snprintf(outbin, sizeof outbin, "%s/tpms_%d_%d", tmpdir, getpid(), i); snprintf(errlog, sizeof errlog, "%s/err", tmpdir); snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir); FILE *f = fopen(src, "wb"); if (!f) { runwait(rmcmd); return -1; } fputs(r->src, f); fclose(f); snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>%s", driver, outbin, src, errlog); if (runwait(cmd) != 0) { int rc = -1; if (r->want != BUILD_FAIL) { fprintf(stderr, "row[%s]: build via %s failed\n", r->label, driver); } else if (expect && !errlog_has(errlog, expect)) { fprintf(stderr, "row[%s]: %s build failed without " "expected diagnostic \"%s\"\n", r->label, driver, expect); rc = -3; /* failed, but for the wrong reason */ } runwait(rmcmd); return rc; } int got = runwait(outbin); runwait(rmcmd); return got; } /* asm_byte_identical — generate .s via cstage's w6c and wwstage's * w6c_ww and diff (rule 10 on the accept rows). */ 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/tpms_asm_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/tpms_asm_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/tpms_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; } int main(void) { const char *bin = getenv("BIN"); if (!bin) bin = "out/bin"; char absbin[2080]; 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[2120]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); char wdrv[2120]; snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); struct { const char *name; const char *path; int is_ww; int gated_on_existence; } drivers[] = { { "cstage", cdrv, 0, 0 }, { "wwstage", wdrv, 1, 1 }, { NULL, NULL, 0, 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_on_existence && access(drivers[d].path, X_OK) != 0) { fprintf(stderr, "tryprop_multisuccess: skip %s (no %s)\n", drivers[d].name, drivers[d].path); continue; } for (int i = 0; i < n; i++) { const char *expect = drivers[d].is_ww ? rows[i].expect_ww : rows[i].expect_cs; int got = run_driver(drivers[d].path, expect, &rows[i], i); total++; int bad = rows[i].want == BUILD_FAIL ? (got != -1) : (got != rows[i].want); if (bad) { fprintf(stderr, "tryprop_multisuccess[%s][%s]: exit=%d want=%d\n", drivers[d].name, rows[i].label, got, rows[i].want); fail++; } } } if (access(wdrv, X_OK) == 0) { for (int i = 0; i < n; i++) { if (rows[i].want == BUILD_FAIL) continue; total++; if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; } } if (fail) { fprintf(stderr, "tryprop_multisuccess: %d/%d fixtures failed\n", fail, total); return 1; } printf("tryprop_multisuccess: %d fixtures passed\n", total); return 0; }