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.
This commit is contained in:
2026-06-16 14:54:19 +09:00
parent 9cceb4ca8d
commit 62c7771594
8 changed files with 689 additions and 24 deletions

View File

@@ -3352,6 +3352,33 @@ fn loadarchive(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = {
m = m.mnext;
};
};
// Pass 3 (#31, task #62): catch a dup the selective pull MASKED. A
// member pass 2 left unloaded yet defining a name some other object
// already `defined` is exactly the cross-package collision archive
// selective-pull would silently skip. lookup (never intern) keeps
// the pull machinery untouched → byte-id-neutral on every clean
// link. Pulled-member and direct-`.o` dups stay caught in loadimage.
// ww has no weak symbols → a genuine dup. (The bare message matches
// loadimage's; the w6l path+name text divergence is #61, separate.)
let dm: *armember = head;
for (dm != nil) {
if (dm.loaded == 0) {
let de: *defent = dm.defs;
for (de != nil) {
let s: *lsym = lookup(l, de.name);
if (s != nil) {
if (s.defined != 0) {
let msg: str = "w6l: duplicate symbol\n";
os.write(2, msg.ptr, msg.len: u64);
l.errs += 1;
};
};
de = de.dnext;
};
};
dm = dm.mnext;
};
return 0;
};

View File

@@ -376,6 +376,33 @@ fn loadarchive(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = {
m = m.mnext;
};
};
// Pass 3 (#31, task #62): catch a dup the selective pull MASKED. A
// member pass 2 left unloaded yet defining a name some other object
// already `defined` is exactly the cross-package collision archive
// selective-pull would silently skip. lookup (never intern) keeps
// the pull machinery untouched → byte-id-neutral on every clean
// link. Pulled-member and direct-`.o` dups stay caught in loadimage.
// ww has no weak symbols → a genuine dup. (The bare message matches
// loadimage's; the w6l path+name text divergence is #61, separate.)
let dm: *armember = head;
for (dm != nil) {
if (dm.loaded == 0) {
let de: *defent = dm.defs;
for (de != nil) {
let s: *lsym = lookup(l, de.name);
if (s != nil) {
if (s.defined != 0) {
let msg: str = "w6l: duplicate symbol\n";
os.write(2, msg.ptr, msg.len: u64);
l.errs += 1;
};
};
de = de.dnext;
};
};
dm = dm.mnext;
};
return 0;
};

View File

@@ -4078,10 +4078,83 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
return 0;
};
// archiveo — twin of cmd/ww/main.c archive_o. Writes a deterministic
// single-member SysV ar archive at `apath` wrapping the `.o` at
// `objpath`. No armap / long-name table: w6l reads each member's ELF
// .symtab directly and skips '/'-named members, so a package `.a` is
// just the global magic + one 60-byte member header + the `.o` bytes
// (newline-padded to even). Zeroed mtime/uid/gid + fixed mode + a fixed
// member name make the bytes a pure function of the `.o` content →
// cstage `.a` == wwstage `.a` (rule 10) and a stable md5 for the 5b
// cache key.
fn archiveo(objpath: *u8, apath: *u8) i32 = {
let objp: *u8;
let objn: u64;
objp, objn = slurp(objpath);
if (objp == nil) {
cerr("ww --sep: cannot read object for archive\n");
return -1;
};
let pad: u64 = 0u64;
if ((objn & 1u64) != 0u64) { pad = 1u64; };
// sizelint-ok: 8B ar(5) magic + 60B member header are FILE-FORMAT
// constants, not type sizes (CLAUDE.md rule 13 carve-out).
let total: u64 = 8u64 + 60u64 + objn + pad;
let outs: []u8 = alloc([], total)!;
let out: *u8 = outs.ptr;
// 60-byte member header at offset 8, ASCII space-filled, fields
// left-justified; the 8-byte global magic precedes it. strinto
// copies a str's bytes (the working i32-index idiom) — a direct
// `out[i] = lit[i: i32]` store trips the cgen's str-index-rvalue arm.
let h: u64 = 8u64;
let j: u64 = 0u64;
for (j < 60u64) { out[h + j] = 32u8; j += 1u64; }; // 0x20 fill
strinto(out, 0u64, "!<arch>\n"); // global magic
strinto(out, h, "pkg.o/"); // name (GNU '/' terminator)
out[h + 16u64] = 48u8; // mtime "0" (zeroed → determinism)
out[h + 28u64] = 48u8; // uid "0"
out[h + 34u64] = 48u8; // gid "0"
strinto(out, h + 40u64, "100644"); // mode (fixed octal)
// size: decimal byte-count of the .o, left-justified at [48..58)
if (objn == 0u64) {
out[h + 48u64] = 48u8;
} else {
let ndig: u64 = 0u64;
let t: u64 = objn;
for (t > 0u64) { ndig += 1u64; t = t / 10u64; };
let d: u64 = ndig;
t = objn;
for (t > 0u64) {
d -= 1u64;
out[h + 48u64 + d] = ((t % 10u64): u8) + 48u8;
t = t / 10u64;
};
};
out[h + 58u64] = 96u8; // member-header magic 0x60
out[h + 59u64] = 10u8; // 0x0a
// the .o bytes, then a '\n' pad iff the size is odd (2-byte align).
let k: u64 = 0u64;
for (k < objn) { out[h + 60u64 + k] = objp[k]; k += 1u64; };
if (pad != 0u64) { out[h + 60u64 + objn] = 10u8; };
let fd: i32 = os.open(pathstr(apath),
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (fd < 0) {
cerr("ww --sep: cannot open archive\n");
return -1;
};
os.writeall(fd, out, total);
os.close(fd);
return 0;
};
// buildonesep — the --sep orchestration: discover deps, reverse-topo,
// the transitive producer loop (one `w6c -c -I` per package, dep-first),
// then a flat `w6l` of the `.o` set. Side files land in a cold
// `<stem>.sepwork` scratch dir. Twin of cstage build_one_sep.
// the transitive producer loop (one `w6c -c -I` per package, dep-first,
// each `.o` wrapped in its own deterministic per-package `.a`), then a
// reverse-topo `w6l` of the `.a` set + libwwrt.a. Side files land in a
// cold `<stem>.sepwork` scratch dir. Twin of cstage build_one_sep.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
@@ -4231,12 +4304,26 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
return 1;
};
};
// Wrap each DEP package's `.o` in its own deterministic `.a`
// (5a). The ROOT stays a positional `.o` (force-loaded — it's
// the build target), so `main` is defined before any archive is
// processed (mirrors buildone's root treatment). The link
// consumes `.o`/`.a`, never `.wwi`.
if (pi != root) {
let apath: *u8 = sepfname(g, pi, scratch, ".a");
if (archiveo(objf, apath) != 0) {
cerr("ww --sep: archive failed\n");
return 1;
};
};
oi += 1;
};
// Flat link: root.o first (order[norder-1]), deps after; libwwrt.a
// selectively pulls runtime symbols. argv: 4 fixed (w6l,-o,out) + 1
// per .o + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil.
// Reverse-topo link of the per-package `.a` set: root.a first
// (order[norder-1]), deps after, then libwwrt.a (which still
// selectively pulls only the runtime members a live undef needs).
// argv: 3 fixed (w6l,-o,out) + 1 per .a + 1 libwwrt + 2*nlibdirs
// + 2*nlibs + 1 nil.
let nldirs: i32 = 0;
let nllibs: i32 = 0;
let ldirs: **u8 = nil;
@@ -4256,7 +4343,10 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
let pos: i32 = 3;
let li: i32 = norder - 1;
for (li >= 0) {
largv[pos] = sepfname(g, order[li], scratch, ".o");
// root: positional `.o` (force-load); deps: `.a` (selective).
let suf: str = ".a";
if (order[li] == root) { suf = ".o"; };
largv[pos] = sepfname(g, order[li], scratch, suf);
pos += 1;
li -= 1;
};

View File

@@ -1204,10 +1204,83 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
return 0;
};
// archiveo — twin of cmd/ww/main.c archive_o. Writes a deterministic
// single-member SysV ar archive at `apath` wrapping the `.o` at
// `objpath`. No armap / long-name table: w6l reads each member's ELF
// .symtab directly and skips '/'-named members, so a package `.a` is
// just the global magic + one 60-byte member header + the `.o` bytes
// (newline-padded to even). Zeroed mtime/uid/gid + fixed mode + a fixed
// member name make the bytes a pure function of the `.o` content →
// cstage `.a` == wwstage `.a` (rule 10) and a stable md5 for the 5b
// cache key.
fn archiveo(objpath: *u8, apath: *u8) i32 = {
let objp: *u8;
let objn: u64;
objp, objn = slurp(objpath);
if (objp == nil) {
cerr("ww --sep: cannot read object for archive\n");
return -1;
};
let pad: u64 = 0u64;
if ((objn & 1u64) != 0u64) { pad = 1u64; };
// sizelint-ok: 8B ar(5) magic + 60B member header are FILE-FORMAT
// constants, not type sizes (CLAUDE.md rule 13 carve-out).
let total: u64 = 8u64 + 60u64 + objn + pad;
let outs: []u8 = alloc([], total)!;
let out: *u8 = outs.ptr;
// 60-byte member header at offset 8, ASCII space-filled, fields
// left-justified; the 8-byte global magic precedes it. strinto
// copies a str's bytes (the working i32-index idiom) — a direct
// `out[i] = lit[i: i32]` store trips the cgen's str-index-rvalue arm.
let h: u64 = 8u64;
let j: u64 = 0u64;
for (j < 60u64) { out[h + j] = 32u8; j += 1u64; }; // 0x20 fill
strinto(out, 0u64, "!<arch>\n"); // global magic
strinto(out, h, "pkg.o/"); // name (GNU '/' terminator)
out[h + 16u64] = 48u8; // mtime "0" (zeroed → determinism)
out[h + 28u64] = 48u8; // uid "0"
out[h + 34u64] = 48u8; // gid "0"
strinto(out, h + 40u64, "100644"); // mode (fixed octal)
// size: decimal byte-count of the .o, left-justified at [48..58)
if (objn == 0u64) {
out[h + 48u64] = 48u8;
} else {
let ndig: u64 = 0u64;
let t: u64 = objn;
for (t > 0u64) { ndig += 1u64; t = t / 10u64; };
let d: u64 = ndig;
t = objn;
for (t > 0u64) {
d -= 1u64;
out[h + 48u64 + d] = ((t % 10u64): u8) + 48u8;
t = t / 10u64;
};
};
out[h + 58u64] = 96u8; // member-header magic 0x60
out[h + 59u64] = 10u8; // 0x0a
// the .o bytes, then a '\n' pad iff the size is odd (2-byte align).
let k: u64 = 0u64;
for (k < objn) { out[h + 60u64 + k] = objp[k]; k += 1u64; };
if (pad != 0u64) { out[h + 60u64 + objn] = 10u8; };
let fd: i32 = os.open(pathstr(apath),
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (fd < 0) {
cerr("ww --sep: cannot open archive\n");
return -1;
};
os.writeall(fd, out, total);
os.close(fd);
return 0;
};
// buildonesep — the --sep orchestration: discover deps, reverse-topo,
// the transitive producer loop (one `w6c -c -I` per package, dep-first),
// then a flat `w6l` of the `.o` set. Side files land in a cold
// `<stem>.sepwork` scratch dir. Twin of cstage build_one_sep.
// the transitive producer loop (one `w6c -c -I` per package, dep-first,
// each `.o` wrapped in its own deterministic per-package `.a`), then a
// reverse-topo `w6l` of the `.a` set + libwwrt.a. Side files land in a
// cold `<stem>.sepwork` scratch dir. Twin of cstage build_one_sep.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
@@ -1357,12 +1430,26 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
return 1;
};
};
// Wrap each DEP package's `.o` in its own deterministic `.a`
// (5a). The ROOT stays a positional `.o` (force-loaded — it's
// the build target), so `main` is defined before any archive is
// processed (mirrors buildone's root treatment). The link
// consumes `.o`/`.a`, never `.wwi`.
if (pi != root) {
let apath: *u8 = sepfname(g, pi, scratch, ".a");
if (archiveo(objf, apath) != 0) {
cerr("ww --sep: archive failed\n");
return 1;
};
};
oi += 1;
};
// Flat link: root.o first (order[norder-1]), deps after; libwwrt.a
// selectively pulls runtime symbols. argv: 4 fixed (w6l,-o,out) + 1
// per .o + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil.
// Reverse-topo link of the per-package `.a` set: root.a first
// (order[norder-1]), deps after, then libwwrt.a (which still
// selectively pulls only the runtime members a live undef needs).
// argv: 3 fixed (w6l,-o,out) + 1 per .a + 1 libwwrt + 2*nlibdirs
// + 2*nlibs + 1 nil.
let nldirs: i32 = 0;
let nllibs: i32 = 0;
let ldirs: **u8 = nil;
@@ -1382,7 +1469,10 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
let pos: i32 = 3;
let li: i32 = norder - 1;
for (li >= 0) {
largv[pos] = sepfname(g, order[li], scratch, ".o");
// root: positional `.o` (force-load); deps: `.a` (selective).
let suf: str = ".a";
if (order[li] == root) { suf = ".o"; };
largv[pos] = sepfname(g, order[li], scratch, suf);
pos += 1;
li -= 1;
};