wcc,ww: @test under separate compilation (M4 E1, #79)

Make `ww test --sep` work the Hare +test way: the -T synth test-main emits a
qualified test.run, and the test package is injected as an ordinary
separately-compiled dependency instead of splicing lib/test source into a
flat unit. Additive — combined stays the default and 910/997 are untouched
(their migration is M4 E2).

- compiler synth (both stages): the -T main emits N_DOT test.run plus a
  synthetic N_USE "test"; cmd/wcc/check.c + selfhost/cmd/wcc/check.ww.
- driver (both stages): build_one_sep gains is_test, injects the test package
  as a root dep, and passes -T to the root; do_test --sep routes a single-file
  test through the sep producer; cmd/ww/main.c + selfhost/cmd/ww/main.ww.
- 989_septest_run gate: ww test --sep on both stages, run-exit + cs==ww
  byte-id of the sep .s, non-vacuous.

The synth's test.run is left ty_err by the checker in both regimes (lib/test's
run is scope-keyed under "" not "test"; cgen emits the correct CALL via run's
//ww:module test directive) — wwstage tolerates it like cstage (rule-10). The
genuine fix, module-keying run under sep so the call type-resolves, is #80.

w6c_ww/wwdump_ww/ww_ww move (their embedded source changed); w6a_ww/w6l_ww and
the combined codegen output are unchanged.
This commit is contained in:
2026-06-17 07:45:46 +09:00
parent fffb6aaf91
commit 37c253c367
9 changed files with 601 additions and 41 deletions

260
test/wcc/989_septest_run.c Normal file
View File

@@ -0,0 +1,260 @@
/*
* 989_septest_run — M4 E1 (#79-B) regression gate: `ww test --sep`.
*
* E1 makes `ww test` work under the separate-compilation producer as an
* ADDITIVE opt-in (combined stays default; 910/997 untouched). The synth
* test-main emits `test.run(__wwtests)` (N_DOT, qualified) + injects a
* synthetic `use test;`; the driver injects `test` as a graph dep of the
* root, passes `-T` to the root producer only, and routes a single-file
* `ww test --sep` through build_one_sep(is_test=1). This gate is the
* STANDING regression for that capability until E2 migrates 997.
*
* Asserts (all COLD — fresh per-(case,stage) WW_PKGCACHE, scratch wiped):
* 1. RUN-EXIT: `ww test --sep <fixture>` exits with the expected code for
* BOTH driver stages — 0 when every @test passes, non-zero when one
* fails. The failing row is the non-vacuity teeth: it proves the @tests
* genuinely RUN under sep (a no-op --sep that linked an empty main would
* exit 0 on the failing fixture and the gate would catch it).
* 2. cs==ww (rule 10): the cstage `ww` and wwstage `ww_ww` sep-drivers emit
* byte-identical per-package .s/.wwi for the passing fixture — including
* __root.s, which carries the synth `CALL test.run`.
* 3. SYNTH PRESENCE (non-vacuity on the -T path): the passing fixture's
* __root.s defines `TEXT main` and emits `CALL test.run(SB)` — proving
* the qualified-synth -T path fired under sep, not a stray combined fall-
* back.
*
* Light wwstage-driver test (CLAUDE.md rule 14): every intermediate is
* `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models
* 989_c6soak_run.c conventions; 989 prefix per the sep-gate precedent.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <dirent.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
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;
}
/* 0 if `needle` occurs in the file at `path`, 1 if absent, -1 on read err. */
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
write_file(const char *path, const char *body)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(body, f);
fclose(f);
return 0;
}
/* cs==ww over a sep test build's per-package output: every .s/.wwi the
* cstage driver produced in `csdir` must be byte-identical to the wwstage
* driver's same-named file in `wwdir`. Returns the count of mismatches. */
static int
cmp_sepwork(const char *csdir, const char *wwdir, const char *label)
{
DIR *d = opendir(csdir);
if (!d) {
fprintf(stderr, "septest FAIL: %s — no cs sepwork %s\n", label, csdir);
return 1;
}
int bad = 0, seen = 0;
struct dirent *ent;
while ((ent = readdir(d)) != NULL) {
const char *nm = ent->d_name;
size_t nl = strlen(nm);
int is_s = (nl > 2 && strcmp(nm + nl - 2, ".s") == 0);
int is_wwi = (nl > 4 && strcmp(nm + nl - 4, ".wwi") == 0);
if (!is_s && !is_wwi) continue;
seen++;
char a[2048], b[2048];
snprintf(a, sizeof a, "%s/%s", csdir, nm);
snprintf(b, sizeof b, "%s/%s", wwdir, nm);
if (files_eq(a, b) != 0) {
fprintf(stderr, "septest FAIL: %s — cs!=ww for %s (rule 10)\n",
label, nm);
bad++;
}
}
closedir(d);
/* An existing-but-empty sepwork would pass the cs==ww loop vacuously. */
if (seen == 0) {
fprintf(stderr, "septest FAIL: %s — no .s/.wwi in %s\n", label, csdir);
bad++;
}
return bad;
}
struct tcase {
const char *label;
const char *src; /* inline fixture written to <td>/<label>.ww */
int expect; /* expected `ww test --sep` run-exit */
int byteid; /* run the cs==ww + synth-presence checks */
};
static struct tcase cases[] = {
/* every @test passes → run-exit 0; the byte-id + synth anchor. */
{ "pass",
"package septest;\n"
"@test fn t_arith() void = {\n"
" let a: i32 = 2;\n"
" if (a + 3 != 5) { abort(); };\n"
"};\n"
"@test fn t_again() void = {\n"
" let s: str = \"ok\";\n"
" if (len(s) != 2) { abort(); };\n"
"};\n", 0, 1 },
/* a failing @test → run-exit 1: proves the tests actually RUN under
* sep (the non-vacuity teeth). */
{ "fail",
"package septest;\n"
"@test fn t_bad() void = {\n"
" if (1 + 1 == 2) { abort(); };\n"
"};\n", 1, 0 },
{ NULL, NULL, 0, 0 },
};
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
char td[64], cmd[8192];
int fail = 0;
snprintf(td, sizeof td, "/tmp/wwseptest_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
mkdir(td, 0755);
for (int i = 0; cases[i].label; i++) {
struct tcase *t = &cases[i];
char rootww[1024];
snprintf(rootww, sizeof rootww, "%s/%s.ww", td, t->label);
if (write_file(rootww, t->src)) { fail++; continue; }
struct { const char *drv, *tag; int rc; }
stg[] = { { "ww", "cs", -1 }, { "ww_ww", "ww", -1 } };
for (int s = 0; s < 2; s++) {
char prog[1024];
/* `.bin` infix keeps the test binary distinct from the
* `<label>.ww` SOURCE; -o leaves <prog>.sepwork for the
* byte-id compare and runs the binary (exit = test result). */
snprintf(prog, sizeof prog, "%s/%s.%s.bin", td, t->label, stg[s].tag);
/* COLD per-(case,stage) cache so every package compiles fresh
* (a warm cache would skip the per-pkg .s/.wwi this gate
* inspects — mirrors 989_c6soak / 989_pkgcache). */
snprintf(cmd, sizeof cmd,
"WW_PKGCACHE='%s/cache.%s.%s' %s/%s test --sep -o %s %s "
">/dev/null 2>&1",
td, t->label, stg[s].tag, bin, stg[s].drv, prog, rootww);
stg[s].rc = runwait(cmd);
}
/* 1. RUN-EXIT: both stages exit with the expected code. */
if (stg[0].rc != t->expect || stg[1].rc != t->expect) {
fprintf(stderr, "septest FAIL: %s exits cs=%d ww=%d (expected %d)\n",
t->label, stg[0].rc, stg[1].rc, t->expect);
fail++;
}
if (!t->byteid) continue;
char csdir[1024], wwdir[1024];
snprintf(csdir, sizeof csdir, "%s/%s.cs.bin.sepwork", td, t->label);
snprintf(wwdir, sizeof wwdir, "%s/%s.ww.bin.sepwork", td, t->label);
/* 2. cs==ww (rule 10): per-package .s/.wwi byte-identical. */
fail += cmp_sepwork(csdir, wwdir, t->label);
/* 3. SYNTH PRESENCE: the -T synth fired under sep. */
char rs[2048];
snprintf(rs, sizeof rs, "%s/__root.s", csdir);
if (file_contains(rs, "TEXT main") != 0) {
fprintf(stderr, "septest FAIL: %s — __root.s lacks `TEXT main` "
"(synth -T main missing)\n", t->label);
fail++;
}
if (file_contains(rs, "CALL\ttest.run(SB)") != 0) {
fprintf(stderr, "septest FAIL: %s — __root.s lacks "
"`CALL test.run(SB)` (qualified synth call missing)\n",
t->label);
fail++;
}
}
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
if (fail) {
fprintf(stderr, "septest: %d check(s) failed\n", fail);
return 1;
}
printf("septest: `ww test --sep` run-exit (pass=0, fail=1) on both driver "
"stages + cs==ww per-pkg .s/.wwi + synth `CALL test.run` in __root.s\n");
return 0;
}