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.
430 lines
13 KiB
C
430 lines
13 KiB
C
/*
|
|
* 989_m2wwi_run — M2 `.wwi` export-data producer gate (task #22 arc).
|
|
* Both stages (rule-10): the new `w6c -I <out.wwi>` producer is DEAD on
|
|
* the live path (combined.ww stays the compile substrate; nothing reads
|
|
* `.wwi`), so this is purely a new cross-stage byte-id substrate.
|
|
*
|
|
* POSITIVE gate — for each package (ascii/strings/getopt, drew2-audited
|
|
* leak-free), drive the target as the PRIMARY module of a driver-combined
|
|
* unit, then:
|
|
* 1. w6c -I cs.wwi and w6c_ww -I ww.wwi both succeed,
|
|
* 2. cs.wwi == ww.wwi byte-for-byte (the new substrate's determinism),
|
|
* 3. cs.wwi RE-PARSES under the existing parser (wwdump -a exits 0) —
|
|
* proves the producer emits genuinely re-parseable ww prototype
|
|
* source, de-risking the M3 consumer.
|
|
* getopt is the recursion stressor (enum→struct→struct, ptr-to-nominal,
|
|
* tagged-union return, qualified pkg.Name). A synth fixture then covers the
|
|
* decl-kinds + type-nodes no lib package reaches: `def` (const-expr unparser
|
|
* + fold parity), `let` global, `[N]T` array, `fn(..)R` fn-ptr, `!T`, tuple,
|
|
* storage-less enum.
|
|
*
|
|
* NEGATIVE gate — a fixture whose exported fn names a PRIVATE (non-
|
|
* exported) nominal must be LOUD-REJECTED by check_exported_type,
|
|
* identically on BOTH stages (same non-zero exit, byte-identical
|
|
* diagnostic). Without it a vacuous no-op check would pass the positive
|
|
* gate silently.
|
|
*
|
|
* Compile/produce/cmp only — no driver run, everything under a getpid-keyed
|
|
* /tmp directory with no source-tree writes. 9xx is full; shares the 989 prefix per the 989_lib_byteid
|
|
* precedent (the `short` name keys the binary).
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/stat.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 const char *
|
|
absbin(void)
|
|
{
|
|
const char *b = getenv("BIN");
|
|
if (!b) b = "out/bin";
|
|
if (b[0] == '/') return b;
|
|
static char buf[2048];
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
|
|
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
|
|
return buf;
|
|
}
|
|
|
|
static int
|
|
slurp(const char *path, char **outbuf, size_t *outlen)
|
|
{
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return -1;
|
|
fseek(f, 0, SEEK_END);
|
|
long n = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
if (n < 0) { fclose(f); return -1; }
|
|
char *b = malloc((size_t)n + 1);
|
|
if (!b) { fclose(f); return -1; }
|
|
if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; }
|
|
b[n] = '\0';
|
|
fclose(f);
|
|
*outbuf = b;
|
|
*outlen = (size_t)n;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
file_contains(const char *path, const char *needle)
|
|
{
|
|
char *b = NULL;
|
|
size_t n = 0;
|
|
if (slurp(path, &b, &n) < 0) return -1;
|
|
int found = (strstr(b, needle) != NULL);
|
|
free(b);
|
|
return found ? 0 : 1;
|
|
}
|
|
|
|
static int
|
|
files_eq(const char *a, const char *b)
|
|
{
|
|
char *ba = NULL, *bb = NULL;
|
|
size_t na = 0, nb = 0;
|
|
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
|
|
free(ba); free(bb);
|
|
return -1;
|
|
}
|
|
int eq = (na == nb && memcmp(ba, bb, na) == 0);
|
|
free(ba); free(bb);
|
|
return eq ? 0 : 1;
|
|
}
|
|
|
|
/* one positive package: build the target as primary, produce the `.wwi`
|
|
* on both stages, require byte-id + re-parse. Returns 0 on pass. */
|
|
static int
|
|
positive(const char *bin, const char *cwd, const char *pkg, int idx)
|
|
{
|
|
char td[64], cmd[4096], src[1024];
|
|
int rc = -1;
|
|
|
|
snprintf(td, sizeof td, "/tmp/wwm2_%d_%d", getpid(), idx);
|
|
if (mkdir(td, 0755) != 0) {
|
|
perror(td);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(src, sizeof src, "%s/lib/%s/%s.ww", cwd, pkg, pkg);
|
|
snprintf(cmd, sizeof cmd, "cp %s %s/%s.ww", src, td, pkg);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: %s — cp\n", pkg);
|
|
goto out;
|
|
}
|
|
|
|
/* #94 sep layout: drive the package file as the build target →
|
|
* its decls are the PRIMARY module of the resolved ROOT unit, which the
|
|
* producer's primary filter narrows to exactly this package's
|
|
* interface. The root unit's amalgamated source is composed at
|
|
* <stem>.sepwork/__root.unit.ww before codegen. `-S` stops after the
|
|
* compiler outputs; the unit is the only artifact consumed here. All
|
|
* generated files stay under td. */
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && timeout 180 %s/ww build -S "
|
|
"-I %s/lib -o %s/%s %s.ww >/dev/null 2>&1",
|
|
td, bin, cwd, td, pkg, pkg);
|
|
runwait(cmd);
|
|
|
|
char comb[1100], cs[1100], ws[1100];
|
|
snprintf(comb, sizeof comb, "%s/%s.sepwork/__root.unit.ww", td, pkg);
|
|
snprintf(cs, sizeof cs, "%s/cs.wwi", td);
|
|
snprintf(ws, sizeof ws, "%s/ww.wwi", td);
|
|
if (access(comb, 0) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: %s — no resolved unit\n", pkg);
|
|
goto out;
|
|
}
|
|
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/w6c -I %s %s >/dev/null 2>&1", bin, cs, comb);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: %s — w6c -I rejected\n", pkg);
|
|
goto out;
|
|
}
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/w6c_ww -I %s %s >/dev/null 2>&1", bin, ws, comb);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: %s — w6c_ww -I rejected\n", pkg);
|
|
goto out;
|
|
}
|
|
|
|
if (files_eq(cs, ws) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: %s — cs.wwi != ww.wwi (byte-id "
|
|
"broken on the .wwi substrate)\n", pkg);
|
|
goto out;
|
|
}
|
|
|
|
/* re-parse: the emitted `.wwi` must be valid ww prototype source. */
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/wwdump -a %s >/dev/null 2>&1", bin, cs);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: %s — emitted .wwi does not "
|
|
"re-parse (wwdump -a)\n", pkg);
|
|
goto out;
|
|
}
|
|
rc = 0;
|
|
out:
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi: cleanup rm -rf %s failed\n", td);
|
|
if (rc == 0) rc = -1;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* negative gate: an exported fn naming a private nominal must be rejected
|
|
* identically (exit + diagnostic) by BOTH stages. Returns 0 on pass. */
|
|
static const char *leak_src =
|
|
"package leaktest;\n"
|
|
"type secret = struct { x: i32 };\n"
|
|
"export fn leaks(s: secret) i32 = { return s.x; };\n"
|
|
"export fn clean(a: i32) i32 = { return a; };\n";
|
|
|
|
static int
|
|
negative(const char *bin)
|
|
{
|
|
char td[64], cmd[4096], p[512];
|
|
int rc = -1;
|
|
|
|
snprintf(td, sizeof td, "/tmp/wwm2neg_%d", getpid());
|
|
if (mkdir(td, 0755) != 0) {
|
|
perror(td);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(p, sizeof p, "%s/leak.ww", td);
|
|
FILE *f = fopen(p, "wb");
|
|
if (!f) goto out;
|
|
fputs(leak_src, f);
|
|
fclose(f);
|
|
|
|
char cse[1100], wwe[1100];
|
|
snprintf(cse, sizeof cse, "%s/cs.err", td);
|
|
snprintf(wwe, sizeof wwe, "%s/ww.err", td);
|
|
|
|
/* cd into td so both stages report the bare `leak.ww:L:C:` prefix. */
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && timeout 180 %s/w6c -I out.wwi leak.ww >/dev/null 2>cs.err",
|
|
td, bin);
|
|
int crc = runwait(cmd);
|
|
snprintf(cmd, sizeof cmd,
|
|
"cd %s && timeout 180 %s/w6c_ww -I out.wwi leak.ww >/dev/null 2>ww.err",
|
|
td, bin);
|
|
int wrc = runwait(cmd);
|
|
|
|
if (crc == 0 || wrc == 0) {
|
|
fprintf(stderr, "m2wwi FAIL: negative — check_exported_type is "
|
|
"vacuous (cstage exit=%d wwstage exit=%d; both must reject "
|
|
"the private-type leak)\n", crc, wrc);
|
|
goto out;
|
|
}
|
|
if (files_eq(cse, wwe) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: negative — reject diagnostics "
|
|
"differ across stages (rule-10)\n");
|
|
goto out;
|
|
}
|
|
rc = 0;
|
|
out:
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi: cleanup rm -rf %s failed\n", td);
|
|
if (rc == 0) rc = -1;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* synth gate: a single self-contained package exercising the decl-kinds +
|
|
* type-AST nodes the lib packages above do NOT cover — `export def` (the
|
|
* const-expr unparser, incl a folded `10+2*3` and a unary `-7`), `export
|
|
* let` (global), `[N]T` array (decl + array-dim ident-ref), an `fn(...) R`
|
|
* fn-pointer type, an `!T` error type, a `(a, b)` tuple return, and a
|
|
* storage-less `enum {...}`. Asserts both stages produce a byte-identical,
|
|
* re-parseable `.wwi` (the const-expr fold + tokname spelling must agree
|
|
* cross-stage — paths the lib gate never reaches). `chan T` is omitted: the
|
|
* wwstage parser does not yet accept it in a return position (pre-existing,
|
|
* unrelated to M2), so the producer's N_TCHAN arm is unreachable there.
|
|
* Returns 0 on pass. */
|
|
static const char *synth_src =
|
|
"package synth;\n"
|
|
"export type color = enum { RED, GREEN = 5, BLUE };\n"
|
|
"export def LIMIT: i32 = 10 + 2 * 3;\n"
|
|
"export def NAME: str = \"hi\\n\";\n"
|
|
"export def FLAG: bool = true;\n"
|
|
"export def NEG: i32 = -7;\n"
|
|
"export let counter: i32;\n"
|
|
"export let grid: [4]i32;\n"
|
|
"export fn apply(f: fn(x: i32) i32, n: i32) i32;\n"
|
|
"export fn risky() !i32;\n"
|
|
"export fn matrix() [LIMIT]u8;\n"
|
|
"export fn pair() (i32, i32);\n"
|
|
"export fn opt(p: *color, b: []u8) (i32 | void);\n"
|
|
/* #47: a codegen/link-relevant attribute (@symbol — the FFI link-
|
|
* symbol override) MUST round-trip into the `.wwi`; dropping it makes
|
|
* sep-compile emit `CALL ext` for a `@symbol("rt_ext")` fn. */
|
|
"@symbol(\"rt_ext\") export fn ext(n: i32) i32;\n";
|
|
|
|
static int
|
|
synth(const char *bin)
|
|
{
|
|
char td[64], cmd[4096], p[512];
|
|
int rc = -1;
|
|
|
|
snprintf(td, sizeof td, "/tmp/wwm2syn_%d", getpid());
|
|
if (mkdir(td, 0755) != 0) {
|
|
perror(td);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(p, sizeof p, "%s/synth.ww", td);
|
|
FILE *f = fopen(p, "wb");
|
|
if (!f) goto out;
|
|
fputs(synth_src, f);
|
|
fclose(f);
|
|
|
|
char cs[1100], ws[1100];
|
|
snprintf(cs, sizeof cs, "%s/cs.wwi", td);
|
|
snprintf(ws, sizeof ws, "%s/ww.wwi", td);
|
|
|
|
/* self-contained package (no imports) → it is its own primary unit,
|
|
* so w6c -I runs directly on the source (no driver-combined step). */
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/w6c -I %s %s/synth.ww >/dev/null 2>&1", bin, cs, td);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: synth — w6c -I rejected\n");
|
|
goto out;
|
|
}
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/w6c_ww -I %s %s/synth.ww >/dev/null 2>&1", bin, ws, td);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: synth — w6c_ww -I rejected\n");
|
|
goto out;
|
|
}
|
|
if (files_eq(cs, ws) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: synth — cs.wwi != ww.wwi (const-expr "
|
|
"/ decl-kind unparse diverges across stages)\n");
|
|
goto out;
|
|
}
|
|
/* #47: the @symbol attribute must survive the round-trip verbatim. */
|
|
if (file_contains(cs, "@symbol(\"rt_ext\") export fn ext") != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: synth — .wwi dropped @symbol "
|
|
"(sep-compile would emit the wrong link symbol)\n");
|
|
goto out;
|
|
}
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/wwdump -a %s >/dev/null 2>&1", bin, cs);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: synth — emitted .wwi does not "
|
|
"re-parse (wwdump -a)\n");
|
|
goto out;
|
|
}
|
|
rc = 0;
|
|
out:
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi: cleanup rm -rf %s failed\n", td);
|
|
if (rc == 0) rc = -1;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* #48 gate: the lib/types limit constants are `export def`s (Hare exports
|
|
* types::I32_MAX &c). Produce types.wwi on both stages directly from the
|
|
* real lib source (no imports → its own primary unit), assert byte-id +
|
|
* re-parse + that the exported limits actually appear in the .wwi (a non-
|
|
* exported def is omitted, so this proves the export reached the interface
|
|
* a cross-package sep-compile consumer reads). Returns 0 on pass. */
|
|
static int
|
|
typesexport(const char *bin, const char *cwd)
|
|
{
|
|
char td[64], cmd[4096], src[1024];
|
|
int rc = -1;
|
|
|
|
snprintf(td, sizeof td, "/tmp/wwm2typ_%d", getpid());
|
|
if (mkdir(td, 0755) != 0) {
|
|
perror(td);
|
|
return -1;
|
|
}
|
|
|
|
snprintf(src, sizeof src, "%s/lib/types/types.ww", cwd);
|
|
char cs[1100], ws[1100];
|
|
snprintf(cs, sizeof cs, "%s/cs.wwi", td);
|
|
snprintf(ws, sizeof ws, "%s/ww.wwi", td);
|
|
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/w6c -I %s %s >/dev/null 2>&1", bin, cs, src);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: types — w6c -I rejected\n");
|
|
goto out;
|
|
}
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/w6c_ww -I %s %s >/dev/null 2>&1", bin, ws, src);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: types — w6c_ww -I rejected\n");
|
|
goto out;
|
|
}
|
|
if (files_eq(cs, ws) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: types — cs.wwi != ww.wwi\n");
|
|
goto out;
|
|
}
|
|
if (file_contains(cs, "export def I32_MAX") != 0
|
|
|| file_contains(cs, "export def RUNE_MAX") != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: types — limit consts not exported "
|
|
"into the .wwi (cross-package sep-compile would not see "
|
|
"types.I32_MAX)\n");
|
|
goto out;
|
|
}
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 180 %s/wwdump -a %s >/dev/null 2>&1", bin, cs);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi FAIL: types — emitted .wwi does not "
|
|
"re-parse (wwdump -a)\n");
|
|
goto out;
|
|
}
|
|
rc = 0;
|
|
out:
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "m2wwi: cleanup rm -rf %s failed\n", td);
|
|
if (rc == 0) rc = -1;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
const char *bin = absbin();
|
|
if (!bin) return 1;
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
|
|
|
const char *pkgs[] = { "ascii", "strings", "getopt", NULL };
|
|
int fail = 0, npos = 0;
|
|
for (int i = 0; pkgs[i]; i++) {
|
|
if (positive(bin, cwd, pkgs[i], i) != 0) fail++;
|
|
else npos++;
|
|
}
|
|
if (synth(bin) != 0) fail++;
|
|
else npos++;
|
|
if (typesexport(bin, cwd) != 0) fail++;
|
|
else npos++;
|
|
if (negative(bin) != 0) fail++;
|
|
|
|
if (fail) {
|
|
fprintf(stderr, "m2wwi: %d check(s) failed\n", fail);
|
|
return 1;
|
|
}
|
|
printf("m2wwi: %d package(s) byte-identical + re-parse; negative "
|
|
"gate rejects (both stages)\n", npos);
|
|
return 0;
|
|
}
|