Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/ fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT- tolerant checked unlinks, exact-path deletion (rm -rf only for an owned pid-keyed dir or a .sepwork beneath one), and cleanup failure fails a passing carrier without overwriting its diagnostic. In the same pass the carriers adapt to the driver contract this branch lands: --sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch contract are asserted, and rows whose runtime or reject coverage moved to test/wcc/data fixtures or test/lang @test owners are trimmed to the byte/artifact/diagnostic observations only they can make. Repair and adaptation ride together because most files interleave both in the same hunks; splitting would manufacture intermediate carrier states that never existed and cannot run against either driver.
404 lines
14 KiB
C
404 lines
14 KiB
C
/*
|
|
* 787_xmod_variant_match — project #13 close. Pins that a `package main`
|
|
* can decompose an IMPORTED union's individual VARIANTS cross-module
|
|
* (`match (e: pkg.u) { case pkg.a => ...; case let o: pkg.b => ... }`),
|
|
* on BOTH stages, byte-identically (rule-10).
|
|
*
|
|
* THE BUG (wwstage-CHECKER-only, cs!=ww): wwstage's match-arm validity
|
|
* (selfhost/cmd/wcc/check.ww casevariantin) and exhaustiveness
|
|
* (casecovers) compared via typeeqast, whose N_TNAME arm is a raw
|
|
* streq. A union's variant is written UNQUALIFIED in its defining
|
|
* module (`a` in pkg.u's body); the cross-module case pattern is the
|
|
* dotted `pkg.a` (one N_TNAME). streq("a","pkg.a") -> false -> wwstage
|
|
* rejected "case: not a variant of scrutinee". cstage compares
|
|
* resolved-Type identity (variant_match, cmd/wcc/check.c:1651), so the
|
|
* qualifier is irrelevant and it built+routed correctly. Hare allows
|
|
* cross-module variant decomposition; align UP to cstage (a too-strict
|
|
* checker, NOT a down-align that would forbid the feature).
|
|
*
|
|
* THE FIX (#13): casevariantpairmatch reduces BOTH pattern and variant
|
|
* to a (module, leaf) pair — a dotted name keeps its own qualifier, a
|
|
* bare name is attributed the union's defining module (taggeddefmod via
|
|
* aliassym .decl.nmod) — and matches the pairs. This accepts a
|
|
* cross-module `case errors.unsupported` (vs bare `unsupported`) while
|
|
* REJECTING a foreign `othermod.unsupported`, and keeps a dotted body
|
|
* variant matching its own qualifier (Shape A). Wired into BOTH
|
|
* casevariantin (validity) and casecovers (exhaustiveness). #10-family
|
|
* (AST-name vs cstage tinfo); precise Type identity is the #10 endgame.
|
|
*
|
|
* The PRE-FIX failure mode is a wwstage CHECKER REJECT, so the
|
|
* discriminator is the `w6c_ww` build of the combined.ww succeeding at
|
|
* all — pre-fix it errored out; post-fix it succeeds AND is byte-id
|
|
* with cstage. Self-contained 2-module fixtures (no lib coupling).
|
|
*
|
|
* scenario | shape | exit | byte-id
|
|
* ------------------+----------------------------------------+------+--------
|
|
* no_default | match pkg.u=!(a|b), bare arms, NO | 21 | cs==ww
|
|
* | default — exercises casecovers | |
|
|
* | exhaustiveness cross-module | |
|
|
* bind_and_default | match pkg.u2=!(a|b|c): bare arm + a | 42 | cs==ww
|
|
* | `case let o: pkg.b` BOUND arm + a | |
|
|
* | `case =>` default; if-checks each arm | |
|
|
* foreign_qualifier | `case o.a` vs pkg.u (o.a is a DIFFERENT | both | (reject
|
|
* | module's same-leaf type) — BOTH |reject| net)
|
|
* | stages must REJECT; guards the fix | |
|
|
* | against a false-ACCEPT (bare leaf- | |
|
|
* | strip would wrongly accept) | |
|
|
* shape_a_dotted_var| union BODY holds dotted variant | 12 | cs==ww
|
|
* | pkg.e=!(errs.bad|local); case errs.bad| |
|
|
* | keeps OWN qualifier (drew Shape A) — | |
|
|
* | sole positive lock on qualmod's | |
|
|
* | dotted-variant branch | |
|
|
*
|
|
* GATE POLARITY: must stay GREEN. Red means wwstage rejected a valid
|
|
* cross-module variant match again (w6c_ww build fails) or the routing
|
|
* diverged (byte-id / runtime).
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <errno.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;
|
|
}
|
|
|
|
static int
|
|
slurp_eq(const char *a, const char *b)
|
|
{
|
|
FILE *fa = fopen(a, "rb");
|
|
FILE *fb = fopen(b, "rb");
|
|
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
|
|
int rc = 0;
|
|
for (;;) {
|
|
int ca = fgetc(fa);
|
|
int cb = fgetc(fb);
|
|
if (ca != cb) { rc = -1; break; }
|
|
if (ca == EOF) break;
|
|
}
|
|
if (ferror(fa) || ferror(fb)) rc = -1;
|
|
if (fclose(fa) != 0) rc = -1;
|
|
if (fclose(fb) != 0) rc = -1;
|
|
return rc;
|
|
}
|
|
|
|
struct file { const char *name; const char *src; };
|
|
|
|
struct scenario {
|
|
const char *label;
|
|
const struct file *files; /* name==NULL terminates */
|
|
int want_exit;
|
|
int expect_reject; /* 1 = BOTH stages must REJECT (checker) */
|
|
};
|
|
|
|
/* ---- no_default: bare cross-module arms, no default (exhaustiveness) */
|
|
static const struct file no_default_files[] = {
|
|
{ "pkg.ww",
|
|
"package pkg;\n"
|
|
"export type a = !void;\n"
|
|
"export type b = !void;\n"
|
|
"export type u = !(a | b);\n"
|
|
"export fn mka() u = { let x: a; return x; };\n"
|
|
"export fn mkb() u = { let y: b; return y; };\n" },
|
|
{ "main.ww",
|
|
"package main;\n"
|
|
"import pkg;\n"
|
|
"fn classify(e: pkg.u) i32 = {\n"
|
|
" match (e) {\n"
|
|
" case pkg.a => return 1;\n"
|
|
" case pkg.b => return 2;\n"
|
|
" };\n"
|
|
"};\n"
|
|
"export fn main() i32 = {\n"
|
|
" return classify(pkg.mkb()) * 10 + classify(pkg.mka());\n"
|
|
"};\n" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
/* ---- bind_and_default: bound arm + default cross-module ----------- */
|
|
static const struct file bind_default_files[] = {
|
|
{ "pkg.ww",
|
|
"package pkg;\n"
|
|
"export type a = !void;\n"
|
|
"export type b = !void;\n"
|
|
"export type c = !void;\n"
|
|
"export type u2 = !(a | b | c);\n"
|
|
"export fn mka() u2 = { let x: a; return x; };\n"
|
|
"export fn mkb() u2 = { let y: b; return y; };\n"
|
|
"export fn mkc() u2 = { let z: c; return z; };\n" },
|
|
{ "main.ww",
|
|
"package main;\n"
|
|
"import pkg;\n"
|
|
"fn classify(e: pkg.u2) i32 = {\n"
|
|
" match (e) {\n"
|
|
" case pkg.a => return 1;\n"
|
|
" case let o: pkg.b => return 2;\n"
|
|
" case => return 9;\n"
|
|
" };\n"
|
|
"};\n"
|
|
"export fn main() i32 = {\n"
|
|
" if (classify(pkg.mka()) != 1) { return 11; };\n"
|
|
" if (classify(pkg.mkb()) != 2) { return 12; };\n"
|
|
" if (classify(pkg.mkc()) != 9) { return 13; };\n"
|
|
" return 42;\n"
|
|
"};\n" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
/* ---- foreign_qualifier: a same-leaf variant from a DIFFERENT module
|
|
* is NOT a variant of the scrutinee — BOTH stages must REJECT. Guards
|
|
* the #13 fix against trading the false-reject for a false-ACCEPT: a
|
|
* bare leaf-strip would wrongly accept `o.a` (leaf "a" matches pkg's
|
|
* variant `a`); the (module,leaf)-pair match rejects it (o != pkg). */
|
|
static const struct file foreign_qualifier_files[] = {
|
|
{ "pkg.ww",
|
|
"package pkg;\n"
|
|
"export type a = !void;\n"
|
|
"export type b = !void;\n"
|
|
"export type u = !(a | b);\n"
|
|
"export fn mka() u = { let x: a; return x; };\n" },
|
|
{ "o.ww",
|
|
"package o;\n"
|
|
"export type a = !void;\n" },
|
|
{ "main.ww",
|
|
"package main;\n"
|
|
"import pkg;\n"
|
|
"import o;\n"
|
|
"fn classify(e: pkg.u) i32 = {\n"
|
|
" match (e) {\n"
|
|
" case o.a => return 1;\n"
|
|
" case => return 0;\n"
|
|
" };\n"
|
|
"};\n"
|
|
"export fn main() i32 = { return classify(pkg.mka()); };\n" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
/* ---- shape_a_dotted_variant: the union's BODY itself holds a dotted
|
|
* cross-module variant (`pkg.e = !(errs.bad | local)`), matched with a
|
|
* dotted `case errs.bad` AND a bare-variant `case pkg.local`. This is
|
|
* drew's Shape A (cf. io.error nesting errors.error): the dotted body
|
|
* variant `errs.bad` must keep ITS OWN qualifier (errs), NOT be forced
|
|
* onto the union's defining module (pkg) — else `case errs.bad` would
|
|
* false-REJECT. The other three scenarios carry only BARE body variants,
|
|
* so this is the sole positive lock on qualmod's dotted-variant branch;
|
|
* a regression that attributes unionmod to every variant breaks here
|
|
* alone. exit = classify(mkbad())*10 + classify(mklocal()) = 1*10+2. */
|
|
static const struct file shape_a_files[] = {
|
|
{ "errs.ww",
|
|
"package errs;\n"
|
|
"export type bad = !void;\n" },
|
|
{ "pkg.ww",
|
|
"package pkg;\n"
|
|
"import errs;\n"
|
|
"export type local = !void;\n"
|
|
"export type e = !(errs.bad | local);\n"
|
|
"export fn mkbad() e = { let x: errs.bad; return x; };\n"
|
|
"export fn mklocal() e = { let y: local; return y; };\n" },
|
|
{ "main.ww",
|
|
"package main;\n"
|
|
"import pkg;\n"
|
|
"import errs;\n"
|
|
"fn classify(x: pkg.e) i32 = {\n"
|
|
" match (x) {\n"
|
|
" case errs.bad => return 1;\n"
|
|
" case pkg.local => return 2;\n"
|
|
" };\n"
|
|
"};\n"
|
|
"export fn main() i32 = {\n"
|
|
" return classify(pkg.mkbad()) * 10 + classify(pkg.mklocal());\n"
|
|
"};\n" },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
static const struct scenario scenarios[] = {
|
|
{ "no_default", no_default_files, 21, 0 },
|
|
{ "bind_and_default", bind_default_files, 42, 0 },
|
|
{ "foreign_qualifier", foreign_qualifier_files, 0, 1 },
|
|
{ "shape_a_dotted_var", shape_a_files, 12, 0 },
|
|
};
|
|
|
|
static int
|
|
run_scenario(const char *bin, const char *cdrv, const char *wdrv,
|
|
const struct scenario *sc)
|
|
{
|
|
(void)bin;
|
|
char dir[] = "/tmp/ww787_XXXXXX";
|
|
if (mkdtemp(dir) == NULL) {
|
|
fprintf(stderr, "787[%s]: mkdtemp failed\n", 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, "787[%s]: write %s\n", sc->label,
|
|
sc->files[i].name); rc = -1; goto done; }
|
|
int bad = fputs(sc->files[i].src, f) == EOF;
|
|
if (fclose(f) != 0) bad = 1;
|
|
if (bad) { fprintf(stderr, "787[%s]: write %s\n", sc->label,
|
|
sc->files[i].name); rc = -1; goto done; }
|
|
}
|
|
|
|
if (sc->expect_reject) {
|
|
/* #94 sep layout: there is no combined unit to re-check. The
|
|
* checker reject now fires inside each driver's w6c pass,
|
|
* so BOTH driver builds MUST fail. w6c_ww (via ww_ww) accepting
|
|
* here is the #13 false-ACCEPT regression. */
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && %s build -I %s "
|
|
"-o %s/main %s/main.ww >/dev/null 2>&1",
|
|
dir, cdrv, dir, dir, dir);
|
|
if (runwait(cmd) == 0) {
|
|
fprintf(stderr, "787[%s]: cstage build SUCCEEDED, "
|
|
"expected reject\n", sc->label);
|
|
rc = -1; goto done;
|
|
}
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && %s build -I %s "
|
|
"-o %s/mainww %s/main.ww >/dev/null 2>&1",
|
|
dir, wdrv, dir, dir, dir);
|
|
if (runwait(cmd) == 0) {
|
|
fprintf(stderr, "787[%s]: ww_ww ACCEPTED foreign variant "
|
|
"(#13 false-accept), expected reject\n", sc->label);
|
|
rc = -1; goto done;
|
|
}
|
|
goto done;
|
|
}
|
|
|
|
/* cstage driver build + run: pins runtime routing. Pin
|
|
* all outputs stay under the scratch dir. */
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && %s build -I %s -o %s/main %s/main.ww",
|
|
dir, cdrv, dir, dir, dir);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "787[%s]: cstage build failed\n", sc->label);
|
|
rc = -1; goto done;
|
|
}
|
|
snprintf(path, sizeof path, "%s/main", dir);
|
|
int got = runwait(path);
|
|
if (got != sc->want_exit) {
|
|
fprintf(stderr, "787[%s]: cstage exit %d, want %d\n",
|
|
sc->label, got, sc->want_exit);
|
|
rc = -1;
|
|
}
|
|
|
|
/* The #13 discriminator: the wwstage driver's build runs the
|
|
* wwstage checker over the same units. Pre-fix it REJECTED the
|
|
* cross-module variant arms (build fails); post-fix it accepts AND
|
|
* its per-package w6c_ww asm is byte-id with cstage's. The root +
|
|
* each imported pkg compile to separate <stem>.sepwork/<pkg>.s;
|
|
* concat (sorted glob, identical set both stages) for the compare. */
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && %s build -I %s -o %s/mainww %s/main.ww",
|
|
dir, wdrv, dir, dir, dir);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "787[%s]: ww_ww build failed "
|
|
"(#13 cross-module variant reject?)\n", sc->label);
|
|
rc = -1; goto done;
|
|
}
|
|
char cs_s[1024], ws_s[1024];
|
|
snprintf(cs_s, sizeof cs_s, "%s/all_cs.s", dir);
|
|
snprintf(ws_s, sizeof ws_s, "%s/all_ww.s", dir);
|
|
snprintf(cmd, sizeof cmd,
|
|
"cat %s/main.sepwork/*.s > %s 2>/dev/null && test -s %s",
|
|
dir, cs_s, cs_s);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "787[%s]: cstage asm aggregation failed\n",
|
|
sc->label);
|
|
rc = -1; goto done;
|
|
}
|
|
snprintf(cmd, sizeof cmd,
|
|
"cat %s/mainww.sepwork/*.s > %s 2>/dev/null && test -s %s",
|
|
dir, ws_s, ws_s);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "787[%s]: wwstage asm aggregation failed\n",
|
|
sc->label);
|
|
rc = -1; goto done;
|
|
}
|
|
if (slurp_eq(cs_s, ws_s) != 0) {
|
|
fprintf(stderr, "787[%s]: cs.s/ww.s DIFFER (rule-10 byte-id "
|
|
"violation)\n", sc->label);
|
|
rc = -1;
|
|
}
|
|
|
|
done:
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s/main.sepwork", dir);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "787[%s]: cleanup main.sepwork\n",
|
|
sc->label); rc = -1; }
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s/mainww.sepwork", dir);
|
|
if (runwait(cmd) != 0) { fprintf(stderr, "787[%s]: cleanup mainww.sepwork\n",
|
|
sc->label); rc = -1; }
|
|
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) {
|
|
fprintf(stderr, "787[%s]: cleanup %s\n", sc->label,
|
|
sc->files[i].name);
|
|
rc = -1;
|
|
}
|
|
}
|
|
const char *children[] = { "main", "mainww", "all_cs.s", "all_ww.s", NULL };
|
|
for (int i = 0; children[i]; i++) {
|
|
snprintf(path, sizeof path, "%s/%s", dir, children[i]);
|
|
if (unlink(path) != 0 && errno != ENOENT) {
|
|
fprintf(stderr, "787[%s]: cleanup %s\n", sc->label,
|
|
children[i]);
|
|
rc = -1;
|
|
}
|
|
}
|
|
if (rmdir(dir) != 0) {
|
|
fprintf(stderr, "787[%s]: cleanup directory\n", sc->label);
|
|
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;
|
|
}
|
|
|
|
char cdrv[2100], wdrv[2100];
|
|
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
|
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
|
if (access(wdrv, X_OK) != 0) {
|
|
fprintf(stderr, "787: ww_ww missing — cannot run the cs==ww "
|
|
"byte-id gate (the whole point of this test)\n");
|
|
return 1;
|
|
}
|
|
|
|
int n = (int)(sizeof scenarios / sizeof scenarios[0]);
|
|
int fail = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
if (run_scenario(bin, cdrv, wdrv, &scenarios[i]) != 0)
|
|
fail++;
|
|
}
|
|
|
|
if (fail) {
|
|
fprintf(stderr, "787 xmod_variant_match: %d/%d scenarios failed\n",
|
|
fail, n);
|
|
return 1;
|
|
}
|
|
printf("xmod_variant_match: %d/%d ok (cstage run + cs==ww byte-id)\n",
|
|
n, n);
|
|
return 0;
|
|
}
|