The out/.pkgcache content-keyed store copied each artifact IN-PLACE
(cp -f / copyfile) to the fixed paths P.wwi/P.o/P.key. Key-last gave
crash-consistency but NOT concurrent-read safety: two same-stage builds
of a shared lib pkg (rt/time/os) target one out/.pkgcache/<pkg>/P.{wwi,o};
once an early finisher writes P.key, a later build's cache_lookup copies
P.wwi/P.o while a mid-finisher is still mid-write -> torn read -> corrupt
link / cs!=ww. The key is content-only, so it is purely the non-atomic
write.
Fix (Go-build-cache pattern, both stages in lock-step, rule 10): write
each artifact to a per-pid same-dir temp (P.wwi.tmp.<pid> etc.) then
rename() into place. Same dir => rename is atomic (cross-fs is not);
per-pid temp => concurrent writers don't clobber each other mid-copy;
content-keyed => last-writer-wins is byte-identical. Key renamed LAST so
a reader that sees the new key always finds complete artifacts. On any
mid-store error the per-pid temps are unlinked so a failed store leaves
no litter (cstage goto cleanup; wwstage cachermtmp helper).
cstage cmd/ww/main.c cache_store: libc rename(2) + getpid().
wwstage selfhost/cmd/ww/main.ww cachestore: new os.rename + cachetmp.
lib/os/os.ww: add rename(2) (RENAME=82), ref/hare/os/os.ha:17 -- returns
raw i32 errno like sibling remove/mkdir/rmdir (ww's os is the flat
syscall floor, no fs:: layer); a second pathbuf2 slot holds newpath
since kpath's single pathbuf can't carry both paths.
cache_lookup is unchanged: it reads cache->private scratch, and an atomic
source is never torn.
The torn-read race is closed BY CONSTRUCTION; a deterministic behavioral
regression-guard isn't feasible through the product build path (content-
keying => concurrent COLD builds all MISS+STORE, never HIT-read a mid-store
entry; a warm cache is never re-stored). The deferred white-box guard is
TASK #105. A WHY-comment at both fix sites records this.
Tests: 989_sepbuild_run KEEPS its private per-pid WW_PKGCACHE -- the
comment is corrected: the pin is NOT a torn-read mask (closed by
construction) but cold-compile isolation for the test's INTERMEDIATE
(.s/.unit.ww) byte-id compare, which a cache HIT legitimately skips
producing. The former 989_pkgcache_atomic_run is renamed to
989_pkgcache_concurrent_run and HONESTLY relabeled: it is a concurrent
shared-cache build-correctness smoke (N concurrent --sep builds sharing
one cache -> every binary byte-identical to an isolated reference + correct
run, both stages), NOT a torn-read/atomicity proof (a review revert-
experiment proved the original claim vacuous). Shrunk to 4 concurrent
builds x 1 batch x both stages. COLD/dev-only, off every byte-id/bootstrap
gate.
selfhost/cmd/ww/main.combined.ww remains stale (its writer was deleted at
the M4 E3-C1 flip; #90 deletes the file) -- not regenerated.
make test: all 445 passed; make sizelint clean; 990-997 byte-id hold.
389 lines
13 KiB
C
389 lines
13 KiB
C
/*
|
|
* 989_sepbuild_run — M3-tail commit-3 `ww build --sep` driver gate (#46).
|
|
*
|
|
* Certifies build_one_sep (cmd/ww/main.c + selfhost/cmd/ww/main.ww twin)
|
|
* END-TO-END through the REAL `ww` / `ww_ww` drivers on a REAL lib chain
|
|
* (NOT the 989_m3sep synth fixture, which exercises the M3-CORE codegen
|
|
* via a hand harness). The driver adds ONLY orchestration around that
|
|
* proven core: discover_deps, reverse-topo, the transitive `w6c -c -I`
|
|
* producer loop, and a flat `w6l`.
|
|
*
|
|
* Chosen chain (smallest real multi-package graph with a genuine
|
|
* multi-level dep edge): root → os → { rt, time }. rt and time are
|
|
* true leaves; os imports both AND its public interface exposes a
|
|
* transitive type (os returns/takes time.instant in sibling protos), so
|
|
* the root's sep-unit needs the TRANSITIVE `.wwi` closure for name
|
|
* resolution — direct-deps-only does not type-check (the lead-ratified
|
|
* superseding of rob-c3-spec §1.3). The graph thus exercises:
|
|
* - discovery of the transitive package set,
|
|
* - reverse-topo dep-first ordering (time/rt before os before root),
|
|
* - the transitive-closure `.wwi` prepend tagged by dotted path,
|
|
* - one-pass `w6c -c -I` (consumer + producer) per package,
|
|
* - flat `w6l` of the `.o` set + libwwrt.a.
|
|
*
|
|
* Asserts (all COLD — `<stem>.sepwork` scratch is wiped each run):
|
|
* 1. Build + run, BOTH stages → exit EXPECT_EXIT (cross-boundary
|
|
* os.getpid resolves + links across sep `.o`s; the program runs).
|
|
* 2. cs==ww (rule 10): per-package `.s`/`.wwi`/`.unit.ww` AND the final
|
|
* binary are byte-identical between `ww --sep` and `ww_ww --sep`.
|
|
* 3. KEYSTONE (load-bearing, §3.1) THROUGH the driver: for each non-leaf
|
|
* package P, transform the driver's own `<P>.unit.ww` by substituting
|
|
* each dep's `.wwi` section with that dep's full directory BODIES,
|
|
* `w6c -c` it, and cmp vs the driver's `<P>.s`. Byte-identical proves
|
|
* the `.wwi` conveys exactly the dep facts P's codegen needs. (Leaves
|
|
* rt/time have no deps → keystone is vacuous; checked on os + root.)
|
|
*
|
|
* Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are
|
|
* `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models
|
|
* 989_m3sep_run.c conventions; 989 prefix per the m3sep/m2wwi precedent.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/stat.h>
|
|
#include <dirent.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
|
|
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 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
|
|
strs_cmp(const void *a, const void *b)
|
|
{
|
|
return strcmp(*(const char *const *)a, *(const char *const *)b);
|
|
}
|
|
|
|
/* Concatenate a package directory's *.ww bodies (less *test.ww and
|
|
* *.combined.ww, byte-sorted) into `out`. Mirrors the driver's body
|
|
* enumeration so the substituted bodies-unit matches the sep-unit
|
|
* structurally. */
|
|
static int
|
|
append_dir_bodies(FILE *out, const char *dir)
|
|
{
|
|
DIR *d = opendir(dir);
|
|
if (!d) return -1;
|
|
char *names[256];
|
|
int n = 0;
|
|
struct dirent *ent;
|
|
while ((ent = readdir(d)) != NULL && n < 256) {
|
|
const char *nm = ent->d_name;
|
|
size_t nl = strlen(nm);
|
|
if (nl <= 3 || strcmp(nm + nl - 3, ".ww") != 0) continue;
|
|
if (nl >= 7 && strcmp(nm + nl - 7, "test.ww") == 0) continue;
|
|
if (nl >= 12 && strcmp(nm + nl - 12, ".combined.ww") == 0) continue;
|
|
names[n++] = strdup(nm);
|
|
}
|
|
closedir(d);
|
|
if (n > 1) qsort(names, (size_t)n, sizeof names[0], strs_cmp);
|
|
int rc = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
char fp[1024];
|
|
snprintf(fp, sizeof fp, "%s/%s", dir, names[i]);
|
|
char *b = NULL;
|
|
size_t bn = 0;
|
|
if (slurp(fp, &b, &bn) == 0) {
|
|
fwrite(b, 1, bn, out);
|
|
fputc('\n', out);
|
|
free(b);
|
|
} else rc = -1;
|
|
free(names[i]);
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* Transform the driver's sep-unit at `unitf` into a bodies-unit at
|
|
* `bodiesf`: every `//ww:module <path>` dep section (its `.wwi` content)
|
|
* is replaced by <path>'s full directory bodies; the trailing
|
|
* `//ww:module-reset` primary body is copied verbatim. The result is the
|
|
* SAME unit P would compile with deps-as-bodies — the keystone's other
|
|
* arm. <libdir> roots the dotted-path → dir mapping (dots → slashes). */
|
|
static int
|
|
compose_bodies(const char *unitf, const char *libdir, const char *bodiesf)
|
|
{
|
|
char *buf = NULL;
|
|
size_t n = 0;
|
|
if (slurp(unitf, &buf, &n) < 0) return -1;
|
|
FILE *out = fopen(bodiesf, "wb");
|
|
if (!out) { free(buf); return -1; }
|
|
int rc = 0;
|
|
size_t i = 0;
|
|
while (i < n) {
|
|
size_t j = i;
|
|
while (j < n && buf[j] != '\n') j++;
|
|
size_t linelen = j - i;
|
|
if (strncmp(buf + i, "//ww:module-reset", 17) == 0) {
|
|
/* primary body: copy this line + everything after. */
|
|
fwrite(buf + i, 1, n - i, out);
|
|
break;
|
|
}
|
|
if (strncmp(buf + i, "//ww:module ", 12) == 0) {
|
|
/* dep section: emit the directive, then dir bodies;
|
|
* skip the original `.wwi` content to the next
|
|
* directive. */
|
|
fwrite(buf + i, 1, linelen, out);
|
|
fputc('\n', out);
|
|
char path[256];
|
|
size_t pl = linelen - 12;
|
|
if (pl >= sizeof path) pl = sizeof path - 1;
|
|
memcpy(path, buf + i + 12, pl);
|
|
path[pl] = '\0';
|
|
char dir[1024];
|
|
char form[256];
|
|
size_t k;
|
|
for (k = 0; path[k]; k++)
|
|
form[k] = (path[k] == '.') ? '/' : path[k];
|
|
form[k] = '\0';
|
|
snprintf(dir, sizeof dir, "%s/%s", libdir, form);
|
|
if (append_dir_bodies(out, dir) < 0) rc = -1;
|
|
/* advance past the wwi content to the next directive. */
|
|
i = j + 1;
|
|
while (i < n) {
|
|
if (strncmp(buf + i, "//ww:module", 11) == 0) break;
|
|
size_t e = i;
|
|
while (e < n && buf[e] != '\n') e++;
|
|
i = (e < n) ? e + 1 : n;
|
|
}
|
|
continue;
|
|
}
|
|
i = j + 1;
|
|
}
|
|
fclose(out);
|
|
free(buf);
|
|
return rc;
|
|
}
|
|
|
|
static const char *root_src =
|
|
"package main;\n"
|
|
"import os;\n"
|
|
"fn main() i32 = { return os.getpid() - os.getpid() + 7; };\n";
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
const char *bin = absbin();
|
|
if (!bin) return 1;
|
|
char td[64], cmd[8192], libdir[2048];
|
|
int fail = 0;
|
|
|
|
snprintf(libdir, sizeof libdir, "%s/../../lib", bin);
|
|
snprintf(td, sizeof td, "/tmp/wwsep_%d", getpid());
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
|
runwait(cmd);
|
|
mkdir(td, 0755);
|
|
|
|
/* This test byte-id compares the per-package INTERMEDIATES (.s, .unit.ww)
|
|
* between the cs and ww legs, which a cache HIT legitimately skips
|
|
* producing (a HIT copies only .wwi/.o and bypasses compose+w6c). So the
|
|
* compare requires every package to COLD-compile: pin a private per-pid
|
|
* cache to force misses. This is orthogonal to #104 — the atomic temp+
|
|
* rename closed the torn-read race (by construction; #105 is the deferred
|
|
* white-box guard). Concurrent shared-cache build correctness is smoked by
|
|
* 989_pkgcache_concurrent_run. Here the pin is for intermediate-compare
|
|
* isolation, not a torn-read mask. */
|
|
char cachedir[80];
|
|
snprintf(cachedir, sizeof cachedir, "%s/pkgcache", td);
|
|
setenv("WW_PKGCACHE", cachedir, 1);
|
|
|
|
char rootww[1024];
|
|
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
|
|
if (write_file(rootww, root_src)) { fail++; goto out; }
|
|
|
|
/* The two driver stages and their scratch dirs. All per-package paths
|
|
* are rebuilt from `td` (a small fixed buffer) + the stage tag rather
|
|
* than chained through a large path buffer, so the snprintfs are
|
|
* provably non-truncating (warning-clean, like 989_m3sep_run). */
|
|
struct { const char *drv, *tag; char prog[1024]; }
|
|
stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } };
|
|
|
|
for (int s = 0; s < 2; s++) {
|
|
snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag);
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 240 %s/%s build --sep -o %s %s >/dev/null 2>&1",
|
|
bin, stg[s].drv, stg[s].prog, rootww);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "sepbuild FAIL: %s build --sep\n", stg[s].drv);
|
|
fail++;
|
|
continue;
|
|
}
|
|
int rc = runwait(stg[s].prog);
|
|
if (rc != EXPECT_EXIT) {
|
|
fprintf(stderr, "sepbuild FAIL: %s prog exit=%d expected %d\n",
|
|
stg[s].drv, rc, EXPECT_EXIT);
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
/* reverse-topo correctness is implicit: every package compiled (a
|
|
* dep's `.wwi` existed before its importer) → build succeeded above.
|
|
* Assert the discovered package set materialized in the scratch. */
|
|
const char *pkgs[] = { "time", "rt", "os", "__root" };
|
|
for (int i = 0; i < 4; i++) {
|
|
char p[1024];
|
|
/* #69: the root is compiled WITHOUT `-I`, so it produces no `.wwi`
|
|
* (its interface is never consumed); assert its `.s` materialized
|
|
* instead. Dep packages still emit a `.wwi`. */
|
|
int is_root = (strcmp(pkgs[i], "__root") == 0);
|
|
snprintf(p, sizeof p, "%s/prog.cs.sepwork/%s%s", td, pkgs[i],
|
|
is_root ? ".s" : ".wwi");
|
|
if (access(p, 0) != 0) {
|
|
fprintf(stderr, "sepbuild FAIL: missing %s%s (discovery/topo)\n",
|
|
pkgs[i], is_root ? ".s" : ".wwi");
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
/* cs==ww (rule 10): per-package .s/.wwi/.unit.ww + final binary. The
|
|
* root has no `.wwi` post-#69, so it is excluded from the .wwi compare. */
|
|
for (int i = 0; i < 4; i++) {
|
|
const char *suf[] = { ".s", ".wwi", ".unit.ww" };
|
|
for (int k = 0; k < 3; k++) {
|
|
if (strcmp(pkgs[i], "__root") == 0 && strcmp(suf[k], ".wwi") == 0)
|
|
continue;
|
|
char a[1024], b[1024];
|
|
snprintf(a, sizeof a, "%s/prog.%s.sepwork/%s%s",
|
|
td, stg[0].tag, pkgs[i], suf[k]);
|
|
snprintf(b, sizeof b, "%s/prog.%s.sepwork/%s%s",
|
|
td, stg[1].tag, pkgs[i], suf[k]);
|
|
if (files_eq(a, b) != 0) {
|
|
fprintf(stderr, "sepbuild FAIL: cs!=ww for %s%s (rule 10)\n",
|
|
pkgs[i], suf[k]);
|
|
fail++;
|
|
}
|
|
}
|
|
}
|
|
if (files_eq(stg[0].prog, stg[1].prog) != 0) {
|
|
fprintf(stderr, "sepbuild FAIL: cs exe != ww exe (rule 10)\n");
|
|
fail++;
|
|
}
|
|
|
|
/* KEYSTONE through the driver: bodies-unit == sep-unit codegen, for
|
|
* each non-leaf package (os, root). Transform the driver's own
|
|
* <P>.unit.ww (deps as .wwi) into a bodies-unit (deps as full dir
|
|
* bodies), `w6c -c` it, cmp vs the driver's <P>.s. */
|
|
const char *keypkgs[] = { "os", "__root" };
|
|
for (int i = 0; i < 2; i++) {
|
|
char unitf[1024], bodiesf[1024], bodies_s[1024], sep_s[1024];
|
|
snprintf(unitf, sizeof unitf, "%s/prog.cs.sepwork/%s.unit.ww", td, keypkgs[i]);
|
|
snprintf(bodiesf, sizeof bodiesf, "%s/%s.bodies.ww", td, keypkgs[i]);
|
|
snprintf(bodies_s, sizeof bodies_s, "%s/%s.bodies.s", td, keypkgs[i]);
|
|
snprintf(sep_s, sizeof sep_s, "%s/prog.cs.sepwork/%s.s", td, keypkgs[i]);
|
|
if (compose_bodies(unitf, libdir, bodiesf) < 0) {
|
|
fprintf(stderr, "sepbuild FAIL: compose bodies for %s\n", keypkgs[i]);
|
|
fail++;
|
|
continue;
|
|
}
|
|
snprintf(cmd, sizeof cmd,
|
|
"timeout 240 %s/w6c -c -o %s %s >/dev/null 2>&1",
|
|
bin, bodies_s, bodiesf);
|
|
if (runwait(cmd) != 0) {
|
|
fprintf(stderr, "sepbuild FAIL: %s bodies -c\n", keypkgs[i]);
|
|
fail++;
|
|
continue;
|
|
}
|
|
if (files_eq(bodies_s, sep_s) != 0) {
|
|
fprintf(stderr, "sepbuild FAIL: %s — bodies.s != driver sep.s "
|
|
"(the .wwi does not convey the dep facts P needs)\n", keypkgs[i]);
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
/* run --sep symmetry: E3-C1 makes sep the sole compile path, so
|
|
* `run` now genuinely sep-compiles-then-runs (the old "no sep run
|
|
* path → loud-reject exit 2" is gone with the amalgamator). BOTH
|
|
* stages must build+run the cross-boundary fixture identically
|
|
* (rule 10) and exit EXPECT_EXIT — `--sep` is an accepted no-op. */
|
|
{
|
|
int rc_cs, rc_ww;
|
|
snprintf(cmd, sizeof cmd,
|
|
"%s/ww run --sep %s >/dev/null 2>&1", bin, rootww);
|
|
rc_cs = runwait(cmd);
|
|
snprintf(cmd, sizeof cmd,
|
|
"%s/ww_ww run --sep %s >/dev/null 2>&1", bin, rootww);
|
|
rc_ww = runwait(cmd);
|
|
if (rc_cs != rc_ww || rc_cs != EXPECT_EXIT) {
|
|
fprintf(stderr, "sepbuild FAIL: run --sep cs=%d ww=%d "
|
|
"(both must run via sep, exit %d)\n", rc_cs, rc_ww,
|
|
EXPECT_EXIT);
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
out:
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
|
|
runwait(cmd);
|
|
if (fail) {
|
|
fprintf(stderr, "sepbuild: %d check(s) failed\n", fail);
|
|
return 1;
|
|
}
|
|
printf("sepbuild: real chain root->os->{rt,time} via build_one_sep — "
|
|
"build+run (exit %d) + cs==ww per-pkg .s/.wwi/.unit + final binary "
|
|
"+ transitive-topo discovery + keystone bodies==.wwi (os,root)\n",
|
|
EXPECT_EXIT);
|
|
return 0;
|
|
}
|