diff --git a/Makefile b/Makefile index 317906fc..500b3726 100644 --- a/Makefile +++ b/Makefile @@ -356,7 +356,14 @@ BYTEID_WRAPPER_SOURCES = test/wcc/753_convwrap_audit.c \ test/wcc/946_structret_run.c \ test/wcc/989_m1mangle_sym.c \ test/wcc/989_m2wwi_run.c \ - test/wcc/989_wwileaf_run.c test/wcc/989_lib_byteid.c + test/wcc/989_wwileaf_run.c + +# Ww-native byte/artifact observers: single-file ww tests under +# test/byteid/ on the test/testenv helper package. Each drives both +# driver stages itself, so the build needs only cstage plus the two +# stage frontends it launches. +BYTEID_WW_TESTS = test/byteid/libbyteid_test.ww +BYTEID_WW_TARGETS = $(BYTEID_WW_TESTS:%=wwtest/%) BOOTSTRAP_WRAPPER_SOURCES = test/wcc/950_selfcheck.c \ test/wcc/991_w6a_ww.c \ test/wcc/992_w6l_ww.c test/wcc/993_ww_ww.c \ @@ -455,6 +462,14 @@ $(LIBRARY_TEST_TARGETS): wwtest/%: $(BIN)/ww \ @$(BIN)/ww test -w $(WWBUILD)/wwtest/$(subst /,_,$*) \ -I $(CURDIR)/lib/ww $* +$(BYTEID_WW_TARGETS): wwtest/%: $(BIN)/ww $(BIN)/w6c $(BIN)/w6a \ + $(BIN)/w6l $(LIB)/libwwrt.a $(BIN)/ww_ww $(BIN)/w6c_ww + @echo "ww test $*" + @mkdir -p $(WWBUILD)/wwtest/$(subst /,_,$*) + @WW_TEST_REPO=$(CURDIR) $(BIN)/ww test \ + -w $(WWBUILD)/wwtest/$(subst /,_,$*) \ + -I $(CURDIR)/test $* + test-lang: $(LANG_TEST_TARGETS) test-library: $(LIBRARY_TEST_TARGETS) $(BIN)/w6c $(BIN)/w6c_ww \ @@ -617,7 +632,8 @@ test-data-byteid: $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww fi; \ exit $$rc -test-byteid: test-lang-byteid test-data-byteid test-native-byteid +test-byteid: test-lang-byteid test-data-byteid test-native-byteid \ + $(BYTEID_WW_TARGETS) # Incremental == clean. The -w reuse path must be byte-identical to a # from-scratch build, or it is not a build-graph optimization but a @@ -821,7 +837,7 @@ nocc: .PHONY: all cstage wwstage wwfixture test-wwfixture-process test-wwfixture \ test test-unit test-compiler-smoke test-compiler test-package \ test-lang test-library test-commit test-lang-byteid \ - $(LANG_TEST_TARGETS) $(LIBRARY_TEST_TARGETS) \ + $(LANG_TEST_TARGETS) $(LIBRARY_TEST_TARGETS) $(BYTEID_WW_TARGETS) \ test-data-byteid test-native-byteid test-byteid \ test-bootstrap-native test-bootstrap test-incremental \ test-platform test-all install clean bootstrap nocc bootstrap-snapshot diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index ad1110d4..c0879619 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -36,8 +36,8 @@ The live declarative compiler corpus has 1,495 fixtures and 2,990 C/WW cells: 338 expected rejections (314 shared and 24 stage-specific), 17 compile-only successes, 198 exit-zero programs, and 942 explicit-exit programs. -134 native C carriers remain. They are partitioned exactly once as five -in-process units, 11 byte/artifact gates, six bootstrap gates, one platform +133 native C carriers remain. They are partitioned exactly once as five +in-process units, 10 byte/artifact gates, six bootstrap gates, one platform gate, and 111 residual compiler, package-layout, ABI, diagnostic-observer, driver, linker, or FFI gates. Rows migrated to fixtures or native `@test` owners were removed from those carriers; there is no compatibility execution @@ -46,11 +46,15 @@ was a cstage-vs-wwstage `.s` byte-compare of sources now living in the corpus were retired into the blanket `test-data-byteid` comparator (the last 13 had their remaining inline sources added as fixtures first); the survivors observe something the blanket cannot: assembly patterns, symbol -tables, `.wwi` round-trips, or the wwstage-driver leg. Wwstage-driver-leg byte identity (`ww_ww` versus `ww` -over the emitted `.s` set) has one owner, `989_lib_byteid`, whose unit -sweep spans lib test fixtures, import probes, and a zero-dep root-only -build; the former 815/940/951 driver-parity carriers were folded into it, -their content identity already owned by their corpus twins. +tables, `.wwi` round-trips, or the wwstage-driver leg. Wwstage-driver-leg +byte identity (`ww_ww` versus `ww` over the emitted `.s` set) has one +owner, now ww-native: `test/byteid/libbyteid_test.ww` on the +`test/testenv` helper package, whose unit sweep spans the 44-entry lib +roster (test fixtures, sentinel-guarded import probes, a zero-dep +root-only build) plus the lib/ completeness scan; the former 815/940/951 +driver-parity carriers were folded into it, their content identity +already owned by their corpus twins, and the native `989_lib_byteid.c` +carrier retired with the port. ## Carrier endgame diff --git a/test/byteid/libbyteid_test.ww b/test/byteid/libbyteid_test.ww new file mode 100644 index 00000000..1f370fd2 --- /dev/null +++ b/test/byteid/libbyteid_test.ww @@ -0,0 +1,434 @@ +package libbyteid_test; + +// cstage-vs-wwstage byte-identity gate over the whole lib/ surface, and +// the ONE owner of wwstage-DRIVER-leg byte identity. Port of the +// retired native carrier test/wcc/989_lib_byteid.c; every assertion +// preserved, .s concat order strengthened from shell-glob to explicit +// byte-lexicographic. +// +// The bootstrap gates (991-995) byte-id only the modules the selfhost +// tools import. Every other lib/ module compiles through the CSTAGE +// driver alone elsewhere, so a cs≠ww divergence there ships gate-green +// (regex.finish did, task #21 FC0). This gate closes the class: each +// lib test fixture is sep-built through BOTH driver stages and the +// per-package asm compared. +// +// #94 sep layout: under sep EACH package compiles to its OWN +// .sepwork/.s — the lib body lands in .s, NOT in +// __root.s. The byte-id concats EVERY per-package .s (lib body + +// lib/test's auto-bundled runner), never just __root.s: comparing the +// root alone would byte-id the harness wrapper while the engine ships +// uncovered. +// +// Three pinned outcomes, loud over blind (task #21 ruling): +// ID — byte-identical across all per-package .s, zero tolerance. +// DIVERGE — known cs≠ww divergence (task #59, cite). Both stages +// must still compile and the asm must still DIFFER; a fix +// fails the entry demanding graduation to ID. +// WWREJECT — cstage compiles, wwstage errors (task #59, cite); +// graduation pinned the same way. +// +// Fixtureless modules are covered by import-probe stubs: `import` +// drags the whole module into its own sep package. The driver silently +// skips an unresolvable import, so each probe carries a sentinel that +// must appear in a per-package .unit.ww, and the completeness scan +// fails loudly on any lib/ dir not enrolled here. + +import os; +import os.exec; +import strings; +import testenv; +import time; + +def MID: i32 = 0; +def MDIVERGE: i32 = 1; +def MWWREJECT: i32 = 2; +def NENTEXPECT: i32 = 44; + +type ent = struct { + fixture: str, // repo-relative .ww; "" -> probe entry + probe: str, // inline import-probe source + inc: str, // extra -I dir beyond dirname(fixture) + mode: i32, + cite: str, // task #59 entry for non-ID modes + sentinel: str, // probe-only: proof the module body landed + moddir: str, // probe-only: completeness key +}; + +// #34: append() rejects struct-call-result sources, so the roster is a +// preallocated slice filled by per-row field writes behind a running +// cursor; the cursor total re-proves the NENTEXPECT count. +fn zeroent(e: *ent) void = { + e.fixture = ""; e.probe = ""; e.inc = ""; e.mode = MID; + e.cite = ""; e.sentinel = ""; e.moddir = ""; +}; + +fn fx(es: []ent, k: *i32, f: str) void = { + let i: i32 = *k; + es[i].fixture = f; + *k = i + 1; +}; + +fn fxi(es: []ent, k: *i32, f: str, inc: str) void = { + let i: i32 = *k; + es[i].fixture = f; + es[i].inc = inc; + *k = i + 1; +}; + +fn pr(es: []ent, k: *i32, p: str, sentinel: str, moddir: str) void = { + let i: i32 = *k; + es[i].probe = p; + es[i].sentinel = sentinel; + es[i].moddir = moddir; + *k = i + 1; +}; + +fn pri(es: []ent, k: *i32, p: str, inc: str, sentinel: str, + moddir: str) void = { + let i: i32 = *k; + es[i].probe = p; + es[i].inc = inc; + es[i].sentinel = sentinel; + es[i].moddir = moddir; + *k = i + 1; +}; + +// The 44-unit roster. Graduation history lives in git (the retired C +// carrier's table comments); cites are kept only where a non-ID pin +// would need them. +fn corpus() []ent = { + let es: []ent = alloc([], NENTEXPECT: u64)!; + es.len = NENTEXPECT; + let z: i32 = 0; + for (z < es.len) { zeroent(&es[z]); z += 1; }; + let n: i32 = 0; + let k: *i32 = &n; + fx(es, k, "lib/bytes/bytestest.ww"); + fx(es, k, "lib/dirs/dirstest.ww"); + fx(es, k, "lib/encoding/base32/base32_test.ww"); + fx(es, k, "lib/encoding/hex/hextest.ww"); + fx(es, k, "lib/encoding/utf8/utf8test.ww"); + fx(es, k, "lib/getopt/getopttest.ww"); + fx(es, k, "lib/hash/adler32/adler32_test.ww"); + fx(es, k, "lib/hash/crc16/crc16_test.ww"); + fx(es, k, "lib/hash/crc32/crc32_test.ww"); + fx(es, k, "lib/hash/crc64/crc64_test.ww"); + fx(es, k, "lib/hash/siphash/siphash_test.ww"); + fx(es, k, "lib/math/checked/checked_test.ww"); + fx(es, k, "lib/math/random/random_test.ww"); + fx(es, k, "lib/memio/memiotest.ww"); + fx(es, k, "lib/os/ostest.ww"); + pr(es, k, "package main;\nimport os.exec;\nfn main() i32 = { return 0; };\n", + "package exec;", "lib/os/exec"); + fx(es, k, "lib/regex/regex_test.ww"); + fx(es, k, "lib/strconv/test/ftostest.ww"); + fx(es, k, "lib/strconv/test/stoftest.ww"); + fx(es, k, "lib/strconv/test/inttest.ww"); + fx(es, k, "lib/strings/stringstest.ww"); + fx(es, k, "lib/temp/temptest.ww"); + fx(es, k, "lib/time/timetest.ww"); + fx(es, k, "lib/bufio/bufiotest.ww"); + fx(es, k, "lib/fmt/fmttest.ww"); + pr(es, k, "package main;\nimport sort;\nfn main() i32 = { return 0; };\n", + "package sort;", "lib/sort"); + pr(es, k, "package main;\nimport path;\nfn main() i32 = { return 0; };\n", + "package path;", "lib/path"); + pr(es, k, "package main;\nimport endian;\nfn main() i32 = { return 0; };\n", + "package endian;", "lib/endian"); + pr(es, k, "package main;\nimport net;\nfn main() i32 = { return 0; };\n", + "package net;", "lib/net"); + pr(es, k, "package main;\nimport hash;\nfn main() i32 = { return 0; };\n", + "package hash;", "lib/hash"); + // fnv lives off the driver's default root (lib/hash/fnv) + pri(es, k, "package main;\nimport fnv;\nfn main() i32 = { return 0; };\n", + "lib/hash/fnv", "package fnv;", "lib/hash/fnv"); + // sentinel is a fn, not `package math;` — lib/math is also package + // math, so a silent fallback there would still match + pr(es, k, "package main;\nimport crypto.math;\nfn main() i32 = { return 0; };\n", + "fn rotl32", "lib/crypto/math"); + pr(es, k, "package main;\nimport c.libc;\nfn main() i32 = { return 0; };\n", + "package libc;", "lib/c/libc"); + // root-only, ZERO-dep build -S: the one driver-leg edge every + // import probe misses (dep-count-0 unit composition). Folded in + // from the retired 815/940/951 driver-parity carriers. No + // sentinel: there is no dep unit to prove. + pr(es, k, "package main;\nfn main() i32 = { return 0; };\n", "", ""); + fx(es, k, "lib/ascii/asciitest.ww"); + fx(es, k, "lib/encoding/base64/base64_test.ww"); + fx(es, k, "lib/errors/errnotest.ww"); + fx(es, k, "lib/log/logtest.ww"); + fx(es, k, "lib/os/stattest.ww"); + // toktest/asttest resolve `import syntax` via -I lib/ww, cf 905 + fxi(es, k, "lib/ww/syntax/toktest.ww", "lib/ww"); + fxi(es, k, "lib/ww/syntax/asttest.ww", "lib/ww"); + fx(es, k, "lib/crypto/sha256/sha256_test.ww"); + fx(es, k, "lib/fnmatch/fnmatchtest.ww"); + fx(es, k, "lib/shlex/shlextest.ww"); + assert(n == NENTEXPECT); + return es; +}; + +// every lib/ dir holding .ww source must be accounted for — enrolled in +// the corpus (fixture dirname or probe moddir) or on this covered list; +// otherwise a new module ships with zero byte-id coverage. +fn covered() []str = { + let cs: []str = []; + // selfhost-embedded: byte-id'd by the 990-997 gates + append(cs, "lib/io"); + append(cs, "lib/math"); + append(cs, "lib/rt"); + append(cs, "lib/types"); + // module body dragged into the lib/strconv/test fixtures + append(cs, "lib/strconv"); + // #17: the @test runner is AUTO-BUNDLED into every -T sep build, + // so every @test fixture byte-ids it cs/ww; 911_attest_record + // also compares it directly. It has no _test.ww of its own. + append(cs, "lib/test"); + return cs; +}; + +fn fail(label: str, why: str) void = { + let m: str = strings.concat("lib_byteid FAIL: ", label, " -- ", why, "\n"); + os.write(2, m.ptr, m.len: u64); + assert(false); +}; + +fn dirnameof(p: str) str = { + let last: i32 = -1; + let i: i32 = 0; + for (i < p.len) { + if (p[i] == '/') { last = i; }; + i += 1; + }; + assert(last > 0); + return strings.sub(p, 0, last); +}; + +fn basenameof(p: str) str = { + let last: i32 = -1; + let i: i32 = 0; + for (i < p.len) { + if (p[i] == '/') { last = i; }; + i += 1; + }; + return strings.sub(p, last + 1, p.len); +}; + +fn stripext(base: str) str = { + assert(strings.hassuffix(base, ".ww")); + return strings.sub(base, 0, base.len - 3); +}; + +// Concatenate every per-package .s under `sepdir` in byte-sorted order; +// "" when the directory is missing or holds no .s. +fn catasm(sepdir: str) str = { + if (!testenv.isdir(sepdir)) { return ""; }; + let names: []str = testenv.listdir(sepdir); + let out: str = ""; + let i: i32 = 0; + for (i < names.len) { + if (strings.hassuffix(names[i], ".s")) { + let body: str = testenv.readfile( + strings.concat(sepdir, "/", names[i])); + out = strings.concat(out, body); + }; + i += 1; + }; + return out; +}; + +fn buildstage(td: str, name: str, drv: str, sub: str, incs: []str, + stem: str, base: str) bool = { + let av: []str = []; + append(av, drv); + append(av, sub); + append(av, "-S"); + let i: i32 = 0; + for (i < incs.len) { + append(av, "-I"); + append(av, incs[i]); + i += 1; + }; + append(av, "-o"); + append(av, stem); + append(av, base); + let co: testenv.commandout; + testenv.runcommand(td, td, name, av, + (180i64 * (time.second: i64)): time.duration, &co); + return co.termination == exec.termination.EXIT && co.code == 0; +}; + +fn checkone(e: *ent) void = { + let td: str = testenv.fresh(); + let label: str = "import-probe"; + if (e.fixture.len != 0) { label = e.fixture; } + else { if (e.moddir.len != 0) { label = e.moddir; }; }; + + let base: str = "probe.ww"; + if (e.fixture.len != 0) { + base = basenameof(e.fixture); + testenv.writefile(strings.concat(td, "/", base), + testenv.readfile(strings.concat(testenv.repo(), "/", + e.fixture))); + } else { + testenv.writefile(strings.concat(td, "/", base), e.probe); + }; + + // dirname(fixture) leads the search path so bare same-module + // imports resolve as they do under the in-tree driver run. + let incs: []str = []; + if (e.fixture.len != 0) { + append(incs, strings.concat(testenv.repo(), "/", + dirnameof(e.fixture))); + }; + if (e.inc.len != 0) { + append(incs, strings.concat(testenv.repo(), "/", e.inc)); + }; + + // @test fixtures are main-less, so `test -S` carries -T + // (synthesizes the entry + auto-bundles lib/test); import probes + // carry their own fn main(), which -T loud-rejects (910), so they + // `build -S`. + let sub: str = "test"; + if (e.fixture.len == 0) { sub = "build"; }; + let stem: str = stripext(base); + let stemc: str = strings.concat(td, "/c_", stem); + let stemw: str = strings.concat(td, "/w_", stem); + + let ce: bool = buildstage(td, "cstage", testenv.driver("ww"), sub, + incs, stemc, base); + let we: bool = buildstage(td, "wwstage", testenv.driver("ww_ww"), sub, + incs, stemw, base); + + // the cstage build resolves the units; its __root.unit.ww proves + // resolution ran. WWREJECT still requires cstage to compile. + if (!ce || !testenv.exists(strings.concat(stemc, + ".sepwork/__root.unit.ww"))) { + fail(label, "cstage produced no resolved unit"); + }; + + // probe coverage: the driver SILENTLY SKIPS an unresolvable + // import, shrinking the probe to an empty main that byte-ids + // trivially. The sentinel must appear in some per-package + // .unit.ww to prove the module landed. + if (e.sentinel.len != 0) { + let sepc: str = strings.concat(stemc, ".sepwork"); + let names: []str = testenv.listdir(sepc); + let found: bool = false; + let i: i32 = 0; + for (i < names.len) { + if (strings.hassuffix(names[i], ".unit.ww")) { + if (testenv.has(testenv.readfile( + strings.concat(sepc, "/", names[i])), + e.sentinel)) { + found = true; + }; + }; + i += 1; + }; + if (!found) { + fail(label, "import silently dropped; probe covers nothing"); + }; + }; + + if (e.mode == MWWREJECT) { + if (we) { + fail(label, strings.concat("wwstage now compiles this; ", + "graduate the ", e.cite, " pin to ID or DIVERGE")); + }; + testenv.clean(td); + return; + }; + if (!we) { fail(label, "wwstage rejected"); }; + + let cs: str = catasm(strings.concat(stemc, ".sepwork")); + let ws: str = catasm(strings.concat(stemw, ".sepwork")); + // two empty concats compare equal — that green covers nothing + if (cs.len == 0 || ws.len == 0) { + fail(label, "empty .s concat"); + }; + let idsame: bool = testenv.same(cs, ws); + if (e.mode == MID && !idsame) { + fail(label, "cs vs ww asm differs (byte-id broken)"); + }; + if (e.mode == MDIVERGE && idsame) { + fail(label, strings.concat("now byte-identical; graduate the ", + e.cite, " pin to ID")); + }; + testenv.clean(td); +}; + +fn accounted(dir: str, es: []ent, cov: []str) bool = { + let i: i32 = 0; + for (i < es.len) { + if (es[i].fixture.len != 0) { + if (testenv.same(dir, dirnameof(es[i].fixture))) { + return true; + }; + } else { if (es[i].moddir.len != 0) { + if (testenv.same(dir, es[i].moddir)) { return true; }; + }; }; + i += 1; + }; + i = 0; + for (i < cov.len) { + if (testenv.same(dir, cov[i])) { return true; }; + i += 1; + }; + return false; +}; + +// Directories under `dir` (repo-relative `rel`) holding .ww source, +// excluding generated .sepwork trees. +fn wwdirs(dir: str, rel: str) []str = { + let out: []str = []; + let names: []str = testenv.listdir(dir); + let hasww: bool = false; + let i: i32 = 0; + for (i < names.len) { + let p: str = strings.concat(dir, "/", names[i]); + if (testenv.isdir(p)) { + if (!strings.hassuffix(names[i], ".sepwork")) { + let sub: []str = wwdirs(p, + strings.concat(rel, "/", names[i])); + let j: i32 = 0; + for (j < sub.len) { + append(out, sub[j]); + j += 1; + }; + }; + } else { + if (strings.hassuffix(names[i], ".ww")) { hasww = true; }; + }; + i += 1; + }; + if (hasww) { append(out, rel); }; + return out; +}; + +@test fn corpuscomplete() void = { + let es: []ent = corpus(); + assert(es.len == NENTEXPECT); + let dirs: []str = wwdirs(strings.concat(testenv.repo(), "/lib"), "lib"); + // an empty scan means the walk itself broke — never pass on that + assert(dirs.len > 0); + let cov: []str = covered(); + let i: i32 = 0; + for (i < dirs.len) { + if (!accounted(dirs[i], es, cov)) { + fail(dirs[i], "un-enrolled lib module; add it to the corpus"); + }; + i += 1; + }; +}; + +@test fn roster() void = { + let es: []ent = corpus(); + assert(es.len == NENTEXPECT); + let i: i32 = 0; + for (i < es.len) { + checkone(&es[i]); + i += 1; + }; +}; diff --git a/test/testenv/testenv.ww b/test/testenv/testenv.ww new file mode 100644 index 00000000..0403128f --- /dev/null +++ b/test/testenv/testenv.ww @@ -0,0 +1,235 @@ +package testenv; + +// Shared plumbing for ww-native test drivers under test/: subprocess +// launch with captured output, file IO, string search, and scratch +// ownership. A utility surface lifted from the proven +// test/package/package_test.ww idiom — not a framework: callers own +// discovery, assertions, and verdicts. + +import os; +import os.exec; +import strings; +import temp; +import time; + +export type commandout = struct { + termination: exec.termination, + code: i32, + stdout: str, + stderr: str, +}; + +fn envrequired(name: str) str = { + match (os.getenv(name)) { + case let value: str => { + assert(value.len != 0); + return strings.dup(value); + }; + case void => abort("missing test environment"); + }; +}; + +// The repository root, provided by the Make target as WW_TEST_REPO. +export fn repo() str = { return envrequired("WW_TEST_REPO"); }; + +export fn driver(name: str) str = { + return strings.concat(repo(), "/out/bin/", name); +}; + +export fn fresh() str = { return strings.dup(temp.dir()); }; + +export fn readfile(path: str) str = { + let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + assert(fd >= 0); + let sr: (i64 | os.oserror) = os.filesize(fd); + let n: i64 = -1i64; + match (sr) { + case let v: i64 => n = v; + case let e: os.oserror => abort("filesize failed"); + }; + assert(n >= 0i64); + let b: []u8 = alloc([], (n + 1i64): u64)!; + b.len = (n + 1i64): i32; + let rr: (i64 | os.oserror) = os.readall(fd, b.ptr, n: u64); + os.close(fd); + let got: i64 = -1i64; + match (rr) { + case let v: i64 => got = v; + case let e: os.oserror => abort("read failed"); + }; + assert(got == n); + let ni: i32 = n: i32; + b[ni] = 0u8; + let out: str; + out.ptr = b.ptr; + out.len = n: i32; + return out; +}; + +export fn writefile(path: str, content: str) void = { + let fd: i32 = os.open(path, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32); + assert(fd >= 0); + match (os.writeall(fd, content.ptr, content.len: u64)) { + case let n: i64 => assert(n == content.len: i64); + case let e: os.oserror => abort("write failed"); + }; + assert(os.close(fd) == 0); +}; + +export fn exists(path: str) bool = { + return os.access(path, 0i32) == 0; +}; + +export fn isdir(path: str) bool = { + let st: os.filestat; + match (os.stat(&st, path)) { + case void => { + return ((st.mode: u32) & 61440u32) == (os.mode.DIR: u32); + }; + case let e: os.oserror => return false; + }; +}; + +// Run argv[0] with `dir` as the working directory, capturing stdout and +// stderr into `/.{stdout,stderr}`. Launch and cleanup +// failures abort; termination and exit code are the caller's verdict. +export fn runcommand(dir: str, root: str, name: str, argv: []str, + lifetime: time.duration, out: *commandout) void = { + let c: exec.command; + c.path = argv[0]; + c.argv = argv; + c.env = os.getenvs(); + c.dir = dir; + c.stdoutpath = strings.concat(root, "/", name, ".stdout"); + c.stderrpath = strings.concat(root, "/", name, ".stderr"); + c.deadline = time.add(time.now(time.clock.monotonic), lifetime); + c.grace = (100i64 * (time.millisecond: i64)): time.duration; + let r: exec.result; + exec.run(&c, &r); + assert(r.errno == 0 && r.cleanuperrno == 0); + out.termination = r.termination; + out.code = r.code; + out.stdout = readfile(c.stdoutpath); + out.stderr = readfile(c.stderrpath); +}; + +export fn clean(root: str) void = { + let av: []str = ["/bin/rm", "-rf", "--", root]; + let c: exec.command; + c.path = av[0]; + c.argv = av; + c.env = os.getenvs(); + c.dir = "/"; + c.stdoutpath = strings.concat(root, ".cleanup.stdout"); + c.stderrpath = strings.concat(root, ".cleanup.stderr"); + c.deadline = time.add(time.now(time.clock.monotonic), time.second); + c.grace = (50i64 * (time.millisecond: i64)): time.duration; + let r: exec.result; + exec.run(&c, &r); + assert(r.termination == exec.termination.EXIT && r.code == 0); + assert(os.remove(c.stdoutpath) == 0); + assert(os.remove(c.stderrpath) == 0); +}; + +export fn pos(haystack: str, needle: str) i32 = { + match (strings.index(haystack, needle)) { + case let n: i32 => return n; + case void => return -1; + }; +}; + +export fn has(haystack: str, needle: str) bool = { + return pos(haystack, needle) >= 0; +}; + +export fn same(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +export fn occurrences(haystack: str, needle: str) i32 = { + if (needle.len == 0 || haystack.len < needle.len) { return 0; }; + let count: i32 = 0; + let i: i32 = 0; + for (i + needle.len <= haystack.len) { + let j: i32 = 0; + let hit: bool = true; + for (j < needle.len) { + if (haystack[i + j] != needle[j]) { hit = false; j = needle.len; } + else { j += 1; }; + }; + if (hit) { count += 1; }; + i += 1; + }; + return count; +}; + +// Byte-lexicographic order, so directory listings sort identically on +// every host regardless of locale. +export fn lexless(a: str, b: str) bool = { + let n: i32 = a.len; + if (b.len < n) { n = b.len; }; + let i: i32 = 0; + for (i < n) { + if (a[i] != b[i]) { return a[i] < b[i]; }; + i += 1; + }; + return a.len < b.len; +}; + +def DIRBUF: i32 = 16384; + +// Entry names of `path` (excluding . and ..), byte-sorted. Aborts on an +// unreadable directory or a malformed dirent stream. +export fn listdir(path: str) []str = { + let names: []str = alloc([], 16u64)!; + let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + assert(fd >= 0); + let buf: []u8 = alloc([], DIRBUF: u64)!; + buf.len = DIRBUF; + let n: i64 = os.getdents64(fd, buf.ptr, buf.len: u64); + for (n > 0i64) { + let off: i32 = 0; + for (off < n: i32) { + // linux_dirent64: d_reclen at 16..17, name at 19. + let reclen: i32 = (buf[off + 16]: i32) + + ((buf[off + 17]: i32) * 256); + assert(reclen >= 20 && off + reclen <= n: i32); + let nb: []u8 = alloc([], 256u64)!; + let k: i32 = 0; + for (buf[off + 19 + k] != 0u8) { + append(nb, buf[off + 19 + k]); + k += 1; + }; + let name: str = strings.frombytes(nb); + if (!same(name, ".") && !same(name, "..")) { + append(names, name); + }; + off += reclen; + }; + n = os.getdents64(fd, buf.ptr, buf.len: u64); + }; + assert(n == 0i64); + assert(os.close(fd) == 0); + // insertion sort; listings are small + let i: i32 = 1; + for (i < names.len) { + let v: str = names[i]; + let j: i32 = i; + for (j > 0) { + if (lexless(v, names[j - 1])) { + names[j] = names[j - 1]; + j -= 1; + } else { break; }; + }; + names[j] = v; + i += 1; + }; + return names; +}; diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c deleted file mode 100644 index 4e4ae237..00000000 --- a/test/wcc/989_lib_byteid.c +++ /dev/null @@ -1,534 +0,0 @@ -/* - * 989_lib_byteid — cstage-vs-wwstage byte-identity gate over the whole - * lib/ surface, and the ONE owner of wwstage-DRIVER-leg byte identity. - * - * The bootstrap gates (991-995) 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 elsewhere, 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 is - * sep-built through BOTH driver stages and the per-package asm compared. - * - * #94 sep layout: the gate drives `ww` (cstage) and `ww_ww` (wwstage). - * Under sep EACH package compiles to its OWN .sepwork/.s — the - * lib body lands in .s, NOT in __root.s (which holds only the test - * fixture / probe main). So the byte-id MUST concat EVERY .s (the - * imported lib body + lib/test's auto-bundled runner included), never - * just __root.s — comparing the root alone would byte-id the harness - * wrapper while the engine ships uncovered (the green-covering-nothing - * trap this gate exists to close). - * - * Three pinned outcomes, loud over blind (task #21 ruling): - * ID — byte-identical across all per-package .s, zero tolerance. - * DIVERGE — known cs≠ww divergence, documented in task #59 with the - * first-diff symbol. Both stages must still compile and - * the concatenated asm must still DIFFER: when a fix lands, - * the entry fails loudly demanding graduation to ID rather - * than silently widening or narrowing coverage. - * WWREJECT — cstage compiles, wwstage 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 its own sep package, 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 in a per-package .unit.ww; and a corpus-completeness scan - * fails loudly on any lib/ dir not enrolled here, so new modules can't - * ship uncovered. - * - * Compile+cmp only; -o redirects every artifact to a getpid-keyed - * /tmp directory for isolation. - * - * 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, NENT_EXPECT = 44 }; - -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 }, - /* graduated from #59.4 DIVERGE by the #22a N_DOT tagged-arg - * cursor arm (ww widened t.N args with a clamped tag 0 — the - * base .s diff is exactly that PUSHQ pair, 2 sites); - * runtime-correct per 979_hex_run */ - { .fixture = "lib/encoding/hex/hextest.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 }, - /* graduated from #59.7 DIVERGE by the #61 *[N]T index-stride fix */ - { .fixture = "lib/hash/siphash/siphash_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 }, - { .probe = "package main;\nimport os.exec;\nfn main() i32 = { return 0; };\n", - .mode = M_ID, .sentinel = "package exec;", .moddir = "lib/os/exec" }, - { .fixture = "lib/regex/regex_test.ww", .mode = M_ID }, - { .fixture = "lib/strconv/test/ftostest.ww", .mode = M_ID }, - /* graduated from #59.10 DIVERGE by the #62 float-literal fold fix - * (wwstage lexer now folds through strconv.stof64, matching - * cstage's strtod bit-for-bit) */ - { .fixture = "lib/strconv/test/stoftest.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 }, - /* graduated from #59.2 DIVERGE by the #129 sretretsize fixes - * (same-module alias priority + callsretsize callee-context swap; - * bufio imports io so cross-module io.stream returns were spuriously - * sret-classified, producing divergent caller prologues) */ - { .fixture = "lib/bufio/bufiotest.ww", .mode = M_ID }, - /* graduated from #59.6 DIVERGE by the #129 same fix (fmt imports - * io transitively through bufio; sretretsize curmod swap closed - * the getopt.printusage spurious LEAQ DI caller divergence) */ - { .fixture = "lib/fmt/fmttest.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" }, - /* #142 graduated (the old #120/#29 global-slice-const blame was - * stale — that family drained earlier): path declares three - * structurally identical !void error singletons, and a VALUE use - * (`return too_long;`) stamped the flattened BODY type, so - * flatvariantidxt loud-rejected the ambiguity. The SK_TYPE value - * arms now stamp the per-decl NAMED (the #66 N_STRUCTLIT - * precedent). Runtime pin: r142_singleton_value_named. */ - { .probe = "package main;\nimport path;\nfn main() i32 = { return 0; };\n", - .mode = M_ID, .cite = "#142 graduated by the SK_TYPE NAMED value stamp", - .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" }, - /* root-only, ZERO-dep build -S: the one driver-leg edge every - * import probe misses (dep-count-0 unit composition). Folded in - * from the retired 815/940/951 driver-parity carriers, whose - * content byteid the corpus twins (r815_/r940_/r951_) own via - * test-data-byteid. No sentinel: there is no dep unit to prove. */ - { .probe = "package main;\nfn main() i32 = { return 0; };\n", - .mode = M_ID }, - /* -------- documented-allowed cs≠ww (task #59) ------------ */ - { .fixture = "lib/ascii/asciitest.ww", - .mode = M_ID, .cite = "#59.1 graduated by #146 str==" }, - /* #59.2 bufio graduated to M_ID above (#129 fix) */ - /* #59.3 base64 graduated to M_ID (pushargsrev N_DOT array-field - * default-hi fix — clear()'s borrow no longer zero-length) */ - { .fixture = "lib/encoding/base64/base64_test.ww", - .mode = M_ID }, - /* #59.4 hextest graduated to M_ID above (#22a reviewer fixes) */ - /* #59.5 graduated: the wwstage nodeisstr/nodeisslice recognizers - * fell to false for an indirect fn-pointer callee (`(*f)()`) and - * for module-global let/const idents, so a 3-word str/slice value - * pushed one word (or zero-filled its widen box) and cgcall - * under-drained; both now fall back to the checker stamp. Runtime - * pins: r595_indirect_call_str_arg, globalconst_{str,slice}_widen. */ - { .fixture = "lib/errors/errnotest.ww", - .mode = M_ID, .cite = "#59.5 graduated by the stamp-keyed recognizers" }, - /* #59.6 fmt graduated to M_ID above (#129 fix) */ - /* #59.7 siphash graduated to M_ID above (#61 fix) */ - /* #59.8 graduated: the wwstage variadic call classification was - * name-keyed — a local fn-ptr field callee whose base shadowed the - * current module name (lib/log's `log.println(log, args...)`) - * picked the module fn's signature (nfixed off by one; the fixed - * arg boxed into the gather, the spread emitted as zeros), and a - * no-collision fn-ptr callee missed entirely (raw N_SPREAD leaked - * as one $0 word). fnptrcalleetfn now resolves local fn-ptr - * callees for CALL target and arg prep alike. Runtime pin: - * r598_fnptr_field_variadic. */ - { .fixture = "lib/log/logtest.ww", - .mode = M_ID, .cite = "#59.8 graduated by the local-first callee resolver" }, - /* #59.9 graduated: the wwstage checker never typed an N_BIN operand - * of `as`, so an enum OR-fold reached cgen unstamped and lowered as - * a phantom tagged assert (unconditional exit 1). checkisas now - * types the N_BIN lhs and the post-order restamp preserves the enum - * stamp. Runtime pin: r599_enum_orfold_as. */ - { .fixture = "lib/os/stattest.ww", - .mode = M_ID, .cite = "#59.9 graduated by the checkisas N_BIN stamp" }, - /* #59.10 stoftest graduated to M_ID above (#62 fix) */ - { .fixture = "lib/ww/syntax/toktest.ww", .inc = "lib/ww", - .mode = M_ID, .cite = "#59.11 graduated by #146 str==" }, - /* toktest/asttest resolve `import syntax` via -I lib/ww, cf 905 */ - { .fixture = "lib/ww/syntax/asttest.ww", .inc = "lib/ww", - .mode = M_ID, .cite = "#59.12 graduated by #146 str==" }, - /* -------- wwstage front-end gaps (task #59) -------------- */ - /* #59.13 graduated: wwstage struct embedding (parser three member - * forms, checker tinfo flatten + promoted-name walkers, cgen - * registerstruct embed descend) — the hash.hash embed compiles - * byte-identically end to end. Runtime pins: r5913_*. */ - { .fixture = "lib/crypto/sha256/sha256_test.ww", - .mode = M_ID, .cite = "#59.13 graduated by the embed port" }, - /* #59.14 graduated to M_ID by #27: the #27b enum-as-int cgen fix - * (value passthrough, not a tagged assertion) + the fnmatch.ww:126 - * `'\\': u8` explicit cast (the rune-lit→u8 narrow w6c_ww rejects at - * a checked position) make w6c_ww compile fnmatch byte-identically. */ - { .fixture = "lib/fnmatch/fnmatchtest.ww", - .mode = M_ID, .cite = "#59.14 graduated by #27" }, - /* #59.15 shlex graduated to M_ID by B-full Layer 1 (#14 nominal - * typeeqast): the cross-module bare-vs-qualified variant forward - * w6c_ww over-rejected now compiles byte-identically. */ - { .fixture = "lib/shlex/shlextest.ww", - .mode = M_ID, .cite = "#59.15 graduated by #14 nominal typeeqast" }, - { .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[8192]; - int rc = -1; - - snprintf(td, sizeof td, "/tmp/wwlbi_%d_%d", getpid(), idx); - if (mkdir(td, 0755) != 0) { - perror(td); - return -1; - } - - 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); - if (e->moddir) - 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. */ - 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); - } - - char stem[512]; - snprintf(stem, sizeof stem, "%s", base); - char *sd = strrchr(stem, '.'); - if (sd) *sd = '\0'; - - /* Two compiler-only sep driver builds. @test fixtures are main-less, - * so `test -S ` carries -T (synthesizes the entry + keeps @test fns + auto- - * bundles lib/test's run() callee) and emits per-package asm without - * a harness run; import-probes carry their own `fn main()`, which -T - * loud-rejects (910), so they `build -S ` (non-T). Each stage's - * artifacts land under its own -o stem so out/ and the source tree - * stay clean. */ - const char *sub = e->fixture ? "test -S " : "build -S "; - char stemc[600], stemw[600]; - snprintf(stemc, sizeof stemc, "%s/c_%s", td, stem); - snprintf(stemw, sizeof stemw, "%s/w_%s", td, stem); - - snprintf(cmd, sizeof cmd, - "cd %s && timeout 180 %s/ww %s %s -o %s %s " - ">/dev/null 2>&1", td, bin, sub, incs, stemc, base); - int ce = runwait(cmd); - snprintf(cmd, sizeof cmd, - "cd %s && timeout 180 %s/ww_ww %s %s -o %s %s " - ">/dev/null 2>&1", td, bin, sub, incs, stemw, base); - int we = runwait(cmd); - - /* the cstage build resolves the units (root + each package); - * its __root.unit.ww proves resolution ran. WWREJECT still requires - * cstage to compile, so this holds for every mode. */ - char rootu[760]; - snprintf(rootu, sizeof rootu, "%s.sepwork/__root.unit.ww", stemc); - if (ce != 0 || access(rootu, 0) != 0) { - fprintf(stderr, "lib_byteid FAIL: %s — cstage produced " - "no resolved unit\n", label); - goto out; - } - - /* probe coverage: the driver SILENTLY SKIPS an unresolvable import - * (cmd/ww/main.c locate_import), shrinking the probe to an empty main - * that byte-ids trivially. The sentinel must appear in some per- - * package .unit.ww (the lib body's own sep unit, not __root) to prove - * the module landed. Fixtures self-protect by calling module symbols. */ - if (e->sentinel) { - snprintf(cmd, sizeof cmd, - "grep -Fq -- '%s' %s.sepwork/*.unit.ww >/dev/null 2>&1", - e->sentinel, stemc); - if (runwait(cmd) != 0) { - fprintf(stderr, "lib_byteid FAIL: %s — import " - "silently dropped (no \"%s\" in any per-package " - "unit); probe covers nothing\n", label, e->sentinel); - goto out; - } - } - - if (e->mode == M_WWREJECT) { - if (we == 0) { - fprintf(stderr, "lib_byteid FAIL: %s — wwstage now " - "compiles this; graduate the %s pin to ID or " - "DIVERGE\n", label, e->cite); - goto out; - } - rc = 0; - goto out; - } - if (we != 0) { - fprintf(stderr, "lib_byteid FAIL: %s — wwstage rejected\n", - label); - goto out; - } - - /* byte-id over EVERY per-package .s (lib body + lib/test included), - * never just __root.s — see the header note. Identical package sets - * both stages, so the *.s glob (shell-sorted) concatenates in the - * same order on each side. */ - char cs[800], ws[800]; - snprintf(cs, sizeof cs, "%s/all_c.s", td); - snprintf(ws, sizeof ws, "%s/all_w.s", td); - snprintf(cmd, sizeof cmd, "cat %s.sepwork/*.s > %s 2>/dev/null", - stemc, cs); - if (runwait(cmd) != 0) { - fprintf(stderr, "lib_byteid FAIL: %s — cstage .s concat " - "failed (no per-package .s)\n", label); - goto out; - } - snprintf(cmd, sizeof cmd, "cat %s.sepwork/*.s > %s 2>/dev/null", - stemw, ws); - if (runwait(cmd) != 0) { - fprintf(stderr, "lib_byteid FAIL: %s — wwstage .s concat " - "failed (no per-package .s)\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); - /* two empty concats compare equal — that green covers nothing */ - if (nc == 0 || nw == 0) { - fprintf(stderr, "lib_byteid FAIL: %s — empty .s concat " - "(cs %zu, ww %zu bytes)\n", label, nc, nw); - goto out; - } - - 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); - if (runwait(cmd) != 0) { - fprintf(stderr, "lib_byteid: cleanup rm -rf %s failed\n", td); - if (rc == 0) rc = -1; - } - 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", - /* module body dragged into the lib/strconv/test fixtures */ - "lib/strconv", - /* #17: the @test runner is AUTO-BUNDLED into every -T sep build (its - * own test.sepwork/test.s), so every @test fixture above byte-ids it - * cs/ww; 911_attest_record also compares it directly. It has no - * _test.ww of its own. */ - "lib/test", - 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]; - /* E3-C1: the flip routes lib @test builds through sep, which drops - * .sepwork/.unit.ww scratch next to source; exclude it so - * the scan enrols real source dirs only, not generated work dirs. */ - snprintf(cmd, sizeof cmd, "cd %s && find lib -name '*.ww' " - "! -path '*.sepwork/*' " - "| 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 nent = (int)(sizeof ents / sizeof ents[0]) - 1; - if (nent != NENT_EXPECT) { - fprintf(stderr, "lib_byteid FAIL: corpus has %d entries, want %d\n", - nent, NENT_EXPECT); - 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; -}