? 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.
478 lines
15 KiB
C
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;
|
|
}
|