diff --git a/Makefile b/Makefile index a56cd0c5..f94c8ddc 100644 --- a/Makefile +++ b/Makefile @@ -536,7 +536,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \ - $(BIN)/test_attest_record \ + $(BIN)/test_attest_record $(BIN)/test_attest_drop \ $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ $(BIN)/test_dirs_toolong_run \ @@ -2958,6 +2958,13 @@ $(BIN)/test_attest_record: test/wcc/911_attest_record.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# C2b — #6 @test drop/keep/link via direct w6c/w6c_ww on import-free fixtures +# (both stages → all six tools + libwwrt for the linkfail leg). +$(BIN)/test_attest_drop: test/wcc/911_attest_drop.c $(BIN)/w6c $(BIN)/w6c_ww \ + $(BIN)/w6a $(BIN)/w6a_ww $(BIN)/w6l $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_fmt_run: test/wcc/970_fmt_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/test/wcc/911_attest_drop.c b/test/wcc/911_attest_drop.c new file mode 100644 index 00000000..64e1d021 --- /dev/null +++ b/test/wcc/911_attest_drop.c @@ -0,0 +1,275 @@ +/* + * 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; +}