wcc: -T test-mode collects @test fns + synthesizes entry, both stages (#15)

@test was parsed then dropped (no consumer); `ww test` needed a hand-written
main listing each test by hand, so adding a @test and forgetting the call
silently skipped it. -T makes the checker collect @test N_FNDECLs in source
order, loud-reject a user main, and append a synthetic
`export fn main() i32 { t0(); ...; return 0; }` at the install->body-check seam;
the existing cgfn emits it (cgen untouched) -> byte-identical by construction.
Mirrors harec's checker-side is_test placement.

Plan-9-lean reduction (user-sanctioned, reinstatable post-CSP): sequential,
abort/nonzero=fail; no setjmp isolation, no fnmatch filter, no file:line.

910/997 rewired from a regex scanner to driving `w6c -T` directly (thin trusted
drivers; the @test content stays ww), with a cross-stage byte-id assert on the
-T output. attest_userman/attest_badsig pin the user-main and bad-signature
rejects.
This commit is contained in:
2026-06-10 02:01:20 +09:00
parent fddd167ce8
commit b795c320c9
12 changed files with 693 additions and 267 deletions

View File

@@ -1,16 +1,27 @@
/*
* 910_at_test — runs @test fn blocks in a source file by
* generating a synthetic main() that calls each one. A test
* passes iff the generated binary exits 0; if any @test calls
* abort/exit(1), the run fails.
* 910_at_test — drives the compiler's `-T` @test mode (task #15).
*
* Scanning is regex-free, matching the literal sequence
* `\n@test\s+fn\s+(IDENT)`. Same parser shape as ww accepts.
* Under `w6c -T`, the checker collects every @test fn and synthesizes
* `export fn main() i32 { t0(); t1(); ...; return 0; }`, calling each in
* source order; a test that runs to completion passes, an abort/div0/
* nonzero exit FAILS the run. This replaces the old driver-side regex
* scan (find_attest) — the @test set now comes from the AST, the single
* source of truth (rob #15 ruling). cgen is untouched: the synthesized
* entry rides the existing cgfn path.
*
* Probes:
* 1. RUN — `w6c -T` the @test-only fixture, assemble, link, run; the
* synthesized entry must exit 0 (every @test passed). Imports are
* resolved by `ww build` into a *.combined.ww (the fixture uses
* `alloc`, whose rt_malloc symbol the driver concatenates); we then
* compile that combined unit with `-T`.
* 2. REJECT — `-T` of a fixture that also defines a user `main` must
* exit nonzero (the synth entry owns main).
* 3. GUARD — `-T` of a fixture whose @test fn is not `fn() void` must
* exit nonzero (rule-7 loud, no silent skip/coerce).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <sys/wait.h>
@@ -23,149 +34,104 @@ runwait(const char *cmd)
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;
}
/* run_fixture — `<comp> -T` the @test fixture (import-resolved into a
* combined unit by <drv> build), assemble, link, run; the synthesized
* entry must exit 0. Returns 0 on success. */
static int
isws(int c)
run_fixture(const char *bin, const char *comp, const char *drv)
{
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
int pid = getpid();
char src[256], comb[256], asmf[256], obj[256], exe[256];
char rt[1024], cmd[4096];
snprintf(src, sizeof src, "/tmp/at910_%s_%d.ww", comp, pid);
snprintf(comb, sizeof comb, "/tmp/at910_%s_%d.combined.ww", comp, pid);
snprintf(asmf, sizeof asmf, "/tmp/at910_%s_%d.s", comp, pid);
snprintf(obj, sizeof obj, "/tmp/at910_%s_%d.o", comp, pid);
snprintf(exe, sizeof exe, "/tmp/at910_%s_%d.exe", comp, pid);
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
static int
isident(int c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_';
}
snprintf(cmd, sizeof cmd, "cp test/wcc/data/attest_pass.ww %s", src);
if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: cp\n"); return 1; }
/* find_attest — scan src for `@test\s+fn\s+(ident)`; on a match,
* write the identifier into `out` (NUL-terminated) and return the
* scan position past the matched ident. Returns -1 when no more. */
static long
find_attest(const char *src, long pos, char *out, size_t outsz)
{
long n = (long)strlen(src);
while (pos < n) {
const char *q = strstr(src + pos, "@test");
if (q == NULL) return -1;
long off = q - src;
/* must be a top-level marker: previous non-space char is
* '\n' or we're at the file start. */
long pre = off - 1;
while (pre >= 0 && (src[pre] == ' ' || src[pre] == '\t')) pre--;
if (pre >= 0 && src[pre] != '\n') {
pos = off + 1;
continue;
}
long c = off + 5; /* past "@test" */
while (c < n && isws(src[c])) c++;
if (c + 2 > n || strncmp(src + c, "fn", 2) != 0) {
pos = off + 1;
continue;
}
c += 2;
if (c < n && isident(src[c])) {
pos = off + 1;
continue;
}
while (c < n && isws(src[c])) c++;
long ids = c;
while (c < n && isident(src[c])) c++;
if (c == ids) {
pos = off + 1;
continue;
}
size_t len = (size_t)(c - ids);
if (len + 1 > outsz) return -1;
memcpy(out, src + ids, len);
out[len] = '\0';
return c;
}
return -1;
}
static char *
slurp(const char *path)
{
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long n = ftell(f);
fseek(f, 0, SEEK_SET);
char *b = malloc((size_t)n + 1);
if (!b) { fclose(f); return NULL; }
if (fread(b, 1, (size_t)n, f) != (size_t)n) {
free(b); fclose(f); return NULL;
}
b[n] = '\0';
fclose(f);
return b;
}
static int
runtests(const char *path, const char *bin)
{
char *src = slurp(path);
if (!src) {
fprintf(stderr, "910 FAIL: cannot read %s\n", path);
return -1;
/* Driver resolves `import rt` into <src>.combined.ww. The build
* itself exits nonzero (the @test-only fixture has no main — that
* is exactly what -T synthesizes), but the combined unit is written
* before any compile step, so we depend on the ARTIFACT, not the
* exit code. */
snprintf(cmd, sizeof cmd, "%s/%s build %s > /dev/null 2>&1", bin, drv, src);
runwait(cmd);
if (access(comb, 0) != 0) {
fprintf(stderr, "910 FAIL: %s build produced no %s\n", drv, comb);
return 1;
}
/* Build a synthetic test driver: original source + a main()
* that calls each @test fn. Each test that runs to completion
* (no abort/exit) counts as a pass. */
char tmpsrc[256];
snprintf(tmpsrc, sizeof tmpsrc, "/tmp/wwd_attest_%d.ww", getpid());
FILE *out = fopen(tmpsrc, "wb");
if (!out) { free(src); return -1; }
fputs(src, out);
fputs("\nexport fn main() i32 = {\n", out);
long pos = 0;
int count = 0;
for (;;) {
char name[128];
long next = find_attest(src, pos, name, sizeof name);
if (next < 0) break;
fprintf(out, "\t%s();\n", name);
pos = next;
count++;
snprintf(cmd, sizeof cmd, "%s/%s -T %s -o %s 2>/dev/null", bin, comp, comb, asmf);
if (runwait(cmd) != 0) {
fprintf(stderr, "910 FAIL: %s -T errored\n", comp);
return 1;
}
fputs("\treturn 0;\n};\n", out);
fclose(out);
free(src);
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, obj, asmf);
if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: w6a\n"); return 1; }
snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null", bin, exe, obj, rt);
if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: w6l\n"); return 1; }
if (count == 0) {
fprintf(stderr, "910 FAIL: no @test fns found in %s\n", path);
unlink(tmpsrc);
return -1;
}
char cmd[2048];
snprintf(cmd, sizeof cmd, "%s/ww run %s 2>/dev/null", bin, tmpsrc);
int rc = runwait(cmd);
unlink(tmpsrc);
int rc = runwait(exe);
if (rc != 0) {
fprintf(stderr, "910 FAIL: %s: %d @test fn(s), driver exited %d\n",
path, count, rc);
return -1;
fprintf(stderr, "910 FAIL: synth entry exited %d (a @test failed)\n", rc);
return 1;
}
return count;
unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe);
return 0;
}
/* reject — `<comp> -T <fixture>` must exit nonzero. */
static int
reject(const char *bin, const char *comp, const char *fixture, const char *what)
{
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s/%s -T %s -o /dev/null 2>/dev/null",
bin, comp, fixture);
if (runwait(cmd) == 0) {
fprintf(stderr, "910 FAIL: %s -T accepted %s (expected reject)\n",
comp, what);
return 1;
}
return 0;
}
/* Each row: a fixture `-T` must loud-reject, and why. */
static const struct {
const char *fixture;
const char *what;
} rejects[] = {
{ "test/wcc/data/attest_userman.ww", "user main" },
{ "test/wcc/data/attest_badsig.ww", "non-void @test" },
};
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
const char *files[] = {
"test/wcc/data/attest_pass.ww",
NULL,
};
int total = 0;
for (int i = 0; files[i]; i++) {
int n = runtests(files[i], bin);
if (n < 0) return 1;
total += n;
}
printf("@test: %d test(s) ran ok\n", total);
const char *bin = absbin();
if (!bin) { fprintf(stderr, "910 FAIL: getcwd\n"); return 1; }
if (run_fixture(bin, "w6c", "ww") != 0) return 1;
for (size_t i = 0; i < sizeof rejects / sizeof rejects[0]; i++)
if (reject(bin, "w6c", rejects[i].fixture, rejects[i].what) != 0)
return 1;
printf("@test -T: run ok + user-main and bad-signature rejected\n");
return 0;
}

View File

@@ -1,11 +1,19 @@
/*
* 997_at_test_ww — same as 910_at_test, driven through `ww_ww`
* (the selfhost driver) instead of `ww` (the C driver). Confirms
* the ww-built toolchain parses, compiles, and runs an @test
* fixture end-to-end.
* 997_at_test_ww — the wwstage twin of 910_at_test, driving the `-T`
* @test mode through `w6c_ww` (the ww-built compiler) instead of `w6c`
* (the C bootstrap). Task #15.
*
* Scanning is regex-free, matching the literal sequence
* `\n@test\s+fn\s+(IDENT)`. Same parser shape as ww accepts.
* Probes:
* 1. RUN — `w6c_ww -T` the @test-only fixture (import-resolved by
* `ww_ww build` into a combined unit), assemble, link, run; the
* synthesized entry must exit 0.
* 2. BYTE-ID (rule 10) — `w6c -T` and `w6c_ww -T` must emit
* byte-identical asm for the same source. The synthesized entry
* rides cgfn at the gated choke point, so the @test set + the
* collection-order calls are identical by construction. This is
* the new entry-synth byte-id gate.
* 3. REJECT — `-T` of a user-main fixture exits nonzero.
* 4. GUARD — `-T` of a non-`fn() void` @test exits nonzero.
*/
#include <stdio.h>
#include <stdlib.h>
@@ -22,149 +30,144 @@ runwait(const char *cmd)
return 1;
}
static int
isws(int c)
static const char *
absbin(void)
{
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
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
isident(int c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_';
}
/* find_attest — scan src for `@test\s+fn\s+(ident)`; on a match,
* write the identifier into `out` (NUL-terminated) and return the
* scan position past the matched ident. Returns -1 when no more. */
static long
find_attest(const char *src, long pos, char *out, size_t outsz)
{
long n = (long)strlen(src);
while (pos < n) {
const char *q = strstr(src + pos, "@test");
if (q == NULL) return -1;
long off = q - src;
/* must be a top-level marker: previous non-space char is
* '\n' or we're at the file start. */
long pre = off - 1;
while (pre >= 0 && (src[pre] == ' ' || src[pre] == '\t')) pre--;
if (pre >= 0 && src[pre] != '\n') {
pos = off + 1;
continue;
}
long c = off + 5; /* past "@test" */
while (c < n && isws(src[c])) c++;
if (c + 2 > n || strncmp(src + c, "fn", 2) != 0) {
pos = off + 1;
continue;
}
c += 2;
if (c < n && isident(src[c])) {
pos = off + 1;
continue;
}
while (c < n && isws(src[c])) c++;
long ids = c;
while (c < n && isident(src[c])) c++;
if (c == ids) {
pos = off + 1;
continue;
}
size_t len = (size_t)(c - ids);
if (len + 1 > outsz) return -1;
memcpy(out, src + ids, len);
out[len] = '\0';
return c;
}
return -1;
}
static char *
slurp(const char *path)
slurp(const char *path, char **outbuf, size_t *outlen)
{
FILE *f = fopen(path, "rb");
if (!f) return NULL;
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 NULL; }
if (fread(b, 1, (size_t)n, f) != (size_t)n) {
free(b); fclose(f); return NULL;
}
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);
return b;
*outbuf = b;
*outlen = (size_t)n;
return 0;
}
static int
runtests(const char *path, const char *bin)
run_fixture(const char *bin)
{
char *src = slurp(path);
if (!src) {
fprintf(stderr, "997 FAIL: cannot read %s\n", path);
return -1;
int pid = getpid();
char src[256], comb[256], asmf[256], obj[256], exe[256];
char rt[1024], cmd[4096];
snprintf(src, sizeof src, "/tmp/at997_%d.ww", pid);
snprintf(comb, sizeof comb, "/tmp/at997_%d.combined.ww", pid);
snprintf(asmf, sizeof asmf, "/tmp/at997_%d.s", pid);
snprintf(obj, sizeof obj, "/tmp/at997_%d.o", pid);
snprintf(exe, sizeof exe, "/tmp/at997_%d.exe", pid);
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
snprintf(cmd, sizeof cmd, "cp test/wcc/data/attest_pass.ww %s", src);
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: cp\n"); return 1; }
/* ww_ww build resolves `import rt` into the combined unit; it exits
* nonzero (no main — that is what -T synthesizes), so we gate on the
* combined ARTIFACT existing, not the exit code. */
snprintf(cmd, sizeof cmd, "%s/ww_ww build %s > /dev/null 2>&1", bin, src);
runwait(cmd);
if (access(comb, 0) != 0) {
fprintf(stderr, "997 FAIL: ww_ww build produced no %s\n", comb);
return 1;
}
/* Build a synthetic test driver: original source + a main()
* that calls each @test fn. Each test that runs to completion
* (no abort/exit) counts as a pass. */
char tmpsrc[256];
snprintf(tmpsrc, sizeof tmpsrc, "/tmp/wwd_attest_ww_%d.ww", getpid());
FILE *out = fopen(tmpsrc, "wb");
if (!out) { free(src); return -1; }
fputs(src, out);
fputs("\nexport fn main() i32 = {\n", out);
snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o %s 2>/dev/null", bin, comb, asmf);
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c_ww -T errored\n"); return 1; }
snprintf(cmd, sizeof cmd, "%s/w6a_ww -o %s %s 2>/dev/null", bin, obj, asmf);
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6a_ww\n"); return 1; }
snprintf(cmd, sizeof cmd, "%s/w6l_ww -o %s %s %s 2>/dev/null", bin, exe, obj, rt);
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6l_ww\n"); return 1; }
long pos = 0;
int count = 0;
for (;;) {
char name[128];
long next = find_attest(src, pos, name, sizeof name);
if (next < 0) break;
fprintf(out, "\t%s();\n", name);
pos = next;
count++;
}
fputs("\treturn 0;\n};\n", out);
fclose(out);
free(src);
if (count == 0) {
fprintf(stderr, "997 FAIL: no @test fns found in %s\n", path);
unlink(tmpsrc);
return -1;
}
char cmd[2048];
snprintf(cmd, sizeof cmd, "%s/ww_ww run %s 2>/dev/null", bin, tmpsrc);
int rc = runwait(cmd);
unlink(tmpsrc);
int rc = runwait(exe);
if (rc != 0) {
fprintf(stderr, "997 FAIL: %s: %d @test fn(s), driver exited %d\n",
path, count, rc);
return -1;
fprintf(stderr, "997 FAIL: synth entry exited %d (a @test failed)\n", rc);
return 1;
}
return count;
unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe);
return 0;
}
/* byteid — `w6c -T` and `w6c_ww -T` on the same source must agree. */
static int
byteid(const char *bin)
{
int pid = getpid();
char cs[256], ws[256], cmd[4096];
snprintf(cs, sizeof cs, "/tmp/at997_bid_c_%d.s", pid);
snprintf(ws, sizeof ws, "/tmp/at997_bid_w_%d.s", pid);
snprintf(cmd, sizeof cmd, "%s/w6c -T test/wcc/data/attest_pass.ww -o %s 2>/dev/null",
bin, cs);
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c -T (byteid)\n"); return 1; }
snprintf(cmd, sizeof cmd, "%s/w6c_ww -T test/wcc/data/attest_pass.ww -o %s 2>/dev/null",
bin, ws);
if (runwait(cmd) != 0) { fprintf(stderr, "997 FAIL: w6c_ww -T (byteid)\n"); return 1; }
char *bc = NULL, *bw = NULL;
size_t nc = 0, nw = 0;
int rc = 0;
if (slurp(cs, &bc, &nc) < 0 || slurp(ws, &bw, &nw) < 0) {
fprintf(stderr, "997 FAIL: slurp byteid asm\n");
rc = 1;
} else if (nc != nw || memcmp(bc, bw, nc) != 0) {
fprintf(stderr, "997 FAIL: -T asm differs (cs %zu, ww %zu)\n", nc, nw);
rc = 1;
}
free(bc); free(bw);
unlink(cs); unlink(ws);
return rc;
}
static int
reject(const char *bin, const char *fixture, const char *what)
{
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s/w6c_ww -T %s -o /dev/null 2>/dev/null", bin, fixture);
if (runwait(cmd) == 0) {
fprintf(stderr, "997 FAIL: w6c_ww -T accepted %s (expected reject)\n", what);
return 1;
}
return 0;
}
/* Each row: a fixture `w6c_ww -T` must loud-reject, and why. */
static const struct {
const char *fixture;
const char *what;
} rejects[] = {
{ "test/wcc/data/attest_userman.ww", "user main" },
{ "test/wcc/data/attest_badsig.ww", "non-void @test" },
};
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
const char *files[] = {
"test/wcc/data/attest_pass.ww",
NULL,
};
int total = 0;
for (int i = 0; files[i]; i++) {
int n = runtests(files[i], bin);
if (n < 0) return 1;
total += n;
}
printf("@test (ww_ww): %d test(s) ran ok\n", total);
const char *bin = absbin();
if (!bin) { fprintf(stderr, "997 FAIL: getcwd\n"); return 1; }
if (run_fixture(bin) != 0) return 1;
if (byteid(bin) != 0) return 1;
for (size_t i = 0; i < sizeof rejects / sizeof rejects[0]; i++)
if (reject(bin, rejects[i].fixture, rejects[i].what) != 0)
return 1;
printf("@test -T (ww_ww): run ok + cs/ww byte-id + rejects ok\n");
return 0;
}

View File

@@ -0,0 +1,18 @@
// #15 negative fixture: an @test fn with a non-`fn() void` signature.
// The collector must loud-reject (nonzero exit) rather than silently
// skip or coerce — rule-7. Pins the signature guard on both stages.
//
// TEETH: a non-void RETURN with NO params is the case ONLY the explicit
// guard catches. The synth entry emits `bad();` (arg count matches 0
// params) and a discarded non-void return is silently allowed, so
// without the guard this compiles clean (exit 0). A param-bearing @test
// would instead trip the checker's independent "not enough arguments"
// (check.c:1879) on the synth `bad()` call — so a param fixture would
// pass the reject probe whether or not the guard exists, isolating
// nothing. Hence the no-param / non-void-return shape.
package data;
@test fn bad() i32 = {
return 0;
};

View File

@@ -0,0 +1,17 @@
// #15 negative fixture: @test fns PLUS an explicit user `main`. Under
// `-T` the entry is synthesized, so a hand-written main collides — both
// stages MUST loud-reject (nonzero exit), mirroring harec's hosted-main
// suppression under is_test (ref/harec/src/check.c:4000).
package data;
@test fn check_ok() void = {
let a: i32 = 1;
if (a != 1) {
let _: i32 = 1 / 0;
};
};
export fn main() i32 = {
return 0;
};