Files
ww/test/wcc/989_separchive_run.c
Hojun-Cho 62c7771594 wcc/ww: self-hosted deterministic ar-writer + archive dup-detect (M3-tail c5a, #62)
Per-package .a archives are written by a self-hosted deterministic ar
writer (zeroed mtime/uid/gid, fixed mode 100644, stable member order)
so cstage and wwstage emit byte-identical archives. The linker
force-loads the root .o positionally and pulls deps from .a; a
post-pull PASS-3 over unloaded members reports duplicate symbols
through the archive (#31). Both stages mirrored (cmd/ + selfhost/).

USER-ruled D2 (self-hosted ar writer); pike P1/P2 link model. Gate
989_separchive_run proves cs.a==ww.a byte-identity, 3x-determinism,
link-consumes-.a (exit 7), and the masked-dup-through-.a loud fire.
2026-06-16 14:54:19 +09:00

326 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 989_separchive_run — M3-tail commit-5a gate (#46, task #62): the
* per-package `.a` substrate + its archive-path #31 dup-detect, both
* stages, COLD. Commit 5a makes `ww build --sep` wrap each DEP package's
* `.o` in a deterministic single-member `.a` (cstage archive_o /
* wwstage archiveo) and link the ROOT as a positional `.o` (force-loaded)
* + dep `.a` reverse-topo + libwwrt.a. The #31 dup the selective pull
* would mask is caught by a post-pull PASS 3 in w6l/w6l_ww load_archive.
*
* Legs (all COLD — `<stem>.sepwork` is wiped each run):
* 1. POSITIVE: root→helper builds + runs exit 7, BOTH stages, through
* the `.a` link path (root `.o` + helper `.a`).
* 2. ★ DETERMINISM (ken, load-bearing): re-archive the same package 3×
* → byte-identical `.a` (proves zeroed mtime/uid/gid + fixed mode +
* fixed member name; a floating md5 would poison the 5b cache key).
* 3. ★ cs `.a` == ww `.a` (rule 10, the NEW byte-id substrate ken binds):
* the dep `.a` from `ww --sep` is byte-identical to the one from
* `ww_ww --sep`.
* 4. ★ #31 dup THROUGH the `.a` path (ken #263 + D3, the leg that proves
* PASS 3 closed the selective-pull hole): two DEP packages each export
* the same link symbol via `@symbol("dup_sym")`, referenced by root.
* One dep's member is pulled; the OTHER lands UNPULLED and defines an
* already-`defined` name — exactly what selective-pull skips. BOTH
* stages must (exit≠0) ∧ (stderr contains "duplicate symbol") ∧ (no
* partial binary). NON-VACUITY flip: give the second dep a DISTINCT
* symbol → both stages build + run (exit 3), proving the leg actually
* discriminates (without PASS 3 the dup leg would WRONGLY pass green).
* 5. ACHIEVABLE-parity posture (same as c4's 989_sepcycle_dup, because
* #61 is a separate commit): cs vs ww is `both exit≠0 ∧ both stderrs
* contain "duplicate symbol"` — NOT exact-stderr-equal (cstage names
* `<path>: duplicate symbol <sym>`, wwstage the bare form).
*
* Light wwstage-driver test (CLAUDE.md rule 14): all fixtures + scratch
* live under /tmp, COLD each run. Models 989_sepbuild_run.c 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
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
file_has(const char *path, const char *needle)
{
char *b = NULL;
size_t n = 0;
if (slurp(path, &b, &n) < 0) return 0;
int found = (strstr(b, needle) != NULL);
free(b);
return found;
}
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;
}
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
char td[64], cmd[8192];
int fail = 0;
snprintf(td, sizeof td, "/tmp/wwar_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
mkdir(td, 0755);
/* ---- root → helper fixture (one dep `.a`) ------------------------ */
char helpdir[1024], helpww[1100], rootww[1100];
snprintf(helpdir, sizeof helpdir, "%s/helper", td);
mkdir(helpdir, 0755);
snprintf(helpww, sizeof helpww, "%s/h.ww", helpdir);
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
if (write_file(helpww,
"package helper;\n"
"export fn val() i32 = { return 7; };\n") ||
write_file(rootww,
"package main;\n"
"import helper;\n"
"fn main() i32 = { return helper.val(); };\n")) {
fail++; goto out;
}
struct { const char *drv, *tag; char prog[1024]; }
stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } };
/* Leg 1: POSITIVE build + run through the `.a` path, both stages. */
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 -I %s -o %s %s >/dev/null 2>&1",
bin, stg[s].drv, td, stg[s].prog, rootww);
if (runwait(cmd) != 0) {
fprintf(stderr, "separchive FAIL: %s build --sep\n", stg[s].drv);
fail++;
continue;
}
int rc = runwait(stg[s].prog);
if (rc != EXPECT_EXIT) {
fprintf(stderr, "separchive FAIL: %s prog exit=%d expected %d\n",
stg[s].drv, rc, EXPECT_EXIT);
fail++;
}
}
/* Root is a positional `.o` (force-loaded), so NO __root.a exists;
* the dep is the only `.a`. Assert the layout the driver produced. */
{
char roota[1100], rooto[1100], helpa[1100];
snprintf(roota, sizeof roota, "%s/prog.cs.sepwork/__root.a", td);
snprintf(rooto, sizeof rooto, "%s/prog.cs.sepwork/__root.o", td);
snprintf(helpa, sizeof helpa, "%s/prog.cs.sepwork/helper.a", td);
if (access(roota, 0) == 0) {
fprintf(stderr, "separchive FAIL: root wrapped in .a "
"(should stay positional .o)\n");
fail++;
}
if (access(rooto, 0) != 0) {
fprintf(stderr, "separchive FAIL: missing root .o\n");
fail++;
}
if (access(helpa, 0) != 0) {
fprintf(stderr, "separchive FAIL: missing dep helper.a\n");
fail++;
}
}
/* Leg 3: cs `.a` == ww `.a` (rule 10, the new byte-id substrate). */
{
char a[1100], b[1100];
snprintf(a, sizeof a, "%s/prog.cs.sepwork/helper.a", td);
snprintf(b, sizeof b, "%s/prog.ww.sepwork/helper.a", td);
if (files_eq(a, b) != 0) {
fprintf(stderr, "separchive FAIL: cs helper.a != ww helper.a "
"(rule 10 .a byte-id)\n");
fail++;
}
}
/* Leg 2: DETERMINISM — re-archive the same package 3× (cold each),
* the `.a` must be byte-identical (zeroed mtime/uid/gid, fixed mode,
* fixed member name). Copy each build's helper.a aside, compare. */
{
char det[3][1100];
int ok = 1;
for (int i = 0; i < 3; i++) {
char prog[1100];
snprintf(prog, sizeof prog, "%s/det%d", td, i);
snprintf(cmd, sizeof cmd,
"timeout 240 %s/ww build --sep -I %s -o %s %s >/dev/null 2>&1",
bin, td, prog, rootww);
if (runwait(cmd) != 0) { ok = 0; break; }
snprintf(det[i], sizeof det[i], "%s/det%d.sepwork/helper.a", td, i);
}
if (!ok) {
fprintf(stderr, "separchive FAIL: determinism build\n");
fail++;
} else if (files_eq(det[0], det[1]) != 0 ||
files_eq(det[1], det[2]) != 0) {
fprintf(stderr, "separchive FAIL: .a not deterministic across "
"3 builds (floating md5 → poisons 5b cache)\n");
fail++;
}
}
/* ---- Leg 4: #31 dup THROUGH the `.a` path (PASS 3) --------------- */
/* Two DEP packages each export the SAME link symbol via @symbol;
* root references both. One member is pulled, the other lands
* UNPULLED defining an already-`defined` name → the masked dup PASS 3
* catches (#53 path-qualifies normal exports, so @symbol is the
* cleanest forced cross-package clash). */
char pkgad[1024], pkgbd[1024], paww[1100], pbww[1100], droot[1100];
snprintf(pkgad, sizeof pkgad, "%s/pkga", td);
snprintf(pkgbd, sizeof pkgbd, "%s/pkgb", td);
mkdir(pkgad, 0755); mkdir(pkgbd, 0755);
snprintf(paww, sizeof paww, "%s/a.ww", pkgad);
snprintf(pbww, sizeof pbww, "%s/b.ww", pkgbd);
snprintf(droot, sizeof droot, "%s/droot.ww", td);
if (write_file(paww,
"package pkga;\n"
"@symbol(\"dup_sym\") export fn afn() i32 = { return 1; };\n") ||
write_file(pbww,
"package pkgb;\n"
"@symbol(\"dup_sym\") export fn bfn() i32 = { return 2; };\n") ||
write_file(droot,
"package main;\n"
"import pkga;\n"
"import pkgb;\n"
"fn main() i32 = { return pkga.afn() + pkgb.bfn(); };\n")) {
fail++; goto out;
}
for (int s = 0; s < 2; s++) {
char prog[1100], errf[1100];
snprintf(prog, sizeof prog, "%s/dup.%s", td, stg[s].tag);
snprintf(errf, sizeof errf, "%s/dup.%s.err", td, stg[s].tag);
snprintf(cmd, sizeof cmd,
"timeout 240 %s/%s build --sep -I %s -o %s %s >/dev/null 2>%s",
bin, stg[s].drv, td, prog, droot, errf);
int rc = runwait(cmd);
if (rc == 0) {
fprintf(stderr, "separchive FAIL: %s accepted a masked "
"cross-pkg dup through .a (exit 0 — PASS 3 missing?)\n",
stg[s].drv);
fail++;
}
if (!file_has(errf, "duplicate symbol")) {
fprintf(stderr, "separchive FAIL: %s missing 'duplicate "
"symbol' on the .a dup path\n", stg[s].drv);
fail++;
}
if (access(prog, 0) == 0) {
fprintf(stderr, "separchive FAIL: %s produced a partial "
"binary on the dup reject\n", stg[s].drv);
fail++;
}
}
/* Leg 4 NON-VACUITY: give pkgb a DISTINCT symbol → no collision →
* both stages build + run (exit 3 = 1 + 2). Proves the dup leg
* actually discriminates (not a vacuous always-fail). */
if (write_file(pbww,
"package pkgb;\n"
"@symbol(\"uniq_sym\") export fn bfn() i32 = { return 2; };\n")) {
fail++; goto out;
}
for (int s = 0; s < 2; s++) {
char prog[1100];
snprintf(prog, sizeof prog, "%s/ok.%s", td, stg[s].tag);
snprintf(cmd, sizeof cmd,
"timeout 240 %s/%s build --sep -I %s -o %s %s >/dev/null 2>&1",
bin, stg[s].drv, td, prog, droot);
if (runwait(cmd) != 0) {
fprintf(stderr, "separchive FAIL(non-vacuity): %s could not "
"build the distinct-symbol graph\n", stg[s].drv);
fail++;
continue;
}
int rc = runwait(prog);
if (rc != 3) {
fprintf(stderr, "separchive FAIL(non-vacuity): %s prog "
"exit=%d expected 3\n", stg[s].drv, rc);
fail++;
}
}
out:
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
runwait(cmd);
if (fail) {
fprintf(stderr, "separchive: %d check(s) failed\n", fail);
return 1;
}
printf("separchive: per-pkg .a (root=.o force-load, deps=.a) build+run "
"(exit %d) + .a determinism (3x identical) + cs.a==ww.a (rule 10) + "
"#31 masked-dup reject THROUGH .a (PASS 3, both stages loud+non-zero, "
"non-vacuity flip exit 3)\n", EXPECT_EXIT);
return 0;
}