diff --git a/Makefile b/Makefile index e33946db..436c647c 100644 --- a/Makefile +++ b/Makefile @@ -419,7 +419,8 @@ MISC_WW_TARGETS = $(MISC_WW_TESTS:%=wwtest/%) # diagnostic gates, driver-CLI parity, @symbol FFI asm needles, .wwi # 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 +TOOL_WW_TESTS = test/tool/rejects_test.ww test/tool/driver_test.ww \ + test/tool/wwdump_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/wwdump_test.ww b/test/tool/wwdump_test.ww new file mode 100644 index 00000000..e8a9911d --- /dev/null +++ b/test/tool/wwdump_test.ww @@ -0,0 +1,152 @@ +package wwdump_test; + +// wwdump_ww observer gates. Ports of the retired native carriers +// test/wcc/901_asserttyped_gap.c and 989_wwdumpgate_run.c; every +// assertion preserved. WWSTAGE-ONLY by construction: the cstage +// wwdump implements only -t/-a, and the asserttyped pass lives in +// wwstage check.ww — there is no cstage twin for either claim +// (wwstage-warn-audit rule: drive the wwstage checker with *_ww +// binaries, never cstage tools). +// +// asserttyped (#15 / A.6.2.1e) — the wwstage checker's post-checker +// nil-type invariant gate is UNOBSERVED by every other suite (the +// ordinary suites drive cstage check.c; only -t/-a run elsewhere). +// Run `wwdump_ww -c` over the gap-bearing manifest and pin the count +// of line-leading "asserttyped:" stderr diagnostics per fixture; the +// manifest is the all-closed floor (every class A-G at zero), so any +// fresh nil-gap or regressed class fails loud and names its fixture. +// wwdump_ww's exit code is deliberately NOT gated: the diagnostics +// land on stderr regardless, and the armed bail flips the exit code — +// the line count is the one signal stable across the fold sequence. +// #90 sep-feed: import-bearing fixtures feed their RESOLVED sep unit +// (`ww build -S` composes .sepwork/__root.unit.ww); the +// import-free test/wcc/901_*.ww companions stay raw-fed. +// +// wwdumpgate (#52, F15 c4) — the -c and -r arms gate on parse-stage +// errors instead of silently emitting over a broken AST: parse-errored +// input under -c exits nonzero with exactly 0 stdout bytes (pre-fix: +// rc=0 with truncated asm — and -c IS the 994 byte-identity probe +// arm); -r likewise exits nonzero; the valid-file -c control exits 0 +// with asm on stdout. The C carrier's wwdump_ww-missing wiring check +// is carried by the Make prerequisite plus runcommand's launch +// assert. + +import os; +import os.exec; +import strings; +import testenv; +import time; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("wwdump FAIL: ", label, " -- ", why, + "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn tmo() time.duration = { + return (180i64 * (time.second: i64)): time.duration; +}; + +// Line-leading occurrences; the '\n' prepend counts a stream-leading +// diagnostic too. +fn countdiag(stderr: str) i32 = { + return testenv.occurrences(strings.concat("\n", stderr), + "\nasserttyped:"); +}; + +// Gap classes A-G (residual-carrier-audit.json 901 entry); want is +// the all-closed floor. sep rows compose the resolved unit first. +@test fn asserttyped() void = { + let rels: []str = [ + "lib/math/checked/checked_test.ww", + "test/wcc/data/selfhost_smoke/case.ww", + "lib/encoding/utf8/utf8.ww", + "lib/fnmatch/fnmatch_test.ww", + "lib/math/random/random_test.ww", + "test/wcc/901_enum_corpus.ww", + "test/wcc/901_forrange_tuple.ww", + "test/wcc/901_massign_blank.ww"]; + let classes: []str = [ + "A module-qual N_DOT call result", + "B fn-ptr struct-field call", + "C abort intrinsic callee", + "D module-leaf == type/fn name", + "D module-leaf == type/fn name", + "E computed enum-member value-expr", + "F for-range tuple-destructure bind", + "G tuple multi-assign discard `_`"]; + let seps: []bool = [true, true, true, true, true, false, false, + false]; + let wants: []i32 = [0, 0, 0, 0, 0, 0, 0, 0]; + let i: i32 = 0; + for (i < rels.len) { + let td: str = testenv.fresh(); + let src: str = strings.concat(testenv.repo(), "/", rels[i]); + if (seps[i]) { + let stem: str = strings.concat(td, "/unit"); + let bo: testenv.commandout; + let bav: []str = [testenv.driver("ww"), "build", "-S", "-o", + stem, src]; + testenv.runcommand(td, td, "resolve", bav, tmo(), &bo); + if (bo.termination != exec.termination.EXIT || bo.code != 0) { + fail(classes[i], strings.concat(rels[i], + ": no resolved sep unit (ww build -S failed)")); + }; + src = strings.concat(stem, ".sepwork/__root.unit.ww"); + if (!testenv.exists(src)) { + fail(classes[i], strings.concat(rels[i], + ": __root.unit.ww missing after ww build -S")); + }; + }; + let co: testenv.commandout; + let av: []str = [testenv.driver("wwdump_ww"), "-c", src]; + testenv.runcommand(td, td, "check", av, tmo(), &co); + assert(co.termination == exec.termination.EXIT); + let got: i32 = countdiag(co.stderr); + if (got != wants[i]) { + // surface the drifted diagnostics before the abort + os.write(2, co.stderr.ptr, co.stderr.len: u64); + fail(classes[i], strings.concat(rels[i], + ": asserttyped count drifted from the manifest")); + }; + testenv.clean(td); + i += 1; + }; +}; + +fn dumprow(td: str, name: str, mode: str, src: str, + out: *testenv.commandout) void = { + let av: []str = [testenv.driver("wwdump_ww"), mode, src]; + testenv.runcommand(td, td, name, av, tmo(), out); + assert(out.termination == exec.termination.EXIT); +}; + +@test fn wwdumpgate() void = { + let td: str = testenv.fresh(); + let badp: str = strings.concat(td, "/bad.ww"); + testenv.writefile(badp, strings.concat( + "package main;\n\n", + "export fn main(argc: i32, argv: **u8) i32 = {\n", + "\treturn 0;\n};\n\n", + "fn broken( {\n")); + let okp: str = strings.concat(td, "/ok.ww"); + testenv.writefile(okp, + "package main;\nexport fn main() i32 = { return 0; };\n"); + + let co: testenv.commandout; + dumprow(td, "bad_c", "-c", badp, &co); + if (co.code == 0 || co.stdout.len != 0) { + fail("bad_c", + "asm emitted or rc=0 on parse-errored input (#52)"); + }; + dumprow(td, "bad_r", "-r", badp, &co); + if (co.code == 0) { + fail("bad_r", "resolve report with rc=0 on parse-errored input"); + }; + dumprow(td, "ok_c", "-c", okp, &co); + if (co.code != 0 || co.stdout.len == 0) { + fail("ok_c", "healthy file rejected or produced no asm"); + }; + testenv.clean(td); +}; diff --git a/test/wcc/901_asserttyped_gap.c b/test/wcc/901_asserttyped_gap.c deleted file mode 100644 index 2411ae13..00000000 --- a/test/wcc/901_asserttyped_gap.c +++ /dev/null @@ -1,241 +0,0 @@ -/* - * 901_asserttyped_gap — ww-stage checker nil-type gap-corpus net. - * - * The ordinary suites drive the CSTAGE `ww` (cmd/ check.c), which has - * no asserttyped pass, and nothing else feeds the ww-stage dumper's - * checker (-c) — only -t/-a (tokens/ast) run elsewhere. So the - * wwstage checker's asserttyped diagnostics (check.ww — the - * #15 / A.6.2.1e post-checker nil-type invariant gate) are UNOBSERVED - * by every other test, yet the gate is now ARMED (a non-exempt - * nil-typed value node writes its diagnostic and os.exit(1)s). This - * probe is the visible counterpart: it counts the per-file diagnostics - * without gating on the bail's exit so a regression names its class. - * - * This probe runs the wwstage checker (wwdump_ww -c) over the - * gap-bearing corpus, counts the per-file asserttyped diagnostics on - * stderr, and pins each count against the manifest below. A fresh - * nil-gap (count up) or a regressed fixed class (count up from 0) fails - * loud; a fold that closes a class drives its count down, which fails - * until the manifest is edited to match. Every class has reached - * all-zero, so the manifest is now the all-closed floor that holds the - * armed bail green. - * - * #90 sep-feed: the E3 flip retired the combined.ww amalgamator, so the - * library/selfhost gap fixtures (A-D) feed their RESOLVED sep unit (see - * resolveunit — `ww build -S` composes .sepwork/__root.unit.ww - * from the dep `.wwi` stubs + the root body) rather than a pre-built - * .combined.ww. The single-file, import-free test fixtures (E-G) - * carry no imports and stay raw-fed. - * - * Gap classes (counts verified empirically at this revision): - * A module-qual N_DOT call result checked_test 0 (closed) - * B fn-ptr struct-field call smoke 0 (closed) - * C abort intrinsic callee utf8 0 (exempt) - * D module-leaf == type/fn name fnmatch 0 (closed) - * D module-leaf == type/fn name random 0 (closed) - * E computed enum-member value-expr enum_corpus 0 (closed) - * F for-range tuple-destructure bind forrange_tuple 0 (closed) - * G tuple multi-assign discard `_` massign_blank 0 (closed) - * - * Exit code of wwdump_ww is intentionally not gated: the diagnostics - * land on stderr regardless of the run's success, and arming the bail - * will itself flip that exit code — the stderr line count is the one - * signal stable across the whole fold sequence. - */ -#include -#include -#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; -} - -/* resolveunit — compose 's RESOLVED translation unit the way the - * sep driver does. `ww build -S -o ` reads the fixture and its - * sibling package sources in place while putting caller-owned compiler - * artifacts below the private workspace, then this probe reads the exact - * .sepwork/__root.unit.ww artifact — - * the single self-contained unit w6c/wwdump consume for the root. Feeding the - * raw module file instead would leave its import refs (os/fmt/strconv …) - * unresolved, a partial unit the asserttyped invariant must never see. - * Returns the unit path in `out`, 0 on success, and -1 otherwise. */ -static int -resolveunit(const char *bin, const char *cwd, const char *fixture, - const char *work, char *out, size_t outsz) -{ - char cmd[4096], src[2048], stem[2048]; - snprintf(src, sizeof src, "%s/%s", cwd, fixture); - snprintf(stem, sizeof stem, "%s/unit", work); - snprintf(cmd, sizeof cmd, - "timeout 180 %s/ww build -S -o %s %s >/dev/null 2>&1", - bin, stem, src); - if (runwait(cmd) != 0) return -1; - snprintf(out, outsz, "%s.sepwork/__root.unit.ww", stem); - return access(out, 0) == 0 ? 0 : -1; -} - -/* Count line-leading "asserttyped:" diagnostics; each warn is one such - * line and no other wwdump output carries the prefix. */ -static int -count_asserttyped(const char *path) -{ - char *b = NULL; - size_t n = 0; - if (slurp(path, &b, &n) < 0) return -1; - const char *needle = "asserttyped:"; - size_t nl = strlen(needle); - int c = 0; - for (size_t i = 0; i + nl <= n; i++) { - if ((i == 0 || b[i - 1] == '\n') && memcmp(b + i, needle, nl) == 0) - c++; - } - free(b); - return c; -} - -int -main(void) -{ - const char *bin = absbin(); - if (!bin) return 1; - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - - /* sep=1 fixtures feed their resolved sep unit (resolveunit); sep=0 - * fixtures are import-free single files fed raw. */ - struct { const char *rel; const char *cls; int want; int sep; } manifest[] = { - { "lib/math/checked/checked_test.ww", - "A module-qual N_DOT call result", 0, 1 }, - { "test/wcc/data/selfhost_smoke/case.ww", - "B fn-ptr struct-field call", 0, 1 }, - { "lib/encoding/utf8/utf8.ww", - "C abort intrinsic callee", 0, 1 }, - { "lib/fnmatch/fnmatch_test.ww", - "D module-leaf == type/fn name", 0, 1 }, - { "lib/math/random/random_test.ww", - "D module-leaf == type/fn name", 0, 1 }, - { "test/wcc/901_enum_corpus.ww", - "E computed enum-member value-expr", 0, 0 }, - { "test/wcc/901_forrange_tuple.ww", - "F for-range tuple-destructure bind", 0, 0 }, - { "test/wcc/901_massign_blank.ww", - "G tuple multi-assign discard `_`", 0, 0 }, - { NULL, NULL, 0, 0 }, - }; - - int fail = 0, n = 0; - for (int i = 0; manifest[i].rel; i++) { - char work[] = "/tmp/atgap_XXXXXX"; - char src[2048], errf[256], cmd[4096], unit[1280]; - char stem[256], scratch[288]; - int rowfail = 0; - n++; - if (mkdtemp(work) == NULL) { - fprintf(stderr, "asserttyped_gap FAIL: %s [%s] " - "workspace acquisition failed\n", - manifest[i].rel, manifest[i].cls); - fail++; - continue; - } - snprintf(stem, sizeof stem, "%s/unit", work); - snprintf(scratch, sizeof scratch, "%s.sepwork", stem); - snprintf(errf, sizeof errf, "%s/asserttyped.err", work); - if (manifest[i].sep) { - if (resolveunit(bin, cwd, manifest[i].rel, work, unit, - sizeof unit) != 0) { - fprintf(stderr, "asserttyped_gap FAIL: %s [%s] " - "no resolved sep unit\n", - manifest[i].rel, manifest[i].cls); - rowfail = 1; - goto row_done; - } - snprintf(src, sizeof src, "%s", unit); - } else { - snprintf(src, sizeof src, "%s/%s", cwd, manifest[i].rel); - } - snprintf(cmd, sizeof cmd, - "timeout 180 %s/wwdump_ww -c %s >/dev/null 2>%s", - bin, src, errf); - runwait(cmd); - int got = count_asserttyped(errf); - if (got != manifest[i].want) { - fprintf(stderr, - "asserttyped_gap FAIL: %s [%s] expected %d, got %d\n", - manifest[i].rel, manifest[i].cls, - manifest[i].want, got); - char dump[4096]; - snprintf(dump, sizeof dump, - "grep '^asserttyped:' %s 1>&2", errf); - runwait(dump); - rowfail = 1; - } - - row_done: - { - int cleanfail = 0; - if (unlink(errf) != 0 && errno != ENOENT) cleanfail = 1; - snprintf(cmd, sizeof cmd, "rm -rf %s", scratch); - if (runwait(cmd) != 0) cleanfail = 1; - if (unlink(stem) != 0 && errno != ENOENT) cleanfail = 1; - if (rmdir(work) != 0) cleanfail = 1; - if (cleanfail) { - fprintf(stderr, "asserttyped_gap FAIL: %s [%s] " - "workspace cleanup failed\n", - manifest[i].rel, manifest[i].cls); - rowfail = 1; - } - } - if (rowfail) fail++; - } - - if (fail) { - fprintf(stderr, - "asserttyped_gap: %d/%d fixture(s) drifted from manifest\n", - fail, n); - return 1; - } - printf("asserttyped_gap: ww-stage checker diagnostic set matches manifest " - "on %d gap-corpus fixtures (A-G all closed; bail armed)\n", n); - return 0; -} diff --git a/test/wcc/989_wwdumpgate_run.c b/test/wcc/989_wwdumpgate_run.c deleted file mode 100644 index 9a072a6d..00000000 --- a/test/wcc/989_wwdumpgate_run.c +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 989_wwdumpgate_run (#52, F15 c4) — wwdump_ww's `-c` and `-r` arms must gate - * on parse-stage errors instead of silently emitting on a broken AST. - * - * THE BUG (wwstage wwdump_ww only, cat-A silent wrong output under rc=0): the - * `-c` codegen arm ran checkfile+cgfile with NO parse-error gate — ps.errs - * was never read — so a parse-errored decl was silently dropped from the AST - * and the rest of the file compiled to asm with exit 0 (and `-c` IS the 994 - * byte-identity probe, so the gate tool itself could ship wrong asm silently). - * The `-r` resolve report arm was likewise ungated. THE FIX: add the - * `l.errs>0 || ps.errs>0` gate after parsefile in both arms, mirroring the - * w6c compiler gate (selfhost/cmd/w6c/main.ww:162 / cmd/w6c/main.c). - * - * WWSTAGE-ONLY: the C wwdump (cstage) implements only -t/-a, not -c/-r — the - * codegen/resolve dump arms are a wwstage-wwdump_ww feature, so there is no - * cstage -c/-r twin to diff against; the gate reference is the w6c compiler. - * - * row | mode | input | result - * -------+------+---------------+---------------------------------- - * bad_c | -c | parse error | rc != 0, zero asm bytes - * bad_r | -r | parse error | rc != 0 - * ok_c | -c | valid file | rc == 0, asm emitted (control) - * - * bad_c / bad_r were RED pre-c4 (rc=0 with truncated asm / a resolve report). - * ok_c pins the healthy emission path unperturbed. - */ -#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 *BAD = - "package main;\n\n" - "export fn main(argc: i32, argv: **u8) i32 = {\n\treturn 0;\n};\n\n" - "fn broken( {\n"; -static const char *OK = - "package main;\nexport fn main() i32 = { return 0; };\n"; - -/* Run ` ` capturing asm on stdout; returns rc, sets - * *asmbytes to the stdout byte count when non-NULL. */ -static int -run_dump(const char *tool, const char *mode, const char *src, long *asmbytes) -{ - char outp[128], cmd[1024]; - snprintf(outp, sizeof outp, "/tmp/wwdg_%d_out", getpid()); - snprintf(cmd, sizeof cmd, "%s %s %s > %s 2>/dev/null", - tool, mode, src, outp); - int rc = runwait(cmd); - if (asmbytes) { - FILE *f = fopen(outp, "rb"); - long n = 0; - if (f) { fseek(f, 0, SEEK_END); n = ftell(f); fclose(f); } - *asmbytes = n; - } - unlink(outp); - return rc; -} - -static int -write_file(const char *path, const char *body) -{ - FILE *f = fopen(path, "wb"); - if (!f) return -1; - fputs(body, f); - fclose(f); - return 0; -} - -int -main(void) -{ - const char *bin = getenv("BIN"); - if (!bin) bin = "out/bin"; - char absbin[1024]; - if (bin[0] != '/') { - char cwd[1024]; - if (getcwd(cwd, sizeof cwd) == NULL) return 1; - snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); - bin = absbin; - } - - char wtool[1024]; - snprintf(wtool, sizeof wtool, "%s/wwdump_ww", bin); - if (access(wtool, X_OK) != 0) { - /* wwdump_ww is the unit under test; absence is a wiring fault. */ - fprintf(stderr, "wwdumpgate: missing %s\n", wtool); - return 1; - } - - char badp[64], okp[64]; - snprintf(badp, sizeof badp, "/tmp/wwdg_bad_%d.ww", getpid()); - snprintf(okp, sizeof okp, "/tmp/wwdg_ok_%d.ww", getpid()); - if (write_file(badp, BAD) || write_file(okp, OK)) return 1; - - int fail = 0, total = 0; - long ab; - - /* bad_c: loud, zero asm */ - total++; - if (run_dump(wtool, "-c", badp, &ab) == 0 || ab != 0) { - fprintf(stderr, "wwdumpgate[bad_c]: emitted asm with rc=0 on " - "parse-errored input (#52); asm=%ld\n", ab); - fail++; - } - - /* bad_r: loud */ - total++; - if (run_dump(wtool, "-r", badp, NULL) == 0) { - fprintf(stderr, "wwdumpgate[bad_r]: resolve report with rc=0 on " - "parse-errored input (#52)\n"); - fail++; - } - - /* ok_c control: rc=0 with asm */ - total++; - if (run_dump(wtool, "-c", okp, &ab) != 0 || ab <= 0) { - fprintf(stderr, "wwdumpgate[ok_c]: healthy file rejected/no asm " - "(asm=%ld)\n", ab); - fail++; - } - - unlink(badp); unlink(okp); - - if (fail) { - fprintf(stderr, "wwdumpgate_run: %d/%d check(s) failed\n", fail, total); - return 1; - } - printf("wwdumpgate_run: %d/%d ok\n", total, total); - return 0; -}