ww: own the scratch contract, drop the package cache, add -S

Scratch acquisition is one bare mkdir that refuses a pre-existing
.sepwork; the path reaches cleanup only after acquisition succeeds.
ww build and ww test -o keep the tree for the caller; ww run and
no-output ww test build under an owned /tmp/ww_{run,test}_<pid> dir
and remove it on every result, escalating rc on cleanup failure.
The content-keyed package cache is deleted from both stages; -S stops
after per-package .s emission; --sep is gone and unknown flags are
rejected; directory ww test delegates to the wwtest coordinator; the
wwstage driver routes every subprocess through os.exec.

The wwstage Makefile recipes move off the persistent <tool>_ww.d
build-in-place shape to a per-invocation mktemp workspace: the old
recipes relied on the retired pre-wipe semantics and any rebuild
would trip the new refusal on their leftover main.sepwork.

The three 989_pkgcache_* carriers lose their subject with the cache
and go with it; the rest of the wave's carrier adaptation follows in
its own commits.
This commit is contained in:
2026-08-07 23:12:54 +09:00
parent 7a2c21acfb
commit 350bcd8913
7 changed files with 1243 additions and 2032 deletions

12
.gitignore vendored
View File

@@ -1,12 +1,12 @@
# Build outputs.
/out/
# Separate-compilation scratch dirs. `ww build` / `ww --sep` (the sole
# build path post-#22 flip) drops <stem>.sepwork/ next to the OUTPUT
# stem; an in-place build (no -o) lands it next to the source. The
# Makefile byte-id gates read <stem>.sepwork/*.s deliberately (rule-10
# #223 discriminator), so the dir is transient-not-tracked (CLAUDE.md
# rule-14), never relocated.
# Separate-compilation artifact dirs. `ww build` and explicit `ww test -o`
# leave caller-owned <stem>.sepwork/ at the selected artifact stem; they
# refuse to replace an existing tree. `ww run` and no-output single-file tests
# put the executable and scratch in one driver-owned temporary directory and
# remove both. Byte gates read selected files deliberately, then remove the
# exact tree, so it is transient-not-tracked.
*.sepwork/
# Claude Code session artifacts.

View File

@@ -132,12 +132,16 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/wwdump_ww.d
cd $(BIN)/wwdump_ww.d && $(CURDIR)/$(BIN)/ww build \
-o main \
-I $(CURDIR)/lib/ww \
-I $(CURDIR)/selfhost/cmd/wcc \
$(CURDIR)/selfhost/cmd/wwdump/main.ww
mv $(BIN)/wwdump_ww.d/main $@
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/wwdump_ww.d/build.XXXXXX") || exit 1; \
rc=0; published=0; \
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
-I $(CURDIR)/lib/ww -I $(CURDIR)/selfhost/cmd/wcc \
$(CURDIR)/selfhost/cmd/wwdump/main.ww) || rc=$$?; \
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
exit $$rc
# ---- ww-side w6c (compiler port, exercised by 994_w6c_ww) -------------
# Thin driver: parse + cgen. The frontend (lex/parse/ast/...) is the
@@ -153,12 +157,16 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/w6c_ww.d
cd $(BIN)/w6c_ww.d && $(CURDIR)/$(BIN)/ww build \
-o main \
-I $(CURDIR)/lib/ww \
-I $(CURDIR)/selfhost/cmd/wcc \
$(CURDIR)/selfhost/cmd/w6c/main.ww
mv $(BIN)/w6c_ww.d/main $@
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6c_ww.d/build.XXXXXX") || exit 1; \
rc=0; published=0; \
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
-I $(CURDIR)/lib/ww -I $(CURDIR)/selfhost/cmd/wcc \
$(CURDIR)/selfhost/cmd/w6c/main.ww) || rc=$$?; \
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
exit $$rc
# ---- ww-side w6a (assembler port, exercised by 991_w6a_ww) ------------
# Built like wwdump_ww. Needs -I selfhost/cmd/w6a for the local types/lex/
@@ -171,11 +179,16 @@ $(BIN)/w6a_ww: selfhost/cmd/w6a/main.ww selfhost/cmd/w6a/opcodes.ww \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/w6a_ww.d
cd $(BIN)/w6a_ww.d && $(CURDIR)/$(BIN)/ww build \
-o main \
-I $(CURDIR)/selfhost/cmd/w6a \
$(CURDIR)/selfhost/cmd/w6a/main.ww
mv $(BIN)/w6a_ww.d/main $@
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6a_ww.d/build.XXXXXX") || exit 1; \
rc=0; published=0; \
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
-I $(CURDIR)/selfhost/cmd/w6a \
$(CURDIR)/selfhost/cmd/w6a/main.ww) || rc=$$?; \
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
exit $$rc
# ---- ww-side w6l (linker port, exercised by 992_w6l_ww) ---------------
# Built like 6a_ww. Needs -I selfhost/cmd/w6l for the local sym/obj/pass/
@@ -189,25 +202,36 @@ $(BIN)/w6l_ww: selfhost/cmd/w6l/main.ww selfhost/cmd/w6l/sym.ww \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/w6l_ww.d
cd $(BIN)/w6l_ww.d && $(CURDIR)/$(BIN)/ww build \
-o main \
-I $(CURDIR)/selfhost/cmd/w6l \
$(CURDIR)/selfhost/cmd/w6l/main.ww
mv $(BIN)/w6l_ww.d/main $@
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6l_ww.d/build.XXXXXX") || exit 1; \
rc=0; published=0; \
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
-I $(CURDIR)/selfhost/cmd/w6l \
$(CURDIR)/selfhost/cmd/w6l/main.ww) || rc=$$?; \
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
exit $$rc
# ---- ww-side ww driver (exercised by 993_ww_ww) ------------------------
# The driver pulls in lib/os (default search path) and orchestrates
# w6c/w6a/w6l like the C driver.
$(BIN)/ww_ww: selfhost/cmd/ww/main.ww lib/os/os.ww lib/rt/malloc.ww \
$(BIN)/ww_ww: selfhost/cmd/ww/main.ww lib/os/exec/exec.ww \
lib/os/os.ww lib/rt/malloc.ww \
lib/time/time.ww lib/strconv/strconv.ww \
lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(BIN)/ww_ww.d
cd $(BIN)/ww_ww.d && $(CURDIR)/$(BIN)/ww build \
-o main \
$(CURDIR)/selfhost/cmd/ww/main.ww
mv $(BIN)/ww_ww.d/main $@
@d=$$(mktemp -d "$(CURDIR)/$(BIN)/ww_ww.d/build.XXXXXX") || exit 1; \
rc=0; published=0; \
(cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \
$(CURDIR)/selfhost/cmd/ww/main.ww) || rc=$$?; \
rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \
if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \
if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \
if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \
exit $$rc
# ---- runtime (libwwrt.a, assembled by our own w6a) ---------------------
$(OBJ)/rt/%.o: rt/%.s $(BIN)/w6a | $(OBJ)/rt

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,227 +0,0 @@
/*
* 989_pkgcache_concurrent_run — concurrent shared-cache build-correctness
* smoke (#104). N distinct root programs that all `import shared` are built
* CONCURRENTLY into ONE shared WW_PKGCACHE; every resulting binary must be
* byte-IDENTICAL to a reference built in ISOLATION (private cache) and run to
* its expected exit. This guards that concurrent `ww build --sep` sharing one
* out/.pkgcache produces correct, deterministic binaries — a regression guard
* for the cache subsystem under contention (store crash, lock bug, wrong-key
* copy, etc.).
*
* It does NOT prove the temp+rename store is atomic against torn reads: with
* content-keying every concurrent cold build MISSES at lookup and STORES (it
* never HIT-reads a mid-store entry), so the torn-read window is not forced
* here. That race is closed by construction at the cache_store fix site; the
* deferred white-box guard is TASK #105.
*
* Both driver stages (rule 10): the cs and ww references are byte-identical.
* Light wwstage-driver test (CLAUDE.md rule 14): all outputs + caches live
* under a private /tmp tree, so it is parallel-safe and off every byte-id /
* bootstrap gate. Models 989_pkgcache_run conventions.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#define NPROG 4
#define BASE_EXIT 7 /* shared.v() == 7; rootI returns 7 + I */
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
write_file(const char *path, const char *body)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(body, f);
fclose(f);
return 0;
}
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
files_eq(const char *a, const char *b)
{
char *ba = NULL, *bb = NULL;
size_t na = 0, nb = 0;
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
free(ba); free(bb);
return -1;
}
int eq = (na == nb && memcmp(ba, bb, na) == 0);
free(ba); free(bb);
return eq ? 0 : 1;
}
static const char *shared_src =
"package shared;\n"
"export fn v() i32 = { return 7; };\n";
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
int fail = 0;
char td[64], cmd[8192], batch[32768];
char sharedww[1024];
snprintf(td, sizeof td, "/tmp/wwconc_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
snprintf(cmd, sizeof cmd, "mkdir -p %s/shared", td);
runwait(cmd);
snprintf(sharedww, sizeof sharedww, "%s/shared/shared.ww", td);
if (write_file(sharedww, shared_src)) { fail++; goto out; }
/* N distinct roots, each importing the one shared pkg. Distinct return
* (7 + I) so each program — and so each clean binary — is distinguishable,
* while the contended cache entry (shared) is common to all. */
for (int i = 0; i < NPROG; i++) {
char rootww[1024], src[256];
snprintf(rootww, sizeof rootww, "%s/root%d.ww", td, i);
snprintf(src, sizeof src,
"package main;\nimport shared;\n"
"fn main() i32 = { return shared.v() + %d; };\n", i);
if (write_file(rootww, src)) { fail++; goto out; }
}
struct { const char *drv, *tag; } stg[] = {
{ "ww", "cs" },
{ "ww_ww", "ww" },
};
for (int s = 0; s < 2; s++) {
const char *drv = stg[s].drv, *tag = stg[s].tag;
char shcache[1024];
snprintf(shcache, sizeof shcache, "%s/cache.%s", td, tag);
/* References: each rootI built ISOLATED (private cache, no contention)
* = the clean baseline bytes a concurrent build must reproduce. */
for (int i = 0; i < NPROG; i++) {
char refcache[1024], refprog[1024], refscr[1024], rootww[1024];
snprintf(refcache, sizeof refcache, "%s/refc.%s.%d", td, tag, i);
snprintf(refprog, sizeof refprog, "%s/ref.%s.%d", td, tag, i);
snprintf(refscr, sizeof refscr, "%s/ref.%s.%d.sepwork", td, tag, i);
snprintf(rootww, sizeof rootww, "%s/root%d.ww", td, i);
snprintf(cmd, sizeof cmd,
"rm -rf %s %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep "
"-o %s %s >/dev/null 2>&1",
refscr, refcache, refcache, bin, drv, refprog, rootww);
if (runwait(cmd) != 0) {
fprintf(stderr, "concur FAIL[%s]: reference build %d failed\n",
drv, i);
fail++;
}
if (runwait(refprog) != BASE_EXIT + i) {
fprintf(stderr, "concur FAIL[%s]: reference %d wrong exit\n",
drv, i);
fail++;
}
}
/* Fresh shared cache so every build in the batch MISS-stores `shared`
* concurrently → write contention on shared/P.{wwi,o,key}. Distinct
* per-prog output so every binary can be verified, not just one. */
snprintf(cmd, sizeof cmd, "rm -rf %s", shcache);
runwait(cmd);
size_t off = 0;
off += (size_t)snprintf(batch + off, sizeof batch - off,
"export WW_PKGCACHE='%s'; ", shcache);
for (int i = 0; i < NPROG; i++) {
off += (size_t)snprintf(batch + off, sizeof batch - off,
"( rm -rf %s/c.%s.%d.sepwork; timeout 240 %s/%s build "
"--sep -o %s/c.%s.%d %s/root%d.ww >/dev/null 2>&1 ) & ",
td, tag, i, bin, drv,
td, tag, i, td, i);
}
off += (size_t)snprintf(batch + off, sizeof batch - off, "wait");
if (off >= sizeof batch) {
fprintf(stderr, "concur FAIL: batch cmd truncated\n");
fail++; goto out;
}
runwait(batch);
for (int i = 0; i < NPROG; i++) {
char prog[1024], refprog[1024];
snprintf(prog, sizeof prog, "%s/c.%s.%d", td, tag, i);
snprintf(refprog, sizeof refprog, "%s/ref.%s.%d", td, tag, i);
if (runwait(prog) != BASE_EXIT + i) {
fprintf(stderr, "concur FAIL[%s]: concurrent prog i=%d "
"wrong/failed exit\n", drv, i);
fail++;
}
if (files_eq(prog, refprog) != 0) {
fprintf(stderr, "concur FAIL[%s]: concurrent binary i=%d "
"!= isolated reference\n", drv, i);
fail++;
}
}
}
/* rule 10: the cs and ww isolated references are byte-identical. */
for (int i = 0; i < NPROG; i++) {
char a[1024], b[1024];
snprintf(a, sizeof a, "%s/ref.cs.%d", td, i);
snprintf(b, sizeof b, "%s/ref.ww.%d", td, i);
if (files_eq(a, b) != 0) {
fprintf(stderr, "concur FAIL: cs != ww reference %d (rule 10)\n", i);
fail++;
}
}
out:
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
if (fail) {
fprintf(stderr, "concur: %d check(s) failed\n", fail);
return 1;
}
printf("concur: %d concurrent --sep builds sharing one cache, both stages "
"— every binary byte-identical to its isolated reference + correct run; "
"cs==ww references (rule 10)\n", NPROG);
return 0;
}

View File

@@ -1,247 +0,0 @@
/*
* 989_pkgcache_poison_run — out/.pkgcache 0-byte-artifact self-heal gate (#10,
* BUG-B). A torn producer write (e.g. disk-full mid-copy) can leave a 0-byte
* P.wwi/P.o under a SELF-CONSISTENT P.key. Pre-fix the cache served that empty
* artifact forever (the key matched and access() saw the file), so every later
* build link-failed or produced a wrong binary — a SILENT serve-wrong that
* never surfaces on its own. The fix rejects size==0 on BOTH the lookup (read)
* and the store (write) side: a poisoned entry self-heals (treated as MISS →
* re-derived), and a torn store never commits the key. The guard is symmetric
* cstage (filenonempty, stat) / wwstage (cachefilesize, open+filesize) — the
* hit/miss DECISION must match (rule 10).
*
* Target: a 2-level graph root(main) -> leaf in a private temp tree. ROOT is
* never cached (it is the build target); `leaf` is the cacheable package. We
* seed a warm cache, POISON the cached artifact to 0 bytes, rebuild, and assert
* the build self-heals: leaf re-compiles (MISS sentinel) AND the program runs
* to its correct value (not an empty/link-failed binary) AND the cache is
* re-stored non-empty.
*
* HIT/MISS sentinel (same as 989_pkgcache_run): a MISS runs w6c → writes
* <scratch>/leaf.s; a HIT copies the cached .o/.wwi and skips compose+w6c+w6a,
* so no leaf.s. The scratch is wiped before each build so leaf.s presence
* reflects exactly the last build.
*
* Table-driven over WHICH artifact is poisoned: { P.wwi | P.o | both }. Each
* row, per stage (ww, ww_ww):
* - warm baseline: an unchanged rebuild HITs (leaf.s ABSENT, prog runs) —
* proves the cache is genuinely warm before we poison it (non-vacuity);
* - POISON the row's artifact(s) to 0 bytes;
* - heal rebuild: leaf re-compiles (leaf.s PRESENT = MISS, not served poison)
* AND prog runs == EXPECT_EXIT AND both cached artifacts are non-empty again
* (re-stored). Pre-fix this row served the 0-byte artifact and the assert
* on leaf.s (and on prog exit) fails.
*
* Light wwstage-driver test (CLAUDE.md rule 14): all build outputs and caches
* live under a private /tmp tree, wiped on exit, so it is parallel-safe and off
* every byte-id/bootstrap gate. Models 989_pkgcache_run conventions.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#define EXPECT_EXIT 7
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
write_file(const char *path, const char *body)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(body, f);
fclose(f);
return 0;
}
/* Truncate `path` to 0 bytes (models a torn producer write under a valid key);
* "wb" opens-and-truncates, leaving the file present but empty. */
static int
truncate0(const char *path)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fclose(f);
return 0;
}
static long
fsize(const char *path)
{
struct stat st;
if (stat(path, &st) != 0) return -1;
return (long)st.st_size;
}
/* <scratch>/<pkg>.s present ⇒ that package compiled this build (MISS). */
static int
sdot(const char *scratch, const char *pkg)
{
char p[1100];
snprintf(p, sizeof p, "%s/%s.s", scratch, pkg);
return access(p, 0) == 0;
}
static const char *leaf_src =
"package leaf;\n"
"export fn base() i32 = { return 7; };\n";
static const char *root_src =
"package main;\n"
"import leaf;\n"
"fn main() i32 = { return leaf.base(); };\n";
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
int fail = 0;
char td[64], cmd[8192];
char rootww[1024], leafww[1024];
char scratch[1024], prog[1024];
snprintf(td, sizeof td, "/tmp/wwpkgpoison_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
snprintf(cmd, sizeof cmd, "mkdir -p %s/leaf", td);
runwait(cmd);
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
snprintf(leafww, sizeof leafww, "%s/leaf/leaf.ww", td);
snprintf(prog, sizeof prog, "%s/p", td);
snprintf(scratch, sizeof scratch, "%s/p.sepwork", td);
if (write_file(rootww, root_src) || write_file(leafww, leaf_src)) {
fail++; goto out;
}
/* Which cached artifact(s) the row poisons. */
struct { const char *tag; int wwi, obj; } rows[] = {
{ "P.wwi", 1, 0 },
{ "P.o", 0, 1 },
{ "both", 1, 1 },
};
struct { const char *drv; } stg[] = { { "ww" }, { "ww_ww" } };
for (int s = 0; s < 2; s++) {
const char *drv = stg[s].drv;
char cache[1024], cwwi[1100], cobj[1100];
snprintf(cache, sizeof cache, "%s/cache.%d", td, s);
snprintf(cwwi, sizeof cwwi, "%s/leaf/P.wwi", cache);
snprintf(cobj, sizeof cobj, "%s/leaf/P.o", cache);
/* The cstage driver rm -rf's the scratch each build; the wwstage
* driver only mkdir's it (#58(a) asymmetry), so a stale leaf.s could
* persist and defeat the MISS/HIT sentinel. The gate owns its scratch:
* wipe it before every build so a fresh leaf.s reflects the last build. */
#define BUILD() do { \
snprintf(cmd, sizeof cmd, \
"rm -rf %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep " \
"-o %s %s >/dev/null 2>&1", scratch, cache, bin, drv, prog, \
rootww); \
} while (0)
/* fresh cache for this stage, cold-seed it (MISS) */
snprintf(cmd, sizeof cmd, "rm -rf %s", cache);
runwait(cmd);
BUILD();
if (runwait(cmd) != 0) {
fprintf(stderr, "poison FAIL[%s]: cold seed build failed\n", drv);
fail++; continue;
}
if (!sdot(scratch, "leaf")) {
fprintf(stderr, "poison FAIL[%s]: cold seed — leaf did not "
"compile (MISS expected)\n", drv);
fail++;
}
if (runwait(prog) != EXPECT_EXIT) {
fprintf(stderr, "poison FAIL[%s]: cold seed prog exit != %d\n",
drv, EXPECT_EXIT);
fail++;
}
for (size_t r = 0; r < sizeof rows / sizeof rows[0]; r++) {
/* warm baseline — the cache is warm (heals re-store it), so an
* unchanged rebuild must HIT; proves we poison a LIVE entry. */
BUILD(); runwait(cmd);
if (sdot(scratch, "leaf")) {
fprintf(stderr, "poison FAIL[%s/%s]: warm baseline — leaf "
"recompiled (cache not warm before poison)\n",
drv, rows[r].tag);
fail++;
}
/* poison the row's artifact(s) to 0 bytes, key stays valid */
if (rows[r].wwi && truncate0(cwwi) != 0) {
fprintf(stderr, "poison FAIL[%s/%s]: cannot truncate P.wwi\n",
drv, rows[r].tag);
fail++;
}
if (rows[r].obj && truncate0(cobj) != 0) {
fprintf(stderr, "poison FAIL[%s/%s]: cannot truncate P.o\n",
drv, rows[r].tag);
fail++;
}
/* heal rebuild — a 0-byte artifact under a valid key must be
* treated as MISS (re-derived), NOT served. */
BUILD(); runwait(cmd);
if (!sdot(scratch, "leaf")) {
fprintf(stderr, "poison FAIL[%s/%s]: served poison — leaf did "
"NOT re-derive after 0-byte artifact (silent serve-wrong)\n",
drv, rows[r].tag);
fail++;
}
if (runwait(prog) != EXPECT_EXIT) {
fprintf(stderr, "poison FAIL[%s/%s]: heal prog exit != %d "
"(empty/link-failed binary)\n", drv, rows[r].tag, EXPECT_EXIT);
fail++;
}
/* re-store must leave both artifacts non-empty (write guard let a
* good store through, AND a torn store never commits a 0-byte). */
if (fsize(cwwi) <= 0 || fsize(cobj) <= 0) {
fprintf(stderr, "poison FAIL[%s/%s]: cached artifact still "
"0-byte after heal (re-store did not repair)\n",
drv, rows[r].tag);
fail++;
}
}
#undef BUILD
}
out:
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
if (fail) {
fprintf(stderr, "poison: %d check(s) failed\n", fail);
return 1;
}
printf("poison: root->leaf via build_one_sep — a 0-byte cached P.wwi/P.o/both "
"under a valid key self-heals (treated as MISS, re-derived to correct "
"value, re-stored non-empty) — both stages\n");
return 0;
}

View File

@@ -1,381 +0,0 @@
/*
* 989_pkgcache_run — M3-tail commit-5b out/.pkgcache content-keyed package
* cache gate (#63). COLD, dev-only: the cache is keyed by content, never
* mtime, and every bootstrap/byte-id gate cold-compiles (the `--sep` scratch
* is wiped each run), so 5b changes NO gate output. This leg certifies the
* cache itself, through the REAL `ww` / `ww_ww` drivers.
*
* Target: a synthetic 3-level graph root(main) -> mid -> leaf in a private
* temp tree, so source bytes and the dep interface are fully under test
* control. The ROOT is never cached (it is the build target); `mid` and
* `leaf` are the cacheable packages. Each stage uses its OWN WW_PKGCACHE dir
* (the compiler-binary line of P.key is intentionally stage-specific — it
* keys CODEGEN identity — so cs and ww keep separate cache namespaces; the
* cacheable OUTPUTS they reuse stay byte-identical, asserted below).
*
* HIT/MISS sentinel: a cache MISS runs w6c, which writes <scratch>/<pkg>.s;
* a HIT copies the cached .o/.wwi and skips compose+w6c+w6a, so no <pkg>.s
* is produced. The scratch is wiped each build, so <pkg>.s presence reflects
* exactly the last build.
*
* Legs, per stage (ww, ww_ww):
* 1. MISS: a cold build compiles (leaf.s AND mid.s present; prog runs == 5).
* 2. HIT (+ non-vacuity baseline): an unchanged rebuild skips both
* (leaf.s AND mid.s ABSENT; prog still runs == 5 from cached artifacts).
* 3. BUST — source edit: perturb mid.ww -> mid MISS, leaf still HIT;
* restore + reseed -> HIT again.
* 4. BUST — dep .wwi change: change leaf's EXPORTED interface -> leaf's .wwi
* md5 changes -> the importer `mid` (own source unchanged) MISSES via its
* dep line; restore + reseed -> HIT again.
* 5. BUST — compiler binary md5: corrupt the stored mid/P.key `w6c` line ->
* the freshly recomputed manifest mismatches -> mid MISS; the rebuild
* rewrites a correct key -> HIT again. (Models a codegen-changed compiler;
* a real WW_W6C swap is cstage-only — the wwstage driver hardcodes its
* toolchain — so a white-box key-line edit proves the class on both.)
* 6. BUST — flag string: corrupt the stored mid/P.key `flags` line -> mid
* MISS; rebuild rewrites -> HIT again.
*
* Cross-stage (rule 10): the cached mid/leaf P.o AND P.wwi are byte-identical
* between the cs and ww caches.
*
* Light wwstage-driver test (CLAUDE.md rule 14): all build outputs and caches
* live under a private /tmp tree, so it is parallel-safe and off every
* byte-id/bootstrap gate. Models 989_sepbuild_run conventions.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#define EXPECT_EXIT 5
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
write_file(const char *path, const char *body)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(body, f);
fclose(f);
return 0;
}
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
files_eq(const char *a, const char *b)
{
char *ba = NULL, *bb = NULL;
size_t na = 0, nb = 0;
if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) {
free(ba); free(bb);
return -1;
}
int eq = (na == nb && memcmp(ba, bb, na) == 0);
free(ba); free(bb);
return eq ? 0 : 1;
}
/* Flip one byte inside the line of `path` that starts with `prefix` (keeping
* the line length so the mutation isolates that one input class, not the
* blob length). Returns 0 on success. */
static int
mutate_keyline(const char *path, const char *prefix)
{
char *buf = NULL;
size_t n = 0;
if (slurp(path, &buf, &n) < 0) return -1;
size_t pl = strlen(prefix);
char *hit = NULL;
for (size_t i = 0; i < n; i++) {
if ((i == 0 || buf[i-1] == '\n') && i + pl <= n &&
memcmp(buf + i, prefix, pl) == 0) {
hit = buf + i + pl;
break;
}
}
if (!hit || hit >= buf + n || *hit == '\n') { free(buf); return -1; }
*hit = (*hit == 'a') ? 'b' : 'a';
FILE *f = fopen(path, "wb");
if (!f) { free(buf); return -1; }
fwrite(buf, 1, n, f);
fclose(f);
free(buf);
return 0;
}
/* <scratch>/<pkg>.s present ⇒ that package compiled this build (MISS). */
static int
sdot(const char *scratch, const char *pkg)
{
char p[1100];
snprintf(p, sizeof p, "%s/%s.s", scratch, pkg);
return access(p, 0) == 0;
}
static const char *leaf_v1 =
"package leaf;\n"
"export fn base() i32 = { return 3; };\n";
/* a CHANGED EXPORTED interface (extra export) → leaf's .wwi md5 changes. */
static const char *leaf_v2 =
"package leaf;\n"
"export fn base() i32 = { return 3; };\n"
"export fn extra() i32 = { return 9; };\n";
static const char *mid_v1 =
"package mid;\n"
"import leaf;\n"
"export fn val() i32 = { return leaf.base() + 2; };\n";
/* mid source perturbed by a comment — busts mid's src md5 without changing
* codegen (so the program result is preserved). */
static const char *mid_v2 =
"package mid;\n"
"import leaf;\n"
"// 5b src-edit bust probe\n"
"export fn val() i32 = { return leaf.base() + 2; };\n";
static const char *root_src =
"package main;\n"
"import mid;\n"
"fn main() i32 = { return mid.val(); };\n";
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
int fail = 0;
char td[64], cmd[8192];
char rootww[1024], midww[1024], leafww[1024];
char scratch[1024], prog[1024];
char cache_cs[1024], cache_ww[1024];
snprintf(td, sizeof td, "/tmp/wwpkgc_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
snprintf(cmd, sizeof cmd, "mkdir -p %s/mid %s/leaf", td, td);
runwait(cmd);
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
snprintf(midww, sizeof midww, "%s/mid/mid.ww", td);
snprintf(leafww, sizeof leafww, "%s/leaf/leaf.ww", td);
snprintf(prog, sizeof prog, "%s/p", td);
snprintf(scratch, sizeof scratch, "%s/p.sepwork", td);
snprintf(cache_cs, sizeof cache_cs, "%s/cache.cs", td);
snprintf(cache_ww, sizeof cache_ww, "%s/cache.ww", td);
if (write_file(rootww, root_src) || write_file(midww, mid_v1) ||
write_file(leafww, leaf_v1)) { fail++; goto out; }
struct { const char *drv, *cache; } stg[] = {
{ "ww", cache_cs },
{ "ww_ww", cache_ww },
};
for (int s = 0; s < 2; s++) {
const char *drv = stg[s].drv;
const char *cache = stg[s].cache;
char midkey[1100];
snprintf(midkey, sizeof midkey, "%s/mid/P.key", cache);
/* The cstage driver rm -rf's the scratch each build; the wwstage
* driver only mkdir's it (pre-existing #58(a) asymmetry), so a
* stale <pkg>.s could persist and defeat the MISS/HIT sentinel.
* The gate owns its scratch: wipe it before every build so a
* fresh <pkg>.s reflects exactly the last compile. */
#define BUILD() do { \
snprintf(cmd, sizeof cmd, \
"rm -rf %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep " \
"-o %s %s >/dev/null 2>&1", scratch, cache, bin, drv, prog, \
rootww); \
} while (0)
#define SDOT(pkg) sdot(scratch, (pkg))
/* fresh cache for this stage */
snprintf(cmd, sizeof cmd, "rm -rf %s", cache);
runwait(cmd);
/* sources back to v1 (a prior stage's restore may have left them) */
write_file(midww, mid_v1);
write_file(leafww, leaf_v1);
/* leg 1 — MISS: cold build compiles both, prog runs */
BUILD();
if (runwait(cmd) != 0) {
fprintf(stderr, "pkgcache FAIL[%s]: cold build failed\n", drv);
fail++; continue;
}
if (!SDOT("leaf") || !SDOT("mid")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg1 MISS — leaf/mid did "
"not compile\n", drv);
fail++;
}
if (runwait(prog) != EXPECT_EXIT) {
fprintf(stderr, "pkgcache FAIL[%s]: leg1 prog exit != %d\n",
drv, EXPECT_EXIT);
fail++;
}
/* leg 2 — HIT + non-vacuity: unchanged rebuild skips both */
BUILD();
runwait(cmd);
if (SDOT("leaf") || SDOT("mid")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg2 HIT — recompiled an "
"unchanged package (cache vacuous)\n", drv);
fail++;
}
if (runwait(prog) != EXPECT_EXIT) {
fprintf(stderr, "pkgcache FAIL[%s]: leg2 prog exit != %d "
"(cached artifact unusable)\n", drv, EXPECT_EXIT);
fail++;
}
/* leg 3 — BUST source edit: mid MISS, leaf still HIT */
write_file(midww, mid_v2);
BUILD();
runwait(cmd);
if (!SDOT("mid") || SDOT("leaf")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg3 src-edit — expected "
"mid MISS + leaf HIT\n", drv);
fail++;
}
/* restore + reseed, then prove HIT returns */
write_file(midww, mid_v1);
BUILD(); runwait(cmd); /* reseeds the v1 key (MISS) */
BUILD(); runwait(cmd); /* must HIT now */
if (SDOT("mid")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg3 non-vacuity — mid did "
"not HIT after restore\n", drv);
fail++;
}
/* leg 4 — BUST dep .wwi: change leaf interface → importer mid MISSES */
write_file(leafww, leaf_v2);
BUILD();
runwait(cmd);
if (!SDOT("leaf") || !SDOT("mid")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg4 dep-.wwi — leaf "
"interface change did not bust importer mid\n", drv);
fail++;
}
write_file(leafww, leaf_v1);
BUILD(); runwait(cmd); /* reseed v1 */
BUILD(); runwait(cmd); /* must HIT now */
if (SDOT("leaf") || SDOT("mid")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg4 non-vacuity — did not "
"HIT after restore\n", drv);
fail++;
}
/* leg 5 — BUST compiler md5: corrupt the stored mid w6c line */
if (mutate_keyline(midkey, "w6c ") != 0) {
fprintf(stderr, "pkgcache FAIL[%s]: leg5 — no w6c line in "
"mid/P.key\n", drv);
fail++;
}
BUILD();
runwait(cmd);
if (!SDOT("mid") || SDOT("leaf")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg5 compiler-md5 — corrupt "
"w6c line did not bust mid (only)\n", drv);
fail++;
}
BUILD(); runwait(cmd); /* key rewritten → must HIT */
if (SDOT("mid")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg5 non-vacuity — mid did "
"not HIT after key rewrite\n", drv);
fail++;
}
/* leg 6 — BUST flag string: corrupt the stored mid flags line */
if (mutate_keyline(midkey, "flags ") != 0) {
fprintf(stderr, "pkgcache FAIL[%s]: leg6 — no flags line in "
"mid/P.key\n", drv);
fail++;
}
BUILD();
runwait(cmd);
if (!SDOT("mid")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg6 flags — corrupt flags "
"line did not bust mid\n", drv);
fail++;
}
BUILD(); runwait(cmd);
if (SDOT("mid")) {
fprintf(stderr, "pkgcache FAIL[%s]: leg6 non-vacuity — mid did "
"not HIT after key rewrite\n", drv);
fail++;
}
#undef BUILD
#undef SDOT
}
/* cross-stage byte-id (rule 10): cached OUTPUTS identical cs==ww. The
* caches end in the v1/HIT state from each stage's leg 6 tail. */
{
const char *pkgs[] = { "mid", "leaf" };
const char *arts[] = { "P.o", "P.wwi" };
for (int i = 0; i < 2; i++)
for (int k = 0; k < 2; k++) {
char a[1100], b[1100];
snprintf(a, sizeof a, "%s/%s/%s", cache_cs, pkgs[i], arts[k]);
snprintf(b, sizeof b, "%s/%s/%s", cache_ww, pkgs[i], arts[k]);
if (files_eq(a, b) != 0) {
fprintf(stderr, "pkgcache FAIL: cs != ww for cached "
"%s/%s (rule 10)\n", pkgs[i], arts[k]);
fail++;
}
}
}
out:
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
if (fail) {
fprintf(stderr, "pkgcache: %d check(s) failed\n", fail);
return 1;
}
printf("pkgcache: 3-level root->mid->leaf via build_one_sep — MISS "
"compiles, HIT skips (non-vacuous, prog runs), and key busts on each "
"input class (src / dep .wwi / compiler md5 / flags) — both stages; "
"cached P.o/P.wwi byte-identical cs==ww\n");
return 0;
}