diff --git a/Makefile b/Makefile index 772b6f47..39ade31e 100644 --- a/Makefile +++ b/Makefile @@ -420,7 +420,8 @@ MISC_WW_TARGETS = $(MISC_WW_TESTS:%=wwtest/%) # wide-rune round-trips, the c6 sep soak). Compiler/driver gates like # test/sep: they run under test-compiler. TOOL_WW_TESTS = test/tool/rejects_test.ww test/tool/driver_test.ww \ - test/tool/wwdump_test.ww test/tool/ffi_test.ww + test/tool/wwdump_test.ww test/tool/ffi_test.ww \ + test/tool/attest_test.ww TOOL_WW_TARGETS = $(TOOL_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ diff --git a/test/tool/attest_test.ww b/test/tool/attest_test.ww new file mode 100644 index 00000000..8b961cd8 --- /dev/null +++ b/test/tool/attest_test.ww @@ -0,0 +1,177 @@ +package attest_test; + +// The #6 @test DROP/KEEP/LINK emission property, driven by w6c / +// w6c_ww DIRECTLY on the import-free test/wcc/data/attest_* fixtures. +// Port of the retired native carrier test/wcc/911_attest_drop.c; +// every assertion preserved. +// +// Direct-frontend is the only host that can compare -T and non-T on +// the SAME input: `ww test` is always -T, `ww build` always non-T. +// A @test fn is spliced out of a non-test build (harec-faithful, +// ref/harec/src/check.c:3941 — checked but never emitted) and kept +// under -T (the synth entry references it); the -T synth's `test.run` +// callee stays an external CALL, so no driver resolution is needed. +// +// nondrop — attest_nondrop.ww compiled four ways (cs/ww x non-T/-T): +// the plain fn's TEXT def is present in both modes; the three @test +// TEXT defs are absent non-T and present under -T. The fixture layout +// exercises the splice loop's unlink edges (head @test, surviving +// plain fn, two consecutive @test). The presence rows run against the +// cstage .s; the cs==ww byte-id in BOTH modes (rule 10) carries them +// onto the wwstage .s by construction. +// +// undefbody — a @test body referencing an undefined symbol rejects in +// non-T on both frontends: the body is type-checked BEFORE the splice +// drops it (harec checks at :3913, drops at :3941). +// +// linkfail — non-test code CALLs a @test fn: non-T compile+assemble +// succeed per stage triple, but the link MUST fail, and the link +// stderr must name calldropped_test (non-vacuity: the failure is ON +// the dropped @test symbol — a libwwrt path drift failing every link +// would otherwise mask the property). + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("attest FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (60i64 * (time.second: i64)): time.duration; +}; + +fn runcode(dir: str, name: str, argv: []str) i32 = { + let co: testenv.commandout; + testenv.runcommand(dir, dir, name, argv, tmo(), &co); + if (co.termination != exec.termination.EXIT) { return -1; }; + return co.code; +}; + +// column-0 anchored TEXT label; the '\n' prepend counts a +// file-leading label too. +fn hastext(s: str, sym: str) bool = { + return testenv.occurrences(strings.concat("\n", s), + strings.concat("\nTEXT ", sym, ",")) > 0; +}; + +@test fn nondrop() void = { + let td: str = testenv.fresh(); + let fixture: str = strings.concat(testenv.repo(), + "/test/wcc/data/attest_nondrop.ww"); + let comps: []str = ["w6c", "w6c", "w6c_ww", "w6c_ww"]; + let testmode: []bool = [false, true, false, true]; + let names: []str = ["cp", "ct", "wp", "wt"]; + let asms: []str = ["", "", "", ""]; + let j: i32 = 0; + for (j < 4) { + let outf: str = strings.concat(td, "/", names[j], ".s"); + let av: []str = alloc([], 5u64)!; + append(av, testenv.driver(comps[j])); + if (testmode[j]) { append(av, "-T"); }; + append(av, fixture); + append(av, "-o"); + append(av, outf); + if (runcode(td, names[j], av) != 0) { + fail("nondrop", strings.concat(comps[j], + " nondrop compile failed")); + }; + asms[j] = testenv.readfile(outf); + j += 1; + }; + + let syms: []str = ["data.nondrop_keep", "data.nondrop_test_a", + "data.nondrop_test_b", "data.nondrop_test_c", + "data.nondrop_keep", "data.nondrop_test_a", + "data.nondrop_test_b", "data.nondrop_test_c"]; + let rowmode: []bool = [false, false, false, false, + true, true, true, true]; + let present: []bool = [true, false, false, false, + true, true, true, true]; + let whats: []str = ["plain fn kept non-T", "head @test dropped non-T", + "mid @test dropped non-T", "consecutive @test dropped non-T", + "plain fn kept under -T", "head @test kept under -T", + "mid @test kept under -T", "consecutive @test kept under -T"]; + let i: i32 = 0; + for (i < syms.len) { + let s: str = asms[0]; + if (rowmode[i]) { s = asms[1]; }; + if (hastext(s, syms[i]) != present[i]) { + fail("nondrop", whats[i]); + }; + i += 1; + }; + if (!testenv.same(asms[0], asms[2])) { + fail("nondrop", "non-T @test-drop asm cs!=ww (rule 10)"); + }; + if (!testenv.same(asms[1], asms[3])) { + fail("nondrop", "-T @test-keep asm cs!=ww (rule 10)"); + }; + testenv.clean(td); +}; + +@test fn undefbody() void = { + let td: str = testenv.fresh(); + let fixture: str = strings.concat(testenv.repo(), + "/test/wcc/data/attest_undefbody.ww"); + let comps: []str = ["w6c", "w6c_ww"]; + let c: i32 = 0; + for (c < 2) { + let av: []str = [testenv.driver(comps[c]), fixture, "-o", + "/dev/null"]; + if (runcode(td, strings.concat("reject_", comps[c]), av) == 0) { + fail("undefbody", strings.concat(comps[c], + " (non-T) accepted an undefined symbol in a @test body")); + }; + c += 1; + }; + testenv.clean(td); +}; + +@test fn linkfail() void = { + let td: str = testenv.fresh(); + let fixture: str = strings.concat(testenv.repo(), + "/test/wcc/data/attest_calldropped.ww"); + let rt: str = strings.concat(testenv.repo(), "/out/lib/libwwrt.a"); + let comps: []str = ["w6c", "w6c_ww"]; + let asmts: []str = ["w6a", "w6a_ww"]; + let linkts: []str = ["w6l", "w6l_ww"]; + let s: i32 = 0; + for (s < 2) { + let asmf: str = strings.concat(td, "/lf.", comps[s], ".s"); + let obj: str = strings.concat(td, "/lf.", comps[s], ".o"); + let exe: str = strings.concat(td, "/lf.", comps[s], ".exe"); + let cav: []str = [testenv.driver(comps[s]), fixture, "-o", asmf]; + if (runcode(td, strings.concat("cc_", comps[s]), cav) != 0) { + fail("linkfail", strings.concat(comps[s], + " non-T calldropped compile failed")); + }; + let aav: []str = [testenv.driver(asmts[s]), "-o", obj, asmf]; + if (runcode(td, strings.concat("as_", asmts[s]), aav) != 0) { + fail("linkfail", strings.concat(asmts[s], + " calldropped assemble failed")); + }; + let lo: testenv.commandout; + let lav: []str = [testenv.driver(linkts[s]), "-o", exe, obj, rt]; + testenv.runcommand(td, td, strings.concat("ld_", linkts[s]), lav, + tmo(), &lo); + assert(lo.termination == exec.termination.EXIT); + if (lo.code == 0) { + fail("linkfail", strings.concat(linkts[s], + " linked calldropped (expected undefined-reference to ", + "the dropped @test sym)")); + }; + if (!testenv.has(lo.stderr, "calldropped_test")) { + fail("linkfail", strings.concat(linkts[s], + " link failed but not on the dropped @test sym (masked)")); + }; + s += 1; + }; + testenv.clean(td); +}; diff --git a/test/wcc/911_attest_drop.c b/test/wcc/911_attest_drop.c deleted file mode 100644 index 64e1d021..00000000 --- a/test/wcc/911_attest_drop.c +++ /dev/null @@ -1,275 +0,0 @@ -/* - * 911_attest_drop — the #6 @test DROP/KEEP/LINK emission property, driven - * by `w6c` / `w6c_ww` DIRECTLY on import-free fixtures (task #83, M4 E2-C2b). - * - * The drop/keep behavior is a property of w6c's `-T` flag, NOT the build - * model: a @test fn is spliced out of a non-test build (harec-faithful, - * ref/harec/src/check.c:3941 — checked but never emitted) and kept under - * -T (the synth entry references it). Direct-w6c is the only host that can - * compare BOTH flags on the same input — `ww test` is always -T, `ww build` - * always non-T. The fixtures are import-free so they feed w6c with no driver - * resolution; the -T synth's `test.run` callee is left as an external CALL - * (resolved at link, never needed for the emission grep), so no combined - * unit is required. Re-hosts the 910/997 nondrop+undefbody+linkfail legs, - * which die when those gates are deleted at the M4 flip. - * - * Legs: - * nondrop — @test defs ABSENT non-T, PRESENT under -T (8 rows); the .s - * is cs==ww in BOTH modes (rule 10 — the drop is symmetric). - * undefbody — a @test body referencing an undef sym rejects in non-T: - * the body is type-checked BEFORE the splice drops it - * (harec checks at :3913, drops at :3941). Both stages. - * linkfail — non-test code CALLs a @test fn: non-T drops the def but the - * CALL survives, so the link fails on the dangling symbol — - * harec-faithful loud failure, never a silent mis-link. Both - * stages. - */ -#include -#include -#include -#include -#include - -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; -} - -/* cmp_asm — w6c and w6c_ww asm for the same input must be byte-identical - * (rule 10). Returns 0 on match. */ -static int -cmp_asm(const char *csa, const char *wsa, const char *what) -{ - char *bc = NULL, *bw = NULL; - size_t nc = 0, nw = 0; - int rc = 0; - if (slurp(csa, &bc, &nc) < 0 || slurp(wsa, &bw, &nw) < 0) { - fprintf(stderr, "911 FAIL: slurp %s asm\n", what); - rc = 1; - } else if (nc != nw || memcmp(bc, bw, nc) != 0) { - fprintf(stderr, "911 FAIL: %s asm cs!=ww (cs %zu, ww %zu)\n", - what, nc, nw); - rc = 1; - } - free(bc); free(bw); - return rc; -} - -/* - * #6: each row asserts whether a symbol's TEXT def appears in the asm emitted - * for attest_nondrop.ww in a given mode. A @test fn is spliced out of a - * non-test build (ref/harec/src/check.c:3941) and kept under -T (the synth - * entry references it); a plain fn is always emitted. The fixture layout - * exercises the splice loop's unlink edges: @test head (prev==nil), a - * surviving plain fn, then two consecutive @test fns (unlink-after-unlink). - */ -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 four ways (cs/ww × non-T/-T) straight - * to asm (no driver resolution), assert each row's TEXT def is present/absent - * on the cstage output, then byte-compare cs vs ww in BOTH modes. The - * present/absent rows run against the cstage .s alone; the cs==ww byte-id - * carries the assertion onto the wwstage .s by construction. */ -static int -nondrop(const char *bin) -{ - int pid = getpid(); - char cp[256], ct[256], wp[256], wt[256], cmd[4096]; - snprintf(cp, sizeof cp, "/tmp/at911nd_cp_%d.s", pid); - snprintf(ct, sizeof ct, "/tmp/at911nd_ct_%d.s", pid); - snprintf(wp, sizeof wp, "/tmp/at911nd_wp_%d.s", pid); - snprintf(wt, sizeof wt, "/tmp/at911nd_wt_%d.s", pid); - - int rc = 0; - struct { const char *comp; const char *flag; const char *out; } jobs[] = { - { "w6c", "", cp }, - { "w6c", "-T ", ct }, - { "w6c_ww", "", wp }, - { "w6c_ww", "-T ", wt }, - }; - for (size_t i = 0; i < sizeof jobs / sizeof jobs[0]; i++) { - snprintf(cmd, sizeof cmd, - "%s/%s %stest/wcc/data/attest_nondrop.ww -o %s 2>/dev/null", - bin, jobs[i].comp, jobs[i].flag, jobs[i].out); - if (runwait(cmd) != 0) { - fprintf(stderr, "911 FAIL: %s %snondrop compile\n", - jobs[i].comp, jobs[i].flag); - rc = 1; - } - } - - for (size_t i = 0; rc == 0 && i < sizeof nondrop_rows / sizeof nondrop_rows[0]; i++) { - const char *f = nondrop_rows[i].testmode ? ct : cp; - 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, "911 FAIL: %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; - } - } - if (rc == 0 && cmp_asm(cp, wp, "non-T @test-drop") != 0) rc = 1; - if (rc == 0 && cmp_asm(ct, wt, "-T @test-keep") != 0) rc = 1; - - unlink(cp); unlink(ct); unlink(wp); unlink(wt); - 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, "911 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). */ -static int -linkfail(const char *bin, const char *comp, const char *asmt, const char *linkt) -{ - int pid = getpid(); - char asmf[256], obj[256], exe[256], lerr[256], rt[1024], cmd[4096]; - snprintf(asmf, sizeof asmf, "/tmp/at911lf_%s_%d.s", comp, pid); - snprintf(obj, sizeof obj, "/tmp/at911lf_%s_%d.o", comp, pid); - snprintf(exe, sizeof exe, "/tmp/at911lf_%s_%d.exe", comp, pid); - snprintf(lerr, sizeof lerr, "/tmp/at911lf_%s_%d.err", comp, pid); - snprintf(rt, sizeof rt, "%s/../lib/libwwrt.a", bin); - - int rc = 0; - 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, "911 FAIL: %s non-T calldropped compile\n", comp); - rc = 1; - } - if (rc == 0) { - snprintf(cmd, sizeof cmd, "%s/%s -o %s %s 2>/dev/null", - bin, asmt, obj, asmf); - if (runwait(cmd) != 0) { - fprintf(stderr, "911 FAIL: %s calldropped\n", asmt); - rc = 1; - } - } - if (rc == 0) { - snprintf(cmd, sizeof cmd, "%s/%s -o %s %s %s 2>%s", - bin, linkt, exe, obj, rt, lerr); - if (runwait(cmd) == 0) { - fprintf(stderr, "911 FAIL: %s linked calldropped (expected " - "undefined-reference to the dropped @test sym)\n", linkt); - rc = 1; - } else { - /* Non-vacuity: the link must fail ON the dropped @test - * symbol, not for an unrelated reason — a libwwrt path drift - * would otherwise mask the property by failing every link. */ - snprintf(cmd, sizeof cmd, "grep -q calldropped_test %s", lerr); - if (runwait(cmd) != 0) { - fprintf(stderr, "911 FAIL: %s link failed but not on the " - "dropped @test sym (masked)\n", linkt); - rc = 1; - } - } - } - unlink(asmf); unlink(obj); unlink(exe); unlink(lerr); - return rc; -} - -/* Both stages: each tool triple compiles + assembles + links the same - * import-free fixtures, so the drop/keep/link property is pinned on cstage - * AND wwstage (the nondrop leg's cs==ww byte-id proves the .s identical; - * the reject + link legs re-run the whole pipeline per stage). */ -static const struct { - const char *comp; - const char *asmt; - const char *linkt; -} stages[] = { - { "w6c", "w6a", "w6l" }, - { "w6c_ww", "w6a_ww", "w6l_ww" }, -}; - -int -main(void) -{ - const char *bin = absbin(); - if (!bin) { fprintf(stderr, "911 FAIL: getcwd\n"); return 1; } - - if (nondrop(bin) != 0) return 1; - for (size_t i = 0; i < sizeof stages / sizeof stages[0]; i++) { - if (reject_plain(bin, stages[i].comp, - "test/wcc/data/attest_undefbody.ww", - "undefined symbol in @test body") != 0) return 1; - if (linkfail(bin, stages[i].comp, stages[i].asmt, - stages[i].linkt) != 0) return 1; - } - - printf("@test -T direct-w6c: non-T drop + -T keep (8 rows, cs==ww both " - "modes) + checked-body reject + dangling-call link-fail, both " - "stages (#6)\n"); - return 0; -}