diff --git a/Makefile b/Makefile index 3a05d3db..723d3d6a 100644 --- a/Makefile +++ b/Makefile @@ -422,6 +422,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_crc32_run $(BIN)/test_crc64_run \ $(BIN)/test_siphash_run $(BIN)/test_sha256_run \ $(BIN)/test_regex_run \ + $(BIN)/test_lib_byteid \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ $(BIN)/test_deref_narrow_run \ @@ -1790,6 +1791,10 @@ $(BIN)/test_regex_run: test/wcc/989_regex_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_lib_byteid: test/wcc/989_lib_byteid.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_bufio_run: test/wcc/998_bufio_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c new file mode 100644 index 00000000..f3d9e6e2 --- /dev/null +++ b/test/wcc/989_lib_byteid.c @@ -0,0 +1,408 @@ +/* + * 989_lib_byteid — cstage-vs-wwstage byte-identity gate over the lib/ + * surface that the embedded combined.ww set does NOT cover. + * + * The 990-997 gates byte-id only the modules the selfhost tools import + * (ascii/bytes/errors/io/math/memio/os/rt/strconv/strings/time/types/ + * utf8 + lex/parse/wcc/ww). Every other lib/ module compiles through + * the CSTAGE driver alone (the 9xx _run tests), so a cs≠ww divergence + * there ships gate-green — regex.finish was cs≠ww for weeks this way + * (task #21, FC0). This gate closes the class: each lib test fixture's + * resolved unit (driver-shaped .combined.ww, cf 990 resolveunit) is + * compiled through BOTH w6c and w6c_ww and the asm byte-compared. + * + * Three pinned outcomes, loud over blind (task #21 ruling): + * ID — byte-identical, zero tolerance. + * DIVERGE — known cs≠ww divergence, documented in task #59 with the + * first-diff symbol. Both stages must still compile and + * the outputs must still DIFFER: when a fix lands, the + * entry fails loudly demanding graduation to ID rather + * than silently widening or narrowing coverage. + * WWREJECT — w6c compiles, w6c_ww errors (front-end gap, task #59). + * Graduation is pinned the same way. + * + * Modules with no test fixture and no embedding (sort/path/endian/net/ + * hash/fnv/crypto.math/c.libc) are covered via import-probe stubs: + * `import` drags the whole module into the combined unit, so both + * cgens chew the full module body. The driver silently skips an + * unresolvable import, so each probe carries a sentinel proving the + * body landed; and a corpus-completeness scan fails loudly on any + * lib/ dir not enrolled here, so new modules can't ship uncovered. + * + * Compile+cmp only — no driver run, no source-tree writes (everything + * lives in a getpid-keyed /tmp dir), so this is phase-1 parallel-safe + * next to the 9xx _run tests that write stem artifacts in-tree. + * + * 9xx is full; shares the 989 prefix per the 989_sha256 precedent (the + * `short` name keys the binary). + */ +#include +#include +#include +#include +#include +#include + +enum { M_ID, M_DIVERGE, M_WWREJECT }; + +struct ent { + const char *fixture; /* repo-relative .ww; NULL → probe entry */ + const char *probe; /* inline import-probe source */ + const char *inc; /* extra -I dir beyond dirname(fixture) */ + int mode; + const char *cite; /* task #59 entry for non-ID modes */ + /* probe-only: the driver SILENTLY SKIPS an unresolvable import + * (cmd/ww/main.c locate_import), so a probe whose module fails + * to resolve shrinks to an empty main and byte-id's trivially — + * green while covering nothing. sentinel must appear in the + * resolved unit to prove the module body actually landed. + * (Fixtures self-protect: they call module symbols, so a drop + * is a loud w6c reject.) moddir feeds the completeness check. */ + const char *sentinel; + const char *moddir; +}; + +static const struct ent ents[] = { + /* -------- byte-identical: zero tolerance ----------------- */ + { .fixture = "lib/bytes/bytestest.ww", .mode = M_ID }, + { .fixture = "lib/dirs/dirstest.ww", .mode = M_ID }, + { .fixture = "lib/encoding/base32/base32_test.ww", .mode = M_ID }, + { .fixture = "lib/encoding/utf8/utf8test.ww", .mode = M_ID }, + { .fixture = "lib/getopt/getopttest.ww", .mode = M_ID }, + { .fixture = "lib/hash/adler32/adler32_test.ww", .mode = M_ID }, + { .fixture = "lib/hash/crc16/crc16_test.ww", .mode = M_ID }, + { .fixture = "lib/hash/crc32/crc32_test.ww", .mode = M_ID }, + { .fixture = "lib/hash/crc64/crc64_test.ww", .mode = M_ID }, + { .fixture = "lib/math/checked/checked_test.ww", .mode = M_ID }, + { .fixture = "lib/math/random/random_test.ww", .mode = M_ID }, + { .fixture = "lib/memio/memiotest.ww", .mode = M_ID }, + { .fixture = "lib/os/ostest.ww", .mode = M_ID }, + { .fixture = "lib/regex/regex_test.ww", .mode = M_ID }, + { .fixture = "lib/strconv/test/decimaltest.ww", .mode = M_ID }, + { .fixture = "lib/strconv/test/ftostest.ww", .mode = M_ID }, + { .fixture = "lib/strconv/test/inttest.ww", .mode = M_ID }, + { .fixture = "lib/strings/stringstest.ww", .mode = M_ID }, + { .fixture = "lib/temp/temptest.ww", .mode = M_ID }, + { .fixture = "lib/time/timetest.ww", .mode = M_ID }, + /* fixtureless modules, import-probe shape */ + { .probe = "package main;\nimport sort;\nfn main() i32 = { return 0; };\n", + .mode = M_ID, .sentinel = "package sort;", .moddir = "lib/sort" }, + { .probe = "package main;\nimport path;\nfn main() i32 = { return 0; };\n", + .mode = M_ID, .sentinel = "package path;", .moddir = "lib/path" }, + { .probe = "package main;\nimport endian;\nfn main() i32 = { return 0; };\n", + .mode = M_ID, .sentinel = "package endian;", .moddir = "lib/endian" }, + { .probe = "package main;\nimport net;\nfn main() i32 = { return 0; };\n", + .mode = M_ID, .sentinel = "package net;", .moddir = "lib/net" }, + { .probe = "package main;\nimport hash;\nfn main() i32 = { return 0; };\n", + .mode = M_ID, .sentinel = "package hash;", .moddir = "lib/hash" }, + /* fnv lives off the driver's default root (lib/hash/fnv) */ + { .probe = "package main;\nimport fnv;\nfn main() i32 = { return 0; };\n", + .inc = "lib/hash/fnv", + .mode = M_ID, .sentinel = "package fnv;", .moddir = "lib/hash/fnv" }, + /* sentinel is a fn, not `package math;` — lib/math is also + * package math, so a silent fallback there would still match */ + { .probe = "package main;\nimport crypto.math;\nfn main() i32 = { return 0; };\n", + .mode = M_ID, .sentinel = "fn rotl32", .moddir = "lib/crypto/math" }, + { .probe = "package main;\nimport c.libc;\nfn main() i32 = { return 0; };\n", + .mode = M_ID, .sentinel = "package libc;", .moddir = "lib/c/libc" }, + /* -------- documented-allowed cs≠ww (task #59) ------------ */ + { .fixture = "lib/ascii/asciitest.ww", + .mode = M_DIVERGE, .cite = "#59.1" }, + { .fixture = "lib/bufio/bufiotest.ww", + .mode = M_DIVERGE, .cite = "#59.2" }, + { .fixture = "lib/encoding/base64/base64_test.ww", + .mode = M_DIVERGE, .cite = "#59.3" }, + { .fixture = "lib/encoding/hex/hextest.ww", + .mode = M_DIVERGE, .cite = "#59.4" }, + { .fixture = "lib/errors/errnotest.ww", + .mode = M_DIVERGE, .cite = "#59.5" }, + { .fixture = "lib/fmt/fmttest.ww", + .mode = M_DIVERGE, .cite = "#59.6" }, + { .fixture = "lib/hash/siphash/siphash_test.ww", + .mode = M_DIVERGE, .cite = "#59.7" }, + { .fixture = "lib/log/logtest.ww", + .mode = M_DIVERGE, .cite = "#59.8" }, + { .fixture = "lib/os/stattest.ww", + .mode = M_DIVERGE, .cite = "#59.9" }, + { .fixture = "lib/strconv/test/stoftest.ww", + .mode = M_DIVERGE, .cite = "#59.10" }, + { .fixture = "lib/ww/lex/toktest.ww", + .mode = M_DIVERGE, .cite = "#59.11" }, + /* asttest resolves `import tok` via -I lib/ww/lex, cf 905 */ + { .fixture = "lib/ww/asttest.ww", .inc = "lib/ww/lex", + .mode = M_DIVERGE, .cite = "#59.12" }, + /* -------- wwstage front-end gaps (task #59) -------------- */ + { .fixture = "lib/crypto/sha256/sha256_test.ww", + .mode = M_WWREJECT, .cite = "#59.13" }, + { .fixture = "lib/fnmatch/fnmatchtest.ww", + .mode = M_WWREJECT, .cite = "#59.14" }, + { .fixture = "lib/shlex/shlextest.ww", + .mode = M_WWREJECT, .cite = "#59.15" }, + { .fixture = NULL }, +}; + +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; +} + +static int +check_one(const char *bin, const char *cwd, const struct ent *e, int idx) +{ + const char *label = e->fixture ? e->fixture : "import-probe"; + char td[64], cmd[4096]; + int rc = -1; + + snprintf(td, sizeof td, "/tmp/wwlbi_%d_%d", getpid(), idx); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + char base[256]; + if (e->fixture) { + const char *b = strrchr(e->fixture, '/'); + snprintf(base, sizeof base, "%s", b ? b + 1 : e->fixture); + snprintf(cmd, sizeof cmd, "cp %s/%s %s/%s", cwd, e->fixture, + td, base); + if (runwait(cmd) != 0) { + fprintf(stderr, "lib_byteid FAIL: %s — cp\n", label); + goto out; + } + } else { + snprintf(base, sizeof base, "probe.ww"); + char p[512]; + snprintf(p, sizeof p, "%s/probe.ww", td); + FILE *f = fopen(p, "wb"); + if (!f) goto out; + fputs(e->probe, f); + fclose(f); + label = e->moddir; + } + + /* dirname(fixture) leads the search path so bare same-module + * imports (`import base32` from lib/encoding/base32) resolve as + * they do under the in-tree driver run; resolveunit's bare-copy + * shape loses that and mis-drops modules (silent expand skip). */ + char incs[2048] = ""; + if (e->fixture) { + const char *slash = strrchr(e->fixture, '/'); + int n = (int)(slash - e->fixture); + snprintf(incs, sizeof incs, "-I %s/%.*s", cwd, n, e->fixture); + } + if (e->inc) { + size_t n = strlen(incs); + snprintf(incs + n, sizeof incs - n, " -I %s/%s", cwd, e->inc); + } + + /* exit deliberately ignored: the combined unit is written before + * codegen (cf 990 resolveunit) and only it matters here. */ + snprintf(cmd, sizeof cmd, + "cd %s && timeout 180 %s/ww build %s %s >/dev/null 2>&1", + td, bin, incs, base); + runwait(cmd); + + char comb[512]; + snprintf(comb, sizeof comb, "%s/%s", td, base); + char *dot = strrchr(comb, '.'); + if (dot) *dot = '\0'; + size_t cn = strlen(comb); + snprintf(comb + cn, sizeof comb - cn, ".combined.ww"); + if (access(comb, 0) != 0) { + fprintf(stderr, "lib_byteid FAIL: %s — no resolved unit\n", label); + goto out; + } + if (e->sentinel) { + char *cb = NULL; + size_t cl = 0; + if (slurp(comb, &cb, &cl) < 0) { + fprintf(stderr, "lib_byteid FAIL: %s — cannot read " + "resolved unit\n", label); + goto out; + } + int hit = strstr(cb, e->sentinel) != NULL; + free(cb); + if (!hit) { + fprintf(stderr, "lib_byteid FAIL: %s — import " + "silently dropped (no \"%s\" in resolved unit); " + "probe covers nothing\n", label, e->sentinel); + goto out; + } + } + + char cs[512], ws[512]; + snprintf(cs, sizeof cs, "%s/c.s", td); + snprintf(ws, sizeof ws, "%s/w.s", td); + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c %s > %s 2>/dev/null", + bin, comb, cs); + if (runwait(cmd) != 0) { + fprintf(stderr, "lib_byteid FAIL: %s — w6c rejected\n", label); + goto out; + } + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c_ww -o %s %s >/dev/null 2>&1", + bin, ws, comb); + int wrc = runwait(cmd); + + if (e->mode == M_WWREJECT) { + if (wrc == 0) { + fprintf(stderr, "lib_byteid FAIL: %s — w6c_ww now " + "compiles this; graduate the %s pin to ID or " + "DIVERGE\n", label, e->cite); + goto out; + } + rc = 0; + goto out; + } + if (wrc != 0) { + fprintf(stderr, "lib_byteid FAIL: %s — w6c_ww rejected\n", label); + goto out; + } + + char *bc = NULL, *bw = NULL; + size_t nc = 0, nw = 0; + if (slurp(cs, &bc, &nc) < 0 || slurp(ws, &bw, &nw) < 0) { + fprintf(stderr, "lib_byteid FAIL: %s — cannot read .s\n", label); + free(bc); free(bw); + goto out; + } + int same = (nc == nw && memcmp(bc, bw, nc) == 0); + free(bc); free(bw); + + if (e->mode == M_ID && !same) { + fprintf(stderr, "lib_byteid FAIL: %s — cs %zu vs ww %zu bytes " + "(byte-id broken)\n", label, nc, nw); + goto out; + } + if (e->mode == M_DIVERGE && same) { + fprintf(stderr, "lib_byteid FAIL: %s — now byte-identical; " + "graduate the %s pin to ID\n", label, e->cite); + goto out; + } + rc = 0; +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + return rc; +} + +/* every lib/ dir holding .ww source must be accounted for — enrolled + * in ents[] (fixture dirname or probe moddir) or on the covered[] + * list below; otherwise a new module ships with zero byte-id coverage + * and the corpus rots silently as lib/ grows. */ +static const char *covered[] = { + /* selfhost-embedded: byte-id'd by the 990-997 gates */ + "lib/io", "lib/math", "lib/rt", "lib/types", "lib/ww/parse", + /* module body dragged into the lib/strconv/test fixtures */ + "lib/strconv", + NULL, +}; + +static int +accounted(const char *dir) +{ + for (int i = 0; ents[i].fixture || ents[i].probe; i++) { + if (ents[i].fixture) { + const char *s = strrchr(ents[i].fixture, '/'); + size_t n = (size_t)(s - ents[i].fixture); + if (strlen(dir) == n && + strncmp(dir, ents[i].fixture, n) == 0) + return 1; + } else if (ents[i].moddir && strcmp(dir, ents[i].moddir) == 0) + return 1; + } + for (int i = 0; covered[i]; i++) + if (strcmp(dir, covered[i]) == 0) + return 1; + return 0; +} + +static int +corpus_complete(const char *cwd) +{ + char cmd[2048]; + snprintf(cmd, sizeof cmd, "cd %s && find lib -name '*.ww' " + "! -name '*.combined.ww' | sed 's|/[^/]*$||' | sort -u", cwd); + FILE *p = popen(cmd, "r"); + if (!p) return -1; + char dir[512]; + int bad = 0, seen = 0; + while (fgets(dir, sizeof dir, p)) { + char *nl = strchr(dir, '\n'); + if (nl) *nl = '\0'; + seen++; + if (!accounted(dir)) { + fprintf(stderr, "lib_byteid FAIL: %s — un-enrolled " + "lib module; add it to the 989 corpus\n", dir); + bad++; + } + } + pclose(p); + /* an empty scan means find itself broke — never pass on that */ + if (seen == 0) { + fprintf(stderr, "lib_byteid FAIL: lib/ scan found nothing\n"); + return -1; + } + return bad ? -1 : 0; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + int fail = 0, n = 0, nid = 0, ndiv = 0, nrej = 0; + if (corpus_complete(cwd) != 0) fail++; + for (int i = 0; ents[i].fixture || ents[i].probe; i++) { + if (check_one(bin, cwd, &ents[i], i) != 0) fail++; + else if (ents[i].mode == M_ID) nid++; + else if (ents[i].mode == M_DIVERGE) ndiv++; + else nrej++; + n++; + } + if (fail) { + fprintf(stderr, "lib_byteid: %d/%d entr(ies) failed\n", fail, n); + return 1; + } + printf("lib_byteid: %d byte-identical, %d pinned-divergent (#59), " + "%d pinned-wwreject (#59) across %d lib units\n", + nid, ndiv, nrej, n); + return 0; +}