Files
ww/test/wcc/806_tryprop_multisuccess.c
Hojun-Cho 48df04a8ca wcc+w6c_ww: loud-gate try-propagation over multi-success unions (F8/F9 interim)
? and ! assume ONE success member end-to-end: the checker collapses
the result to the first non-error variant (check.c tagged_success_type
/ check.ww exprtype) and cgen emits a single tag compare, so any other
success member is silently mistaken for an error — ? propagates it to
the caller (p11h: []capture read back as nomem, exit 21), ! aborts on
it. Until the honest subset-union result typing lands (task #14, harec
check.c:2759-2835), both stages loud-reject |success| > 1 at the
checker choke-points (one per stage), identical diagnostic, both ops
per rob's one-class ruling (#133 precedent). (T|err1|err2) — one
success, many errors — stays legal (925 canary + new accept rows).

F9 rides along (task #12): wwstage scruttype only resolves IDENT/DOT,
so the direct forms f()? is T / match(f()?) / f()! is T slipped its
lenient-miss contract and were silently ACCEPTED where cstage rejects
(cs!=ww, gate-blind). checkisas/checkmatchexhaust now resolve the
try-result via exprtype, keyed on the RESOLVED success type — a named
tagged success ((ab|nomem)? is i32) keeps being accepted, matching
cstage's verdict empirically.

test/wcc/806: 11 rows x dual driver + byte-id accepts (26 fixtures);
reject rows pin exact per-stage diagnostic text; p11h + q_card2_unw
graduated to rejects; call-arg-position reject + void-success accept
pin position-independence and the dominant lib/ (void|err)? shape.
Tasks #5 + #12; #14 lifts both gates together.
2026-06-04 10:20:18 +09:00

478 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 src[64], tmpdir[64], errlog[80], cmd[1200];
snprintf(src, sizeof src, "/tmp/tpms_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/tpms_%d_d_%d", getpid(), i);
snprintf(errlog, sizeof errlog, "%s.err", src);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>%s",
tmpdir, driver, 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 */
}
unlink(src); unlink(errlog); rmdir(tmpdir);
return rc;
}
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 = runwait(outbin);
unlink(src); unlink(errlog); unlink(outbin); rmdir(tmpdir);
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;
}