Files
ww/test/wcc/910_at_test.c
Hojun-Cho 939c984f51 wcc,ww: prepend synth use test; user fn run coexists with runner (M4 E2, #80)
The -T harness synthesized `use test;` after name-binding, so the lib/test runner run keyed the bare scope and collided with a user-defined bare fn run — a spurious "duplicate fn run" reject (the E1 tolerance seam). Prepending the synth use before binding keys the runner as test.run in the test module namespace, distinct from the user bare run; the two coexist. Hare-faithful: the runner is its own test module (ref/hare/test/+test.ha:97). Inverts attest_userrun.ww from the #23-mandated reject to a coexist fixture; gate asserts exactly 1 TEXT run + 1 TEXT test.run on the -T asm (distinct symbols, not a dead-dup). Closes #80.
2026-06-17 23:54:21 +09:00

453 lines
17 KiB
C

/*
* 910_at_test — drives the compiler's `-T` @test mode (task #15).
*
* 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 <unistd.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 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
run_fixture(const char *bin, const char *comp, const char *drv)
{
int pid = getpid();
char src[256], comb[256], binout[256], asmf[256], obj[256], exe[256];
char rt[1024], cmd[4096];
snprintf(src, sizeof src, "/tmp/at910_%s_%d.ww", comp, pid);
snprintf(binout, sizeof binout, "/tmp/at910_%s_%d", 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);
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; }
/* `test -c -o <stem>` (compile-only) resolves imports AND auto-bundles
* lib/test (#17) into <stem>.combined.ww — the synth's run() callee
* must link below — WITHOUT running the harness. We depend on the
* combined ARTIFACT; `-o` keeps the binary off the CWD. */
snprintf(cmd, sizeof cmd, "%s/%s test -c -o %s %s > /dev/null 2>&1",
bin, drv, binout, src);
runwait(cmd);
if (access(comb, 0) != 0) {
fprintf(stderr, "910 FAIL: %s build produced no %s\n", drv, comb);
return 1;
}
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;
}
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; }
int rc = runwait(exe);
if (rc != 0) {
fprintf(stderr, "910 FAIL: synth entry exited %d (a @test failed)\n", rc);
return 1;
}
unlink(src); unlink(comb); unlink(asmf); unlink(obj); unlink(exe);
unlink(binout);
char tmp[300];
snprintf(tmp, sizeof tmp, "%s.s", binout); unlink(tmp);
snprintf(tmp, sizeof tmp, "%s.o", binout); unlink(tmp);
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" },
};
/*
* #6: each row asserts whether a symbol's TEXT def appears in the asm
* `<comp>` emits for attest_nondrop.ww in a given mode. A @test fn is
* spliced out of a non-test build (harec ref/harec/src/check.c:3941 —
* checked but never emitted) and kept under -T (the synth entry calls
* it); a plain fn is always emitted.
*/
static const struct {
const char *sym; /* TEXT label to grep for */
int testmode; /* 1 => -T, 0 => plain build */
int present; /* expected: 1 present, 0 absent */
const char *what;
} nondrop_rows[] = {
{ "data.nondrop_keep", 0, 1, "plain fn kept non-T" },
{ "data.nondrop_test_a", 0, 0, "head @test dropped non-T" },
{ "data.nondrop_test_b", 0, 0, "mid @test dropped non-T" },
{ "data.nondrop_test_c", 0, 0, "consecutive @test dropped non-T" },
{ "data.nondrop_keep", 1, 1, "plain fn kept under -T" },
{ "data.nondrop_test_a", 1, 1, "head @test kept under -T" },
{ "data.nondrop_test_b", 1, 1, "mid @test kept under -T" },
{ "data.nondrop_test_c", 1, 1, "consecutive @test kept under -T" },
};
/* nondrop — compile attest_nondrop.ww plain and `-T`, then assert each
* row's TEXT def is present/absent. */
static int
nondrop(const char *bin, const char *comp)
{
int pid = getpid();
char plain[256], tee[256], cmd[4096];
snprintf(plain, sizeof plain, "/tmp/at910nd_%s_p_%d.s", comp, pid);
snprintf(tee, sizeof tee, "/tmp/at910nd_%s_t_%d.s", comp, pid);
snprintf(cmd, sizeof cmd,
"%s/%s test/wcc/data/attest_nondrop.ww -o %s 2>/dev/null",
bin, comp, plain);
if (runwait(cmd) != 0) {
fprintf(stderr, "910 FAIL: %s non-T nondrop compile\n", comp);
return 1;
}
/* The -T compile synthesizes a main that calls lib/test's run(), so it
* needs the lib/test-inclusive combined `ww test -c` writes (the plain
* compile above has no synth, so it stays on the raw fixture). */
char stem[256], comb[300];
snprintf(stem, sizeof stem, "/tmp/at910nd_%s_c_%d", comp, pid);
snprintf(comb, sizeof comb, "%s.combined.ww", stem);
snprintf(cmd, sizeof cmd,
"%s/ww test -c -o %s test/wcc/data/attest_nondrop.ww > /dev/null 2>&1",
bin, stem);
runwait(cmd);
snprintf(cmd, sizeof cmd,
"%s/%s -T %s -o %s 2>/dev/null", bin, comp, comb, tee);
if (runwait(cmd) != 0) {
fprintf(stderr, "910 FAIL: %s -T nondrop compile\n", comp);
return 1;
}
int rc = 0;
for (size_t i = 0; i < sizeof nondrop_rows / sizeof nondrop_rows[0]; i++) {
const char *f = nondrop_rows[i].testmode ? tee : plain;
snprintf(cmd, sizeof cmd, "grep -q '^TEXT %s,' %s",
nondrop_rows[i].sym, f);
int found = runwait(cmd) == 0;
if (found != nondrop_rows[i].present) {
fprintf(stderr, "910 FAIL: %s — %s: %s %s (expected %s)\n",
comp, nondrop_rows[i].what, nondrop_rows[i].sym,
found ? "present" : "absent",
nondrop_rows[i].present ? "present" : "absent");
rc = 1;
break;
}
}
unlink(plain); unlink(tee); unlink(comb);
char tmp[300];
snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp);
snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp);
unlink(stem);
return rc;
}
/* reject_plain — `<comp> <fixture>` (NON-T) must exit nonzero. Pins that
* a @test body is type-checked before the #6 splice drops it: an
* undefined symbol in the body is caught loud even though the fn never
* reaches codegen (harec checks at :3913, drops at :3941). */
static int
reject_plain(const char *bin, const char *comp, const char *fixture,
const char *what)
{
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s/%s %s -o /dev/null 2>/dev/null",
bin, comp, fixture);
if (runwait(cmd) == 0) {
fprintf(stderr, "910 FAIL: %s (non-T) accepted %s "
"(expected reject)\n", comp, what);
return 1;
}
return 0;
}
/* linkfail — a plain fn calling a @test fn, built non-T: compile +
* assemble succeed, but the link MUST fail (the dropped @test def leaves
* the call's symbol dangling — harec-faithful loud failure, never a
* silent mis-link). Linker behavior is stage-independent, so cstage
* suffices; 997's byte-id proves the wwstage .s is identical. */
static int
linkfail(const char *bin, const char *comp)
{
int pid = getpid();
char asmf[256], obj[256], exe[256], rt[1024], cmd[4096];
snprintf(asmf, sizeof asmf, "/tmp/at910lf_%s_%d.s", comp, pid);
snprintf(obj, sizeof obj, "/tmp/at910lf_%s_%d.o", comp, pid);
snprintf(exe, sizeof exe, "/tmp/at910lf_%s_%d.exe", comp, pid);
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
snprintf(cmd, sizeof cmd,
"%s/%s test/wcc/data/attest_calldropped.ww -o %s 2>/dev/null",
bin, comp, asmf);
if (runwait(cmd) != 0) {
fprintf(stderr, "910 FAIL: %s non-T calldropped compile\n", comp);
return 1;
}
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, obj, asmf);
if (runwait(cmd) != 0) {
fprintf(stderr, "910 FAIL: w6a calldropped\n");
unlink(asmf);
return 1;
}
snprintf(cmd, sizeof cmd, "%s/w6l -o %s %s %s 2>/dev/null",
bin, exe, obj, rt);
int rc = 0;
if (runwait(cmd) == 0) {
fprintf(stderr, "910 FAIL: calldropped linked (expected "
"undefined-reference to the dropped @test sym)\n");
rc = 1;
}
unlink(asmf); unlink(obj); unlink(exe);
return rc;
}
/* accept — `<comp> <fixture>` (NON-T) must exit zero. The #23 cross-package
* legal control: distinct-mod same-leaf decls coexist, so the dup reject
* keyed on (name, mod) must NOT fire. */
static int
accept(const char *bin, const char *comp, const char *fixture, const char *what)
{
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s/%s %s -o /dev/null 2>/dev/null",
bin, comp, fixture);
if (runwait(cmd) != 0) {
fprintf(stderr, "910 FAIL: %s rejected %s (expected accept)\n",
comp, what);
return 1;
}
return 0;
}
/* count_text — number of `TEXT <sym>,` directive lines in an asm file
* (Plan-9 `TEXT name,$frame`), -1 on open failure. A COUNT (not presence)
* is what distinguishes the #84 dead-dup: the bug yields 0x `TEXT run` +
* 2x `TEXT test.run`, which a mere presence check would miss. */
static int
count_text(const char *path, const char *sym)
{
char want[128];
snprintf(want, sizeof want, "TEXT %s,", sym);
size_t wlen = strlen(want);
FILE *f = fopen(path, "r");
if (f == NULL) return -1;
char line[8192];
int n = 0;
while (fgets(line, sizeof line, f) != NULL)
if (strncmp(line, want, wlen) == 0) n++;
fclose(f);
return n;
}
/* coexist_run — a @test unit that ALSO defines a user `fn run` must
* COEXIST with lib/test's bound runner, not collide. Post-#80 the synth
* `use test;` is prepended before binding, so lib/test's `run` keys under
* "test" (not "") and no longer duplicate-collides with the user's bare
* `run` (which mangles bare via #84). `<comp> -T` of the `<drv> test -c`
* combined now ACCEPTS; the -T asm carries EXACTLY 1 `TEXT run` (user,
* bare) + 1 `TEXT test.run` (lib runner) — the static-label proof that the
* user run is distinct in the real @test/-T path where #84 lives; then,
* assembled, linked and run, the synth entry exits 0 (the user `run` did
* not hijack the runner and the @test passes). Pre-#80 this loud-rejected
* "duplicate fn run". */
static int
coexist_run(const char *bin, const char *comp, const char *drv)
{
int pid = getpid();
char stem[256], comb[300], asmf[320], obj[320], exe[320];
char rt[1024], cmd[4096];
snprintf(stem, sizeof stem, "/tmp/at910cr_%s_%d", comp, pid);
snprintf(comb, sizeof comb, "%s.combined.ww", stem);
snprintf(asmf, sizeof asmf, "%s.run.s", stem);
snprintf(obj, sizeof obj, "%s.run.o", stem);
snprintf(exe, sizeof exe, "%s.exe", stem);
snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin);
snprintf(cmd, sizeof cmd,
"%s/%s test -c -o %s test/wcc/data/attest_userrun.ww > /dev/null 2>&1",
bin, drv, stem);
runwait(cmd);
if (access(comb, 0) != 0) {
fprintf(stderr, "910 FAIL: %s build produced no %s\n", drv, comb);
return 1;
}
int rc = 0;
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 rejected user `fn run` coexist "
"(expected accept post-#80)\n", comp);
rc = 1;
}
if (rc == 0) {
int nr = count_text(asmf, "run");
int nt = count_text(asmf, "test.run");
if (nr != 1 || nt != 1) {
fprintf(stderr, "910 FAIL: %s -T asm label count run=%d "
"test.run=%d (want 1/1) — user `fn run` not distinct from "
"lib runner (a #84 dead-dup gives run=0/test.run=2)\n",
comp, nr, nt);
rc = 1;
}
}
if (rc == 0) {
snprintf(cmd, sizeof cmd, "%s/w6a -o %s %s 2>/dev/null", bin, obj, asmf);
if (runwait(cmd) != 0) { fprintf(stderr, "910 FAIL: coexist w6a\n"); rc = 1; }
}
if (rc == 0) {
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: coexist w6l\n"); rc = 1; }
}
if (rc == 0 && runwait(exe) != 0) {
fprintf(stderr, "910 FAIL: coexist synth entry nonzero — user "
"`fn run` hijacked the runner or the @test failed\n");
rc = 1;
}
unlink(comb); unlink(asmf); unlink(obj); unlink(exe);
char tmp[320];
snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp);
snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp);
unlink(stem);
return rc;
}
/* modfn_run — `<drv> run <fixture>` and assert exit 6 (#30). The fixture's
* main returns `aa() + aa.helper()` = 1 + 5: the bare `aa()` must resolve to
* the fn (1) and the qualified `aa.helper()` to the module member (5). A
* mis-resolution that still compiled would run to a different code; this
* pins the resolved TARGETS, not just acceptance. */
static int
modfn_run(const char *bin, const char *drv, const char *fixture, const char *what)
{
char cmd[4096];
snprintf(cmd, sizeof cmd, "%s/%s run %s > /dev/null 2>&1", bin, drv, fixture);
int rc = runwait(cmd);
if (rc != 6) {
fprintf(stderr, "910 FAIL: %s run %s exited %d (expected 6 — bare fn "
"+ qualified module resolution)\n", drv, what, rc);
return 1;
}
return 0;
}
int
main(void)
{
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;
if (nondrop(bin, "w6c") != 0) return 1;
if (reject_plain(bin, "w6c", "test/wcc/data/attest_undefbody.ww",
"undefined symbol in @test body") != 0) return 1;
if (linkfail(bin, "w6c") != 0) return 1;
/* #23 — top-level duplicate-decl reject (one row per kind: all four
* route through installtop, so each deserves its own pin) + legal
* cross-package control + the builtin-redecl carve-out accept. */
if (reject_plain(bin, "w6c", "test/wcc/data/dup_fn.ww",
"same-module duplicate fn") != 0) return 1;
if (reject_plain(bin, "w6c", "test/wcc/data/dup_type.ww",
"same-module duplicate type") != 0) return 1;
if (reject_plain(bin, "w6c", "test/wcc/data/dup_def.ww",
"same-module duplicate def") != 0) return 1;
if (reject_plain(bin, "w6c", "test/wcc/data/dup_let.ww",
"same-module duplicate let") != 0) return 1;
if (accept(bin, "w6c", "test/wcc/data/dup_xpkg_ok.ww",
"cross-package same-name") != 0) return 1;
if (accept(bin, "w6c", "test/wcc/data/builtin_redecl_ok.ww",
"builtin nomem redecl (carve-out)") != 0) return 1;
/* #30 — a top-level fn whose leaf matches an imported module name
* coexists with the module bareword (different namespaces). Accept
* proves both the bare `aa()` (fn) and qualified `aa.helper()`
* (module) resolve; a mis-resolution would fail to compile. Both
* decl orders are pinned (use-before-value AND value-before-use): the
* order-dependence is exactly what regresses silently — cstage is
* order-independent, wwstage closes both directions via the symmetric
* promotes in installtop + installdecl's N_USE arm. */
if (accept(bin, "w6c", "test/wcc/data/modfn_coexist_ok.ww",
"fn named like imported module (use-before-value)") != 0) return 1;
if (accept(bin, "w6c", "test/wcc/data/modfn_coexist_vbu_ok.ww",
"fn named like imported module (value-before-use)") != 0) return 1;
/* runtime-resolve: both orders run to exit 6 (bare fn=1 + module
* member=5), pinning the resolved targets on the cstage driver. */
if (modfn_run(bin, "ww", "test/wcc/data/modfn_coexist_ok.ww",
"use-before-value") != 0) return 1;
if (modfn_run(bin, "ww", "test/wcc/data/modfn_coexist_vbu_ok.ww",
"value-before-use") != 0) return 1;
if (coexist_run(bin, "w6c", "ww") != 0) return 1;
printf("@test -T: run ok + user-main and bad-signature rejected + "
"non-T @test drop + checked-body + dangling-call link-fail (#6) + "
"dup fn/type/def/let reject + xpkg + builtin-redecl accept + "
"modfn coexist (#30) + fn-run coexist (#80)\n");
return 0;
}