test: re-express 989_lib_byteid byte-id corpus gate to sep .sepwork layout (M4 E3, #94)

94b of the C-mig2 split (own commit: lone phase-1 test-commit cache
gate). The corpus gate now drives two --sep builds (ww / ww_ww) per lib
unit and byte-diffs the concatenated per-package .sepwork/*.s. Keying on
every per-package .s — the imported lib body (e.g. log.s) and lib/test
(test.s), never collapsed to __root.s — preserves the cs!=ww lib-body
coverage the gate exists for. Tri-mode (ID/DIVERGE/WWREJECT) per module,
import-landed sentinel, and corpus_complete scan all preserved. 43/43
(net=M_ID via #102). Test-only; all 5 binary pins HOLD.
This commit is contained in:
2026-06-18 17:26:09 +09:00
parent 4583977ce0
commit fa365f00b2
2 changed files with 94 additions and 77 deletions

View File

@@ -3129,8 +3129,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)
$(BIN)/test_lib_byteid: test/wcc/989_lib_byteid.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_m2wwi_run: test/wcc/989_m2wwi_run.c $(BIN)/ww $(BIN)/w6c \

View File

@@ -7,31 +7,41 @@
* 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.
* (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 --sep` (cstage) and `ww_ww --sep`
* (wwstage) instead of feeding a single .combined.ww to raw w6c/w6c_ww.
* Under sep EACH package compiles to its OWN <stem>.sepwork/<pkg>.s — the
* lib body lands in <pkg>.s, NOT in __root.s (which holds only the test
* fixture / probe main). So the byte-id MUST concat EVERY <pkg>.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, zero tolerance.
* 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 outputs must still DIFFER: when a fix lands, the
* entry fails loudly demanding graduation to ID rather
* 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 — w6c compiles, w6c_ww errors (front-end gap, task #59).
* Graduation is pinned the same way.
* WWREJECT — cstage --sep compiles, wwstage --sep 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
* `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; and a corpus-completeness scan fails loudly on any
* lib/ dir not enrolled here, so new modules can't ship uncovered.
* 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 — 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.
* Compile+cmp only (--sep -o redirects every artifact to a getpid-keyed
* /tmp dir; WW_PKGCACHE pinned there so out/.pkgcache is untouched), so
* this stays phase-1 parallel-safe next to the 9xx _run tests.
*
* 9xx is full; shares the 989 prefix per the 989_sha256 precedent (the
* `short` name keys the binary).
@@ -217,7 +227,7 @@ 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];
char td[64], cmd[8192];
int rc = -1;
snprintf(td, sizeof td, "/tmp/wwlbi_%d_%d", getpid(), idx);
@@ -248,8 +258,7 @@ check_one(const char *bin, const char *cwd, const struct ent *e, int idx)
/* 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). */
* they do under the in-tree driver run. */
char incs[2048] = "";
if (e->fixture) {
const char *slash = strrchr(e->fixture, '/');
@@ -261,73 +270,63 @@ check_one(const char *bin, const char *cwd, const struct ent *e, int idx)
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. A @test
* fixture builds via `ww test -c -o <stem>` (compile-only) so the
* resolved unit AUTO-BUNDLES lib/test — the #17 synth's `run()` callee
* must resolve when w6c/w6c_ww -T compile the combined below. Both the
* `-o <stem>` (test) and the next-to-source (build) paths land the
* combined at <stem>.combined.ww. Import-probes carry their own main
* and stay a plain non-test build. */
char stem[512];
snprintf(stem, sizeof stem, "%s", base);
char *sd = strrchr(stem, '.');
if (sd) *sd = '\0';
if (e->fixture)
snprintf(cmd, sizeof cmd,
"cd %s && timeout 180 %s/ww test -c -o %s %s %s >/dev/null 2>&1",
td, bin, stem, incs, base);
else
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.combined.ww", td, stem);
if (access(comb, 0) != 0) {
fprintf(stderr, "lib_byteid FAIL: %s — no resolved unit\n", label);
/* Two sep driver builds. @test fixtures are main-less, so `test -c
* --sep` 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 --sep` (non-T). Each stage's
* artifacts land under its own -o stem so out/ and the source tree
* stay clean; WW_PKGCACHE is pinned to td. */
const char *sub = e->fixture ? "test -c --sep" : "build --sep";
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 && WW_PKGCACHE=%s/pc timeout 180 %s/ww %s %s -o %s %s "
">/dev/null 2>&1", td, td, bin, sub, incs, stemc, base);
int ce = runwait(cmd);
snprintf(cmd, sizeof cmd,
"cd %s && WW_PKGCACHE=%s/pw timeout 180 %s/ww_ww %s %s -o %s %s "
">/dev/null 2>&1", td, td, bin, sub, incs, stemw, base);
int we = runwait(cmd);
/* the cstage --sep 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 --sep 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) {
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) {
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 resolved unit); "
"probe covers nothing\n", label, e->sentinel);
"silently dropped (no \"%s\" in any per-package "
"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);
/* Fixtures are main-less @test files: -T synthesizes the entry and
* keeps the @test fns symmetrically (non-T drops them on both stages,
* #6, so byte-id needs -T). Import-probes carry their own
* `fn main()`, which -T loud-rejects (910), so they stay non-T. */
const char *tflag = e->fixture ? "-T " : "";
snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c %s%s > %s 2>/dev/null",
bin, tflag, 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 %s-o %s %s >/dev/null 2>&1",
bin, tflag, ws, comb);
int wrc = runwait(cmd);
if (e->mode == M_WWREJECT) {
if (wrc == 0) {
fprintf(stderr, "lib_byteid FAIL: %s — w6c_ww now "
if (we == 0) {
fprintf(stderr, "lib_byteid FAIL: %s — wwstage --sep now "
"compiles this; graduate the %s pin to ID or "
"DIVERGE\n", label, e->cite);
goto out;
@@ -335,11 +334,26 @@ check_one(const char *bin, const char *cwd, const struct ent *e, int idx)
rc = 0;
goto out;
}
if (wrc != 0) {
fprintf(stderr, "lib_byteid FAIL: %s — w6c_ww rejected\n", label);
if (we != 0) {
fprintf(stderr, "lib_byteid FAIL: %s — wwstage --sep 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 (system(cmd)) {}
snprintf(cmd, sizeof cmd, "cat %s.sepwork/*.s > %s 2>/dev/null",
stemw, ws);
if (system(cmd)) {}
char *bc = NULL, *bw = NULL;
size_t nc = 0, nw = 0;
if (slurp(cs, &bc, &nc) < 0 || slurp(ws, &bw, &nw) < 0) {
@@ -376,9 +390,10 @@ static const char *covered[] = {
"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 combined, 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. */
/* #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,
};