diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 6097841d..f04b9edb 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -3228,4 +3228,38 @@ check_file(Checker *c, Node *file) } } c->cur_mod = NULL; + + /* + * #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is + * fully checked above — pass 2 walked its body like every fn — but + * is NOT emitted in a non-test build. harec skips append_decl for + * FN_TEST && !is_test, so the fn never reaches unit->declarations + * (the list codegen walks); the body is still type-checked, only the + * emission is dropped. ww shares one file->list across check + cgen + * (no separate checked-decl list, project_hare_ast_no_result), so we + * splice the already-checked @test fns out here, after pass 2 — they + * stay checked, never reach cg_file. The -T path is untouched: its + * synth main calls the @test fns, so they must remain. Prereq for + * in-package @test colocation (#9). Twin: selfhost/cmd/wcc/check.ww. + */ + if (!c->is_test) { + Node *prev = NULL; + for (Node *d = file->list; d; ) { + int istest = 0; + if (d->kind == N_FNDECL) + for (Node *at = d->attr; at; at = at->next) + if (at->str + && strcmp(at->str, "test") == 0) { + istest = 1; + break; + } + Node *nx = d->next; + if (istest) { + if (prev == NULL) file->list = nx; + else prev->next = nx; + } else + prev = d; + d = nx; + } + } } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7010ec53..3e78d3f5 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15996,6 +15996,42 @@ export fn checkfile(c: *checker, file: *node) void = { d = d.next; }; + // #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is fully + // checked above (pass 2 + pass 3 walked it like every fn) but is NOT + // emitted in a non-test build. harec skips append_decl for + // FN_TEST && !is_test, so the fn never reaches the codegen decl list; + // the body is still checked, only the emission is dropped. ww shares + // one file.list across check + cgen (no separate checked-decl list), + // so we splice the already-checked @test fns out here, after all + // passes — they stay checked, never reach cgen. The -T path is + // untouched: its synth main calls the @test fns, so they must remain. + // Twin: cmd/wcc/check.c. + if (c.istest == 0) { + let prev: *node = nil; + let e: *node = file.list; + for (e != nil) { + let istest: bool = false; + if (e.kind == nkind.N_FNDECL) { + let at: *node = e.attr; + for (at != nil) { + if (at.kind == nkind.N_ATTR + && streq(at.str, "test")) { + istest = true; + }; + at = at.next; + }; + }; + let nx: *node = e.next; + if (istest) { + if (prev == nil) { file.list = nx; } + else { prev.next = nx; }; + } else { + prev = e; + }; + e = nx; + }; + }; + let empty: str; c.curmod = empty; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index acb4f276..9f4ff229 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -5664,6 +5664,42 @@ export fn checkfile(c: *checker, file: *node) void = { d = d.next; }; + // #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is fully + // checked above (pass 2 + pass 3 walked it like every fn) but is NOT + // emitted in a non-test build. harec skips append_decl for + // FN_TEST && !is_test, so the fn never reaches the codegen decl list; + // the body is still checked, only the emission is dropped. ww shares + // one file.list across check + cgen (no separate checked-decl list), + // so we splice the already-checked @test fns out here, after all + // passes — they stay checked, never reach cgen. The -T path is + // untouched: its synth main calls the @test fns, so they must remain. + // Twin: cmd/wcc/check.c. + if (c.istest == 0) { + let prev: *node = nil; + let e: *node = file.list; + for (e != nil) { + let istest: bool = false; + if (e.kind == nkind.N_FNDECL) { + let at: *node = e.attr; + for (at != nil) { + if (at.kind == nkind.N_ATTR + && streq(at.str, "test")) { + istest = true; + }; + at = at.next; + }; + }; + let nx: *node = e.next; + if (istest) { + if (prev == nil) { file.list = nx; } + else { prev.next = nx; }; + } else { + prev = e; + }; + e = nx; + }; + }; + let empty: str; c.curmod = empty; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d4fa3642..ccbb7460 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15996,6 +15996,42 @@ export fn checkfile(c: *checker, file: *node) void = { d = d.next; }; + // #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is fully + // checked above (pass 2 + pass 3 walked it like every fn) but is NOT + // emitted in a non-test build. harec skips append_decl for + // FN_TEST && !is_test, so the fn never reaches the codegen decl list; + // the body is still checked, only the emission is dropped. ww shares + // one file.list across check + cgen (no separate checked-decl list), + // so we splice the already-checked @test fns out here, after all + // passes — they stay checked, never reach cgen. The -T path is + // untouched: its synth main calls the @test fns, so they must remain. + // Twin: cmd/wcc/check.c. + if (c.istest == 0) { + let prev: *node = nil; + let e: *node = file.list; + for (e != nil) { + let istest: bool = false; + if (e.kind == nkind.N_FNDECL) { + let at: *node = e.attr; + for (at != nil) { + if (at.kind == nkind.N_ATTR + && streq(at.str, "test")) { + istest = true; + }; + at = at.next; + }; + }; + let nx: *node = e.next; + if (istest) { + if (prev == nil) { file.list = nx; } + else { prev.next = nx; }; + } else { + prev = e; + }; + e = nx; + }; + }; + let empty: str; c.curmod = empty; diff --git a/test/wcc/910_at_test.c b/test/wcc/910_at_test.c index 0f2578e9..af31a871 100644 --- a/test/wcc/910_at_test.c +++ b/test/wcc/910_at_test.c @@ -121,6 +121,132 @@ static const struct { { "test/wcc/data/attest_badsig.ww", "non-void @test" }, }; +/* + * #6: each row asserts whether a symbol's TEXT def appears in the asm + * `` 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; + } + snprintf(cmd, sizeof cmd, + "%s/%s -T test/wcc/data/attest_nondrop.ww -o %s 2>/dev/null", + bin, comp, 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); + return rc; +} + +/* reject_plain — ` ` (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; +} + int main(void) { @@ -131,7 +257,12 @@ main(void) 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; - printf("@test -T: run ok + user-main and bad-signature rejected\n"); + printf("@test -T: run ok + user-main and bad-signature rejected + " + "non-T @test drop + checked-body + dangling-call link-fail (#6)\n"); return 0; } diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index 2c54631d..17154f0a 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -292,8 +292,8 @@ check_one(const char *bin, const char *cwd, const struct ent *e, int idx) snprintf(cs, sizeof cs, "%s/c.s", td); snprintf(ws, sizeof ws, "%s/w.s", td); /* Fixtures are main-less @test files: -T synthesizes the entry and - * keeps the @test fns symmetrically (non-T drops them cstage-side, - * task #6, so byte-id needs -T). Import-probes carry their own + * keeps the @test fns symmetrically (non-T drops them on both stages, + * #6, so byte-id needs -T). Import-probes carry their own * `fn main()`, which -T loud-rejects (910), so they stay non-T. */ const char *tflag = e->fixture ? "-T " : ""; snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c %s%s > %s 2>/dev/null", diff --git a/test/wcc/997_at_test_ww.c b/test/wcc/997_at_test_ww.c index fd8214c8..26cc0e80 100644 --- a/test/wcc/997_at_test_ww.c +++ b/test/wcc/997_at_test_ww.c @@ -156,6 +156,104 @@ static const struct { { "test/wcc/data/attest_badsig.ww", "non-void @test" }, }; +/* #6: TEXT-def present/absent rows for attest_nondrop.ww (mirror 910). */ +static const struct { + const char *sym; + int testmode; /* 1 => -T, 0 => plain build */ + int present; + 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 — drive the #6 splice through w6c_ww (rows) AND assert the + * non-T asm is cs/ww byte-identical (rule 10: the drop must be symmetric, + * not just cstage-side — 997's byteid above only covered -T mode). */ +static int +nondrop(const char *bin) +{ + int pid = getpid(); + char wp[256], wt[256], cp[256], cmd[4096]; + snprintf(wp, sizeof wp, "/tmp/at997nd_wp_%d.s", pid); + snprintf(wt, sizeof wt, "/tmp/at997nd_wt_%d.s", pid); + snprintf(cp, sizeof cp, "/tmp/at997nd_cp_%d.s", pid); + + snprintf(cmd, sizeof cmd, + "%s/w6c_ww test/wcc/data/attest_nondrop.ww -o %s 2>/dev/null", bin, wp); + if (runwait(cmd) != 0) { + fprintf(stderr, "997 FAIL: w6c_ww non-T nondrop compile\n"); + return 1; + } + snprintf(cmd, sizeof cmd, + "%s/w6c_ww -T test/wcc/data/attest_nondrop.ww -o %s 2>/dev/null", bin, wt); + if (runwait(cmd) != 0) { + fprintf(stderr, "997 FAIL: w6c_ww -T nondrop compile\n"); + return 1; + } + snprintf(cmd, sizeof cmd, + "%s/w6c test/wcc/data/attest_nondrop.ww -o %s 2>/dev/null", bin, cp); + if (runwait(cmd) != 0) { + fprintf(stderr, "997 FAIL: w6c non-T nondrop compile\n"); + 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 ? wt : wp; + 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, "997 FAIL: w6c_ww — %s: %s %s (expected %s)\n", + nondrop_rows[i].what, nondrop_rows[i].sym, + found ? "present" : "absent", + nondrop_rows[i].present ? "present" : "absent"); + rc = 1; + break; + } + } + if (rc == 0) { + char *bc = NULL, *bw = NULL; + size_t nc = 0, nw = 0; + if (slurp(cp, &bc, &nc) < 0 || slurp(wp, &bw, &nw) < 0) { + fprintf(stderr, "997 FAIL: slurp non-T nondrop asm\n"); + rc = 1; + } else if (nc != nw || memcmp(bc, bw, nc) != 0) { + fprintf(stderr, "997 FAIL: non-T @test-drop asm differs " + "(cs %zu, ww %zu)\n", nc, nw); + rc = 1; + } + free(bc); free(bw); + } + unlink(wp); unlink(wt); unlink(cp); + return rc; +} + +/* reject_plain — `w6c_ww ` (NON-T) must exit nonzero. Pins (as + * 910 does for cstage) that wwstage type-checks a @test body before the + * #6 splice drops it: an undefined symbol is caught loud even though the + * fn never reaches codegen (checked at check.ww pass 2/3, dropped after). */ +static int +reject_plain(const char *bin, const char *fixture, const char *what) +{ + char cmd[4096]; + snprintf(cmd, sizeof cmd, "%s/w6c_ww %s -o /dev/null 2>/dev/null", + bin, fixture); + if (runwait(cmd) == 0) { + fprintf(stderr, "997 FAIL: w6c_ww (non-T) accepted %s " + "(expected reject)\n", what); + return 1; + } + return 0; +} + int main(void) { @@ -167,7 +265,11 @@ main(void) for (size_t i = 0; i < sizeof rejects / sizeof rejects[0]; i++) if (reject(bin, rejects[i].fixture, rejects[i].what) != 0) return 1; + if (nondrop(bin) != 0) return 1; + if (reject_plain(bin, "test/wcc/data/attest_undefbody.ww", + "undefined symbol in @test body") != 0) return 1; - printf("@test -T (ww_ww): run ok + cs/ww byte-id + rejects ok\n"); + printf("@test -T (ww_ww): run ok + cs/ww byte-id + rejects + " + "non-T @test drop cs/ww byte-id + checked-body (#6)\n"); return 0; } diff --git a/test/wcc/data/attest_calldropped.ww b/test/wcc/data/attest_calldropped.ww new file mode 100644 index 00000000..651f424f --- /dev/null +++ b/test/wcc/data/attest_calldropped.ww @@ -0,0 +1,18 @@ +// #6 fixture: a plain fn (the entry) CALLS a @test fn. In non-T the +// @test def is spliced out but the CALL survives — harec-faithful: the +// reference is left DANGLING and fails LOUDLY at link, never a silent +// mis-link (ref/harec/src/check.c:3941 drops the decl; the call site's +// symbol is never satisfied). Built non-T then linked: the link MUST +// fail with an undefined reference to the dropped @test symbol (910 +// linkfail probe). No -T variant — a user main is -T-rejected (910). + +package data; + +@test fn calldropped_test() void = { + let a: i32 = 1; +}; + +export fn main() i32 = { + calldropped_test(); + return 0; +}; diff --git a/test/wcc/data/attest_nondrop.ww b/test/wcc/data/attest_nondrop.ww new file mode 100644 index 00000000..68425600 --- /dev/null +++ b/test/wcc/data/attest_nondrop.ww @@ -0,0 +1,28 @@ +// #6 fixture: non-(-T) @test drop, harec-fidelity (ref/harec/src/ +// check.c:3941 — FN_TEST && !is_test is checked but not emitted). A +// normal fn must survive a non-test build; uncalled @test fns must NOT +// reach codegen. Under -T the synth entry calls the @test fns, so their +// TEXT defs reappear. No imports — compiles straight to asm in both +// modes for a TEXT-label grep (910/997 nondrop rows). +// +// Layout exercises the splice loop's unlink edges: a @test fn FIRST +// (prev==nil head-unlink), a surviving plain fn, then TWO consecutive +// @test fns (unlink-after-unlink). All three @test defs vanish non-T. + +package data; + +@test fn nondrop_test_a() void = { + let a: i32 = 1; +}; + +fn nondrop_keep() i32 = { + return 1; +}; + +@test fn nondrop_test_b() void = { + let a: i32 = 1; +}; + +@test fn nondrop_test_c() void = { + let a: i32 = 1; +}; diff --git a/test/wcc/data/attest_undefbody.ww b/test/wcc/data/attest_undefbody.ww new file mode 100644 index 00000000..514d0189 --- /dev/null +++ b/test/wcc/data/attest_undefbody.ww @@ -0,0 +1,17 @@ +// #6 fixture: a @test body is type-checked in a non-test build even +// though the fn is then spliced out. harec checks the body +// (ref/harec/src/check.c:3913) BEFORE the append_decl skip (:3941), so +// the splice is check-THEN-drop, never drop-then-skip-check. Pins that +// placement decision: an undefined symbol in the @test body must be +// caught LOUDLY in non-T (a pre-pass splice would silence it). Compiled +// WITHOUT -T (910/997 plain-reject row). + +package data; + +fn keep() i32 = { + return 1; +}; + +@test fn undefbody_test() void = { + undefined_symbol_xyz(); +};