installdecl routes all four kinds (fn/type/def/let) through installtop, which turns scopedefineinmodule's nil return into cstage's exact "duplicate <kind> <name>" reject, keyed (name,mod) so cross-package same-leaf decls coexist. Builtin redecls are dropped, not dup-errored: cstage never scopes builtins (lookup_builtin first, check.c:69), so a user redecl is dead there — wwstage mirrors via scopesamekeysym + no-source-decl test. -T synth __wwtests installs direct, mirroring check.c:3079. Closes the silent dup-fn hole (user fn run vs lib/test run built a broken test binary with no diagnostic). Per-kind reject rows + cross-package/builtin accept byte-id rows + -T collision parity row in 910/997. (#23-team, category-A addendum closed)
358 lines
13 KiB
C
358 lines
13 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 <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;
|
|
}
|
|
|
|
/* collide_run — a @test unit defining a user `fn run` collides with
|
|
* lib/test's bound runner once `<drv> test -c` bundles it; `<comp> -T` of
|
|
* the combined must loud-reject the duplicate (#23). Pre-fix wwstage built
|
|
* a binary that called the user run and silently skipped every @test. */
|
|
static int
|
|
collide_run(const char *bin, const char *comp, const char *drv)
|
|
{
|
|
int pid = getpid();
|
|
char stem[256], comb[300], cmd[4096];
|
|
snprintf(stem, sizeof stem, "/tmp/at910cr_%s_%d", comp, pid);
|
|
snprintf(comb, sizeof comb, "%s.combined.ww", stem);
|
|
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;
|
|
}
|
|
snprintf(cmd, sizeof cmd, "%s/%s -T %s -o /dev/null 2>/dev/null",
|
|
bin, comp, comb);
|
|
int rc = 0;
|
|
if (runwait(cmd) == 0) {
|
|
fprintf(stderr, "910 FAIL: %s -T accepted user `fn run` collision "
|
|
"(expected duplicate-fn reject)\n", comp);
|
|
rc = 1;
|
|
}
|
|
unlink(comb);
|
|
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;
|
|
}
|
|
|
|
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;
|
|
if (collide_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 + "
|
|
"fn-run collision (#23)\n");
|
|
return 0;
|
|
}
|