Files
ww/test/wcc/806_tryprop_multisuccess.c
Hojun-Cho ce3a25a0b4 test: contain sepwork scratch per-driver tmpdir, fix /tmp+in-repo leak (#8)
The wcc test drivers ran `ww build <bare-/tmp src>` with no -o, so the
compiler's <stem>.sepwork scratch landed beside the source and was never
cleaned: unbounded /tmp growth (2195 stale dirs observed) that fills tmpfs
and fabricates phantom test failures + silent harness aborts, and for
in-repo fixture builds leaked .sepwork into the tracked tree.

Each leaking build now writes its source + output inside a per-invocation
tmpdir, passes -o <tmpdir>/<stem> so the .sepwork lands inside it, and
rm -rf's the tmpdir on every exit path -- including fopen-fail and the
expected-fail reject builds (scratch is mkdir'd before the build can fail).
`ww run` and explicit-`-o`/byte-id helpers are left as-is; the 990/993
byte-id comparison logic is byte-for-byte unchanged.

Two items filed separately (this commit holds the no-Makefile / no-main.c
rail):
- #13: a stale <src>.s byte-id readback (749) silently no-ops since
  separate-compile emits .s to <ostem>.sepwork/__root.s; documented inline.
- #14: build-system Makefile recipes build selfhost/cmd/*/main.ww with no
  -o and leak main.sepwork in-tree (bounded, gitignored; own commit).

One concern -- sepwork leak hygiene -- across 228 drivers; uniform
transform applied per-file and two-round reviewed. make test: all 402
passed, zero net-new /tmp scratch, zero test-driven in-repo .sepwork.
2026-06-22 23:29:39 +09:00

474 lines
15 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
}