wcc/ww: ww build --sep separate-compilation driver (M3-tail c3, #46)

build_one_sep (both stages + ww/main.combined.ww regen): discover_deps
(transitive directory-package set, dotted-path identity), tri-color
reverse-topo (cycle bails; loud reject is commit 4), the transitive
producer loop (one w6c -c -I pass per package, dep-first, each both
consumer and producer of its .wwi), and a flat w6l of the .o set.
combined.ww stays the DEFAULT live path; --sep is additive.

Every dep is tagged by its full dotted import path on prepend
(//ww:module <path>), so the definer's qualified symbol (#53) == the
consumer's qualified reference (#40) and the sep .o set links. The
prepend is the TRANSITIVE closure (lead-ratified, superseding
rob-c3-spec §1.3 direct-deps): a dep's interface can name a transitive
dep's type (os exposes time.instant), so the consuming unit needs the
whole closure for resolution — direct-deps-only does not type-check.
Consistent with the current flat-unit transitive-namespace model (the
visibility tighten is #45, post-M4).

Gate 989_sepbuild_run drives ww + ww_ww --sep on the real chain
root->os->{rt,time}: build+run (exit 7) + cs==ww per-pkg .s/.wwi/.unit
+ final binary + transitive-topo discovery + keystone bodies==.wwi
(os,root) through the real driver. Cold scratch; -o-redirected.
This commit is contained in:
2026-06-16 03:29:16 +09:00
parent 0c4a5ecea0
commit e37886b27d
5 changed files with 1948 additions and 12 deletions

View File

@@ -3720,6 +3720,545 @@ type lflags = struct {
nlibs: i32,
};
// ---- ww build --sep — M3-tail separate-compilation driver ------------
//
// Port of cmd/ww/main.c build_one_sep (task #46/c3). The `--sep` path
// materializes each imported package's `.wwi` interface and compiles
// every package on its own (`w6c -c`), then flat-links the `.o` set.
// combined.ww stays the DEFAULT live path; --sep is additive.
//
// Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND
// producer (writes this package's `.wwi` via -I). Reverse-topo order
// guarantees a package's deps' `.wwi` exist before it compiles.
//
// The load-bearing rule (#56, rob-resolved): every dep is tagged by its
// FULL DOTTED import path on prepend (`//ww:module <path>`), so the
// definer's qualified symbol (#53) equals the consumer's qualified
// reference (#40) and the sep `.o`s link. The prepend is the TRANSITIVE
// closure of a package's deps (lead-ratified): a dep's interface can
// name a transitive dep's type, so the consuming unit needs the whole
// closure for resolution. The unit composition is byte-identical to the
// cstage driver (rule 10) so w6c/w6c_ww emit identical `.s`.
def SEP_MAXPKG: i32 = 256;
type seppkg = struct {
path: *u8, // dotted import path, NUL-term; root path[0]==0
entry: *u8, // resolved package dir (or file, file root), NUL-term
isdir: i32,
deps: []i32, // direct-dep indices into sepgraph.pkg
ndeps: i32,
color: i32, // tri-color DFS: 0 white, 1 gray, 2 black
};
type sepgraph = struct {
pkg: []seppkg, // alloc'd SEP_MAXPKG
n: i32,
};
// Find a package by dotted path, or add it. Returns index, -1 if full.
fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = {
let i: i32 = 0;
for (i < g.n) {
if (cstreq(g.pkg[i].path, path)) { return i; };
i += 1;
};
if (g.n >= SEP_MAXPKG) {
cerr("ww --sep: too many packages\n");
return -1;
};
let plen: u64 = cstrlen(path);
let elen: u64 = cstrlen(entry);
g.pkg[g.n].path = arenadupcstr(path, plen);
g.pkg[g.n].entry = arenadupcstr(entry, elen);
g.pkg[g.n].isdir = isdir;
let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!;
dslot.len = SEP_MAXPKG;
g.pkg[g.n].deps = dslot;
g.pkg[g.n].ndeps = 0;
g.pkg[g.n].color = 0;
let r: i32 = g.n;
g.n += 1;
return r;
};
// Build "<scratch>/<base><suffix>" NUL-term; base = path, or "__root"
// for the empty root path.
fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = {
let buf: []u8 = alloc([], (os.PATH_MAX: u64))!;
buf.len = os.PATH_MAX;
let off: u64 = cstrinto(buf.ptr, 0u64, scratch);
off = byteinto(buf.ptr, off, 47u8); // '/'
if (g.pkg[pi].path[0u64] != 0u8) {
off = cstrinto(buf.ptr, off, g.pkg[pi].path);
} else {
off = strinto(buf.ptr, off, "__root");
};
off = strinto(buf.ptr, off, suffix);
cstrseal(buf.ptr, off);
return buf.ptr;
};
// Scan one source file for top-level `import IDENT;`. A DIRECTORY import
// is a package boundary: add as a direct dep of pi. A FILE import is an
// intra-package split: fold its imports into pi. Mirrors cstage
// sep_scan_file (collects PATHS, not bytes).
fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
fv: *expctx) i32 = {
let fview: str;
fview.ptr = file;
fview.len = cstrlen(file): i32;
let fdup: str = strings.dup(fview);
if (visitseen(fv, fdup)) { return 0; };
visitadd(fv, fdup);
let bufp: *u8;
let blen: u64;
bufp, blen = slurp(file);
if (bufp == nil) {
cerr("ww --sep: cannot read source\n");
return -1;
};
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
for (j < blen) {
if (bufp[j] == 10u8) { break; }; // '\n'
j += 1u64;
};
let idp: *u8;
let idn: u64;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
let isdir: i32 = 0;
let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir);
if (ipath != nil) {
if (isdir != 0) {
let nm: []u8 = alloc([], idn + 1u64)!;
let k: u64 = 0u64;
for (k < idn) { nm[k] = idp[k]; k += 1u64; };
nm[idn] = 0u8;
let di: i32 = sepfindoradd(g, nm.ptr, ipath, 1);
if (di < 0) { return -1; };
let seen: bool = false;
let m: i32 = 0;
for (m < g.pkg[pi].ndeps) {
if (g.pkg[pi].deps[m] == di) { seen = true; };
m += 1;
};
if (!seen) {
if (g.pkg[pi].ndeps >= SEP_MAXPKG) { return -1; };
g.pkg[pi].deps[g.pkg[pi].ndeps] = di;
g.pkg[pi].ndeps += 1;
};
} else {
if (sepscanfile(g, pi, ipath, searchpath, fv) < 0) {
return -1;
};
};
};
};
i = j + 1u64;
};
return 0;
};
// Discover pi's direct deps + recurse. Enumerate the package's own
// files (dir → *.ww less *test.ww; file → the file) and scan each.
// `color` doubles as a scanned-marker (2); reset to white before topo.
fn sepscanpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = {
if (g.pkg[pi].color == 2) { return 0; };
g.pkg[pi].color = 2;
let fv: expctx;
fv.out = -1;
fv.dirs = searchpath;
fv.visit = nil;
let rc: i32 = 0;
if (g.pkg[pi].isdir != 0) {
let names: **u8;
let n: i32;
names, n = enumeratedir(g.pkg[pi].entry);
let dlen: u64 = cstrlen(g.pkg[pi].entry);
let i: i32 = 0;
for (i < n) {
if (rc == 0) {
let nlen: u64 = cstrlen(names[i]);
let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!;
let k: u64 = 0u64;
for (k < dlen) { fp[k] = g.pkg[pi].entry[k]; k += 1u64; };
fp[dlen] = 47u8; // '/'
k = 0u64;
for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; };
fp[dlen + 1u64 + nlen] = 0u8;
rc = sepscanfile(g, pi, fp.ptr, searchpath, &fv);
};
i += 1;
};
} else {
rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv);
};
if (rc < 0) { return rc; };
let k: i32 = 0;
for (k < g.pkg[pi].ndeps) {
if (sepscanpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { return -1; };
k += 1;
};
return 0;
};
// DFS post-order over the dep DAG → reverse-topo (deps before importer).
// Tri-color: a back-edge BAILS rather than spinning (loud cycle reject is
// commit 4). Cite Hare gather (deps.ha:123).
fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32) i32 = {
if (g.pkg[pi].color == 2) { return 0; };
if (g.pkg[pi].color == 1) {
cerr("ww --sep: import cycle (loud reject lands commit 4)\n");
return -1;
};
g.pkg[pi].color = 1;
let k: i32 = 0;
for (k < g.pkg[pi].ndeps) {
if (septopovisit(g, g.pkg[pi].deps[k], order, no) < 0) { return -1; };
k += 1;
};
g.pkg[pi].color = 2;
order[*no] = pi;
*no += 1;
return 0;
};
// Mark pi's transitive deps (excluding pi) in inset[].
fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = {
let k: i32 = 0;
for (k < g.pkg[pi].ndeps) {
let di: i32 = g.pkg[pi].deps[k];
if (inset[di] == 0u8) {
inset[di] = 1u8;
sepmarkdeps(g, di, inset);
};
k += 1;
};
};
// Emit one of pi's own source files into the sep-unit under the
// //ww:module-reset primary boundary (so -c emits its decls, imported
// ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead);
// FILE imports fold in (intra-package split).
fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8) void = {
let pview: str;
pview.ptr = path;
pview.len = cstrlen(path): i32;
let pdup: str = strings.dup(pview);
if (visitseen(visit, pdup)) { return; };
visitadd(visit, pdup);
let bufp: *u8;
let blen: u64;
bufp, blen = slurp(path);
if (bufp == nil) {
cerr("ww --sep: cannot read source\n");
return;
};
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
for (j < blen) {
if (bufp[j] == 10u8) { break; }; // '\n'
j += 1u64;
};
let idp: *u8;
let idn: u64;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
let isdir: i32 = 0;
let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir);
if (ipath != nil) {
if (isdir == 0) {
sepemitbody(fd, ipath, visit, searchpath);
};
};
};
i = j + 1u64;
};
let d: str = "//ww:module-reset\n";
os.writeall(fd, d.ptr, d.len: u64);
os.writeall(fd, bufp, blen);
os.writeall(fd, "\n".ptr, 1u64);
};
fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8) void = {
let names: **u8;
let n: i32;
names, n = enumeratedir(dir);
let dlen: u64 = cstrlen(dir);
let i: i32 = 0;
for (i < n) {
let nlen: u64 = cstrlen(names[i]);
let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!;
let k: u64 = 0u64;
for (k < dlen) { fp[k] = dir[k]; k += 1u64; };
fp[dlen] = 47u8; // '/'
k = 0u64;
for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; };
fp[dlen + 1u64 + nlen] = 0u8;
sepemitbody(fd, fp.ptr, visit, searchpath);
i += 1;
};
};
// Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s
// (reverse-topo order, each tagged by its dotted path), then pi's own
// body under //ww:module-reset.
fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
norder: i32, searchpath: *u8, unitf: *u8) i32 = {
let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (u < 0) {
cerr("ww --sep: cannot open unit\n");
return -1;
};
let inset: []u8 = alloc([], g.n: u64)!;
inset.len = g.n;
let z: i32 = 0;
for (z < g.n) { inset[z] = 0u8; z += 1; };
sepmarkdeps(g, pi, inset);
let oi: i32 = 0;
for (oi < norder) {
let dj: i32 = order[oi];
if (dj != pi) {
if (inset[dj] != 0u8) {
let wwi: *u8 = sepfname(g, dj, scratch, ".wwi");
let wb: *u8;
let wn: u64;
wb, wn = slurp(wwi);
if (wb == nil) {
cerr("ww --sep: missing wwi\n");
os.close(u);
return -1;
};
let dm: str = "//ww:module ";
os.writeall(u, dm.ptr, dm.len: u64);
os.writeall(u, g.pkg[dj].path, cstrlen(g.pkg[dj].path));
os.writeall(u, "\n".ptr, 1u64);
os.writeall(u, wb, wn);
os.writeall(u, "\n".ptr, 1u64);
};
};
oi += 1;
};
let bv: expctx;
bv.out = u;
bv.dirs = searchpath;
bv.visit = nil;
if (g.pkg[pi].isdir != 0) {
sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath);
} else {
sepemitbody(u, g.pkg[pi].entry, &bv, searchpath);
};
os.close(u);
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.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
let a6: *u8 = joinpathlit(selfdir, "w6a_ww");
let l6: *u8 = joinpathlit(selfdir, "w6l_ww");
// Default lib search path: <selfdir>/../../lib
let dotdotlib: []u8 = alloc([], (os.PATH_MAX: u64))!;
dotdotlib.len = os.PATH_MAX;
{
let off: u64 = cstrinto(dotdotlib.ptr, 0u64, selfdir);
off = strinto(dotdotlib.ptr, off, "/../../lib");
cstrseal(dotdotlib.ptr, off);
};
// Source directory (mirrors buildone).
let srcd: []u8 = alloc([], (os.PATH_MAX: u64))!;
srcd.len = os.PATH_MAX;
if (entryisdir != 0) {
let slen: u64 = cstrlen(src);
let k: u64 = 0u64;
for (k < slen) { srcd[k] = src[k]; k += 1u64; };
for (slen > 1u64) {
if (srcd[slen - 1u64] != 47u8) { break; };
slen -= 1u64;
};
srcd[slen] = 0u8;
} else {
let slen: u64 = cstrlen(src);
let last: u64 = slen;
let found: bool = false;
let i: u64 = slen;
for (i > 0u64) {
i -= 1u64;
if (src[i] == 47u8) { last = i; found = true; i = 0u64; };
};
if (found) {
let k: u64 = 0u64;
for (k < last) { srcd[k] = src[k]; k += 1u64; };
srcd[last] = 0u8;
} else {
srcd[0] = 46u8; // '.'
srcd[1] = 0u8;
};
};
// searchpath = srcd + ':' + incs + ':' + dotdotlib.
let searchpath: []u8 = alloc([], (os.PATH_MAX: u64) * 3u64)!;
searchpath.len = ((os.PATH_MAX: u64) * 3u64): i32;
{
let off: u64 = cstrinto(searchpath.ptr, 0u64, srcd.ptr);
off = byteinto(searchpath.ptr, off, 58u8); // ':'
if (incs[0u64] != 0u8) {
off = cstrinto(searchpath.ptr, off, incs);
off = byteinto(searchpath.ptr, off, 58u8);
};
off = cstrinto(searchpath.ptr, off, dotdotlib.ptr);
cstrseal(searchpath.ptr, off);
};
// Stem for the scratch dir (mirrors buildone).
let stem: []u8 = alloc([], (os.PATH_MAX: u64))!;
stem.len = os.PATH_MAX;
if (entryisdir != 0) {
let dlen: u64 = cstrlen(srcd.ptr);
let bo: u64 = basenameoff(srcd.ptr, dlen);
let off: u64 = cstrinto(stem.ptr, 0u64, srcd.ptr);
stem[off] = 47u8; off += 1u64; // '/'
let i: u64 = bo;
for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; };
cstrseal(stem.ptr, off);
} else {
makestem(stem.ptr, src);
};
let effstem: *u8 = stem.ptr;
if (objstem != nil) { effstem = objstem; };
let scratch: *u8 = appendlit(effstem, ".sepwork");
// rm -rf scratch (cold); recreate. Reuse os.removeall if present;
// here we mkdir and rely on TRUNC opens to overwrite stale files.
os.mkdir(pathstr(scratch), 493i32); // 0o755 (idempotent; stale files TRUNC'd)
// libwwrt.a path: <selfdir>/../lib/libwwrt.a
let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!;
libwwrt.len = os.PATH_MAX;
{
let off: u64 = cstrinto(libwwrt.ptr, 0u64, selfdir);
off = strinto(libwwrt.ptr, off, "/../lib/libwwrt.a");
cstrseal(libwwrt.ptr, off);
};
// Discover.
let pkgslot: []seppkg = alloc([], SEP_MAXPKG: u64)!;
pkgslot.len = SEP_MAXPKG;
let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!;
let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir);
if (root < 0) { return 1; };
if (sepscanpkg(g, root, searchpath.ptr) < 0) { return 1; };
// Reset colors, reverse-topo.
let ci: i32 = 0;
for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; };
let order: []i32 = alloc([], g.n: u64)!;
order.len = g.n;
let norder: i32 = 0;
if (septopovisit(g, root, order, &norder) < 0) { return 1; };
// Producer loop — dep-first, one `w6c -c -I` per package.
let oi: i32 = 0;
for (oi < norder) {
let pi: i32 = order[oi];
let unitf: *u8 = sepfname(g, pi, scratch, ".unit.ww");
let wwi: *u8 = sepfname(g, pi, scratch, ".wwi");
let asmf: *u8 = sepfname(g, pi, scratch, ".s");
let objf: *u8 = sepfname(g, pi, scratch, ".o");
if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, unitf) < 0) {
return 1;
};
{
let argv: []*u8 = alloc([], 8u64)!;
argv.len = 8;
argv[0] = "w6c\0".ptr;
argv[1] = "-c\0".ptr;
argv[2] = "-I\0".ptr;
argv[3] = wwi;
argv[4] = "-o\0".ptr;
argv[5] = asmf;
argv[6] = unitf;
argv[7] = nil;
if (procrun(c6, argv.ptr) != 0) {
cerr("ww --sep: w6c failed\n");
return 1;
};
};
{
let argv: []*u8 = alloc([], 5u64)!;
argv.len = 5;
argv[0] = "w6a\0".ptr;
argv[1] = "-o\0".ptr;
argv[2] = objf;
argv[3] = asmf;
argv[4] = nil;
if (procrun(a6, argv.ptr) != 0) {
cerr("ww --sep: w6a 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.
let nldirs: i32 = 0;
let nllibs: i32 = 0;
let ldirs: **u8 = nil;
let llibs: **u8 = nil;
if (lf != nil) {
nldirs = lf.nlibdirs;
nllibs = lf.nlibs;
ldirs = lf.libdirs;
llibs = lf.libs;
};
let total: i32 = 3 + norder + 1 + 2 * nldirs + 2 * nllibs + 1;
let largv: []*u8 = alloc([], total: u64)!;
largv.len = total;
largv[0] = "w6l\0".ptr;
largv[1] = "-o\0".ptr;
largv[2] = out;
let pos: i32 = 3;
let li: i32 = norder - 1;
for (li >= 0) {
largv[pos] = sepfname(g, order[li], scratch, ".o");
pos += 1;
li -= 1;
};
largv[pos] = libwwrt.ptr; pos += 1;
let k: i32 = 0;
for (k < nldirs) {
largv[pos] = "-L\0".ptr;
largv[pos + 1] = ldirs[k];
pos += 2;
k += 1;
};
k = 0;
for (k < nllibs) {
largv[pos] = "-l\0".ptr;
largv[pos + 1] = llibs[k];
pos += 2;
k += 1;
};
largv[pos] = nil;
if (procrun(l6, largv.ptr) != 0) {
cerr("ww --sep: w6l failed\n");
return 1;
};
return 0;
};
// buildone — compile `src` (file or directory) into the executable
// named `out`.
// selfdir: NUL-terminated dir containing this driver and the
@@ -4088,6 +4627,7 @@ fn defaultoutpath(src: *u8) *u8 = {
fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let src: *u8 = nil;
let wantsep: i32 = 0; // --sep: M3-tail separate-compilation path
let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3
let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!;
incs.len = ((os.PATH_MAX: u64) * 2u64): i32;
@@ -4106,7 +4646,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
for (i < argc) {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) { // '-'
if (p[1u64] == 73u8) { // '-I'
if (cstreqlit(p, "--sep")) { // M3-tail separate-compile
wantsep = 1;
} else { if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
dir = p + 2u64;
@@ -4174,7 +4716,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
} else {
cerr("ww build: unknown flag\n");
return 2;
}; }; }; };
}; }; }; }; };
} else {
if (src == nil) { src = p; };
};
@@ -4221,6 +4763,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibdirs = nlibdirs;
lf.libs = libs.ptr;
lf.nlibs = nlibs;
if (wantsep != 0) {
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf);
};
return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
};
@@ -4276,7 +4821,10 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
else {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) {
if (p[1u64] == 73u8) {
if (cstreqlit(p, "--sep")) { // build-only; run rejects (rule 10)
cerr("ww run: --sep is only valid with build\n");
return 2;
} else { if (p[1u64] == 73u8) {
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
dir = p + 2u64;
@@ -4343,7 +4891,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
} else {
cerr("ww run: unknown flag\n");
return 2;
}; }; }; };
}; }; }; }; };
i += 1;
} else {
if (src == nil) {

View File

@@ -846,6 +846,545 @@ type lflags = struct {
nlibs: i32,
};
// ---- ww build --sep — M3-tail separate-compilation driver ------------
//
// Port of cmd/ww/main.c build_one_sep (task #46/c3). The `--sep` path
// materializes each imported package's `.wwi` interface and compiles
// every package on its own (`w6c -c`), then flat-links the `.o` set.
// combined.ww stays the DEFAULT live path; --sep is additive.
//
// Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND
// producer (writes this package's `.wwi` via -I). Reverse-topo order
// guarantees a package's deps' `.wwi` exist before it compiles.
//
// The load-bearing rule (#56, rob-resolved): every dep is tagged by its
// FULL DOTTED import path on prepend (`//ww:module <path>`), so the
// definer's qualified symbol (#53) equals the consumer's qualified
// reference (#40) and the sep `.o`s link. The prepend is the TRANSITIVE
// closure of a package's deps (lead-ratified): a dep's interface can
// name a transitive dep's type, so the consuming unit needs the whole
// closure for resolution. The unit composition is byte-identical to the
// cstage driver (rule 10) so w6c/w6c_ww emit identical `.s`.
def SEP_MAXPKG: i32 = 256;
type seppkg = struct {
path: *u8, // dotted import path, NUL-term; root path[0]==0
entry: *u8, // resolved package dir (or file, file root), NUL-term
isdir: i32,
deps: []i32, // direct-dep indices into sepgraph.pkg
ndeps: i32,
color: i32, // tri-color DFS: 0 white, 1 gray, 2 black
};
type sepgraph = struct {
pkg: []seppkg, // alloc'd SEP_MAXPKG
n: i32,
};
// Find a package by dotted path, or add it. Returns index, -1 if full.
fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = {
let i: i32 = 0;
for (i < g.n) {
if (cstreq(g.pkg[i].path, path)) { return i; };
i += 1;
};
if (g.n >= SEP_MAXPKG) {
cerr("ww --sep: too many packages\n");
return -1;
};
let plen: u64 = cstrlen(path);
let elen: u64 = cstrlen(entry);
g.pkg[g.n].path = arenadupcstr(path, plen);
g.pkg[g.n].entry = arenadupcstr(entry, elen);
g.pkg[g.n].isdir = isdir;
let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!;
dslot.len = SEP_MAXPKG;
g.pkg[g.n].deps = dslot;
g.pkg[g.n].ndeps = 0;
g.pkg[g.n].color = 0;
let r: i32 = g.n;
g.n += 1;
return r;
};
// Build "<scratch>/<base><suffix>" NUL-term; base = path, or "__root"
// for the empty root path.
fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = {
let buf: []u8 = alloc([], (os.PATH_MAX: u64))!;
buf.len = os.PATH_MAX;
let off: u64 = cstrinto(buf.ptr, 0u64, scratch);
off = byteinto(buf.ptr, off, 47u8); // '/'
if (g.pkg[pi].path[0u64] != 0u8) {
off = cstrinto(buf.ptr, off, g.pkg[pi].path);
} else {
off = strinto(buf.ptr, off, "__root");
};
off = strinto(buf.ptr, off, suffix);
cstrseal(buf.ptr, off);
return buf.ptr;
};
// Scan one source file for top-level `import IDENT;`. A DIRECTORY import
// is a package boundary: add as a direct dep of pi. A FILE import is an
// intra-package split: fold its imports into pi. Mirrors cstage
// sep_scan_file (collects PATHS, not bytes).
fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8,
fv: *expctx) i32 = {
let fview: str;
fview.ptr = file;
fview.len = cstrlen(file): i32;
let fdup: str = strings.dup(fview);
if (visitseen(fv, fdup)) { return 0; };
visitadd(fv, fdup);
let bufp: *u8;
let blen: u64;
bufp, blen = slurp(file);
if (bufp == nil) {
cerr("ww --sep: cannot read source\n");
return -1;
};
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
for (j < blen) {
if (bufp[j] == 10u8) { break; }; // '\n'
j += 1u64;
};
let idp: *u8;
let idn: u64;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
let isdir: i32 = 0;
let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir);
if (ipath != nil) {
if (isdir != 0) {
let nm: []u8 = alloc([], idn + 1u64)!;
let k: u64 = 0u64;
for (k < idn) { nm[k] = idp[k]; k += 1u64; };
nm[idn] = 0u8;
let di: i32 = sepfindoradd(g, nm.ptr, ipath, 1);
if (di < 0) { return -1; };
let seen: bool = false;
let m: i32 = 0;
for (m < g.pkg[pi].ndeps) {
if (g.pkg[pi].deps[m] == di) { seen = true; };
m += 1;
};
if (!seen) {
if (g.pkg[pi].ndeps >= SEP_MAXPKG) { return -1; };
g.pkg[pi].deps[g.pkg[pi].ndeps] = di;
g.pkg[pi].ndeps += 1;
};
} else {
if (sepscanfile(g, pi, ipath, searchpath, fv) < 0) {
return -1;
};
};
};
};
i = j + 1u64;
};
return 0;
};
// Discover pi's direct deps + recurse. Enumerate the package's own
// files (dir → *.ww less *test.ww; file → the file) and scan each.
// `color` doubles as a scanned-marker (2); reset to white before topo.
fn sepscanpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = {
if (g.pkg[pi].color == 2) { return 0; };
g.pkg[pi].color = 2;
let fv: expctx;
fv.out = -1;
fv.dirs = searchpath;
fv.visit = nil;
let rc: i32 = 0;
if (g.pkg[pi].isdir != 0) {
let names: **u8;
let n: i32;
names, n = enumeratedir(g.pkg[pi].entry);
let dlen: u64 = cstrlen(g.pkg[pi].entry);
let i: i32 = 0;
for (i < n) {
if (rc == 0) {
let nlen: u64 = cstrlen(names[i]);
let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!;
let k: u64 = 0u64;
for (k < dlen) { fp[k] = g.pkg[pi].entry[k]; k += 1u64; };
fp[dlen] = 47u8; // '/'
k = 0u64;
for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; };
fp[dlen + 1u64 + nlen] = 0u8;
rc = sepscanfile(g, pi, fp.ptr, searchpath, &fv);
};
i += 1;
};
} else {
rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv);
};
if (rc < 0) { return rc; };
let k: i32 = 0;
for (k < g.pkg[pi].ndeps) {
if (sepscanpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { return -1; };
k += 1;
};
return 0;
};
// DFS post-order over the dep DAG → reverse-topo (deps before importer).
// Tri-color: a back-edge BAILS rather than spinning (loud cycle reject is
// commit 4). Cite Hare gather (deps.ha:123).
fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32) i32 = {
if (g.pkg[pi].color == 2) { return 0; };
if (g.pkg[pi].color == 1) {
cerr("ww --sep: import cycle (loud reject lands commit 4)\n");
return -1;
};
g.pkg[pi].color = 1;
let k: i32 = 0;
for (k < g.pkg[pi].ndeps) {
if (septopovisit(g, g.pkg[pi].deps[k], order, no) < 0) { return -1; };
k += 1;
};
g.pkg[pi].color = 2;
order[*no] = pi;
*no += 1;
return 0;
};
// Mark pi's transitive deps (excluding pi) in inset[].
fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = {
let k: i32 = 0;
for (k < g.pkg[pi].ndeps) {
let di: i32 = g.pkg[pi].deps[k];
if (inset[di] == 0u8) {
inset[di] = 1u8;
sepmarkdeps(g, di, inset);
};
k += 1;
};
};
// Emit one of pi's own source files into the sep-unit under the
// //ww:module-reset primary boundary (so -c emits its decls, imported
// ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead);
// FILE imports fold in (intra-package split).
fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8) void = {
let pview: str;
pview.ptr = path;
pview.len = cstrlen(path): i32;
let pdup: str = strings.dup(pview);
if (visitseen(visit, pdup)) { return; };
visitadd(visit, pdup);
let bufp: *u8;
let blen: u64;
bufp, blen = slurp(path);
if (bufp == nil) {
cerr("ww --sep: cannot read source\n");
return;
};
let i: u64 = 0u64;
for (i < blen) {
let j: u64 = i;
for (j < blen) {
if (bufp[j] == 10u8) { break; }; // '\n'
j += 1u64;
};
let idp: *u8;
let idn: u64;
idp, idn = scanuse(bufp + i, j - i);
if (idp != nil) {
let isdir: i32 = 0;
let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir);
if (ipath != nil) {
if (isdir == 0) {
sepemitbody(fd, ipath, visit, searchpath);
};
};
};
i = j + 1u64;
};
let d: str = "//ww:module-reset\n";
os.writeall(fd, d.ptr, d.len: u64);
os.writeall(fd, bufp, blen);
os.writeall(fd, "\n".ptr, 1u64);
};
fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8) void = {
let names: **u8;
let n: i32;
names, n = enumeratedir(dir);
let dlen: u64 = cstrlen(dir);
let i: i32 = 0;
for (i < n) {
let nlen: u64 = cstrlen(names[i]);
let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!;
let k: u64 = 0u64;
for (k < dlen) { fp[k] = dir[k]; k += 1u64; };
fp[dlen] = 47u8; // '/'
k = 0u64;
for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; };
fp[dlen + 1u64 + nlen] = 0u8;
sepemitbody(fd, fp.ptr, visit, searchpath);
i += 1;
};
};
// Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s
// (reverse-topo order, each tagged by its dotted path), then pi's own
// body under //ww:module-reset.
fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
norder: i32, searchpath: *u8, unitf: *u8) i32 = {
let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (u < 0) {
cerr("ww --sep: cannot open unit\n");
return -1;
};
let inset: []u8 = alloc([], g.n: u64)!;
inset.len = g.n;
let z: i32 = 0;
for (z < g.n) { inset[z] = 0u8; z += 1; };
sepmarkdeps(g, pi, inset);
let oi: i32 = 0;
for (oi < norder) {
let dj: i32 = order[oi];
if (dj != pi) {
if (inset[dj] != 0u8) {
let wwi: *u8 = sepfname(g, dj, scratch, ".wwi");
let wb: *u8;
let wn: u64;
wb, wn = slurp(wwi);
if (wb == nil) {
cerr("ww --sep: missing wwi\n");
os.close(u);
return -1;
};
let dm: str = "//ww:module ";
os.writeall(u, dm.ptr, dm.len: u64);
os.writeall(u, g.pkg[dj].path, cstrlen(g.pkg[dj].path));
os.writeall(u, "\n".ptr, 1u64);
os.writeall(u, wb, wn);
os.writeall(u, "\n".ptr, 1u64);
};
};
oi += 1;
};
let bv: expctx;
bv.out = u;
bv.dirs = searchpath;
bv.visit = nil;
if (g.pkg[pi].isdir != 0) {
sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath);
} else {
sepemitbody(u, g.pkg[pi].entry, &bv, searchpath);
};
os.close(u);
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.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
let a6: *u8 = joinpathlit(selfdir, "w6a_ww");
let l6: *u8 = joinpathlit(selfdir, "w6l_ww");
// Default lib search path: <selfdir>/../../lib
let dotdotlib: []u8 = alloc([], (os.PATH_MAX: u64))!;
dotdotlib.len = os.PATH_MAX;
{
let off: u64 = cstrinto(dotdotlib.ptr, 0u64, selfdir);
off = strinto(dotdotlib.ptr, off, "/../../lib");
cstrseal(dotdotlib.ptr, off);
};
// Source directory (mirrors buildone).
let srcd: []u8 = alloc([], (os.PATH_MAX: u64))!;
srcd.len = os.PATH_MAX;
if (entryisdir != 0) {
let slen: u64 = cstrlen(src);
let k: u64 = 0u64;
for (k < slen) { srcd[k] = src[k]; k += 1u64; };
for (slen > 1u64) {
if (srcd[slen - 1u64] != 47u8) { break; };
slen -= 1u64;
};
srcd[slen] = 0u8;
} else {
let slen: u64 = cstrlen(src);
let last: u64 = slen;
let found: bool = false;
let i: u64 = slen;
for (i > 0u64) {
i -= 1u64;
if (src[i] == 47u8) { last = i; found = true; i = 0u64; };
};
if (found) {
let k: u64 = 0u64;
for (k < last) { srcd[k] = src[k]; k += 1u64; };
srcd[last] = 0u8;
} else {
srcd[0] = 46u8; // '.'
srcd[1] = 0u8;
};
};
// searchpath = srcd + ':' + incs + ':' + dotdotlib.
let searchpath: []u8 = alloc([], (os.PATH_MAX: u64) * 3u64)!;
searchpath.len = ((os.PATH_MAX: u64) * 3u64): i32;
{
let off: u64 = cstrinto(searchpath.ptr, 0u64, srcd.ptr);
off = byteinto(searchpath.ptr, off, 58u8); // ':'
if (incs[0u64] != 0u8) {
off = cstrinto(searchpath.ptr, off, incs);
off = byteinto(searchpath.ptr, off, 58u8);
};
off = cstrinto(searchpath.ptr, off, dotdotlib.ptr);
cstrseal(searchpath.ptr, off);
};
// Stem for the scratch dir (mirrors buildone).
let stem: []u8 = alloc([], (os.PATH_MAX: u64))!;
stem.len = os.PATH_MAX;
if (entryisdir != 0) {
let dlen: u64 = cstrlen(srcd.ptr);
let bo: u64 = basenameoff(srcd.ptr, dlen);
let off: u64 = cstrinto(stem.ptr, 0u64, srcd.ptr);
stem[off] = 47u8; off += 1u64; // '/'
let i: u64 = bo;
for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; };
cstrseal(stem.ptr, off);
} else {
makestem(stem.ptr, src);
};
let effstem: *u8 = stem.ptr;
if (objstem != nil) { effstem = objstem; };
let scratch: *u8 = appendlit(effstem, ".sepwork");
// rm -rf scratch (cold); recreate. Reuse os.removeall if present;
// here we mkdir and rely on TRUNC opens to overwrite stale files.
os.mkdir(pathstr(scratch), 493i32); // 0o755 (idempotent; stale files TRUNC'd)
// libwwrt.a path: <selfdir>/../lib/libwwrt.a
let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!;
libwwrt.len = os.PATH_MAX;
{
let off: u64 = cstrinto(libwwrt.ptr, 0u64, selfdir);
off = strinto(libwwrt.ptr, off, "/../lib/libwwrt.a");
cstrseal(libwwrt.ptr, off);
};
// Discover.
let pkgslot: []seppkg = alloc([], SEP_MAXPKG: u64)!;
pkgslot.len = SEP_MAXPKG;
let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!;
let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir);
if (root < 0) { return 1; };
if (sepscanpkg(g, root, searchpath.ptr) < 0) { return 1; };
// Reset colors, reverse-topo.
let ci: i32 = 0;
for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; };
let order: []i32 = alloc([], g.n: u64)!;
order.len = g.n;
let norder: i32 = 0;
if (septopovisit(g, root, order, &norder) < 0) { return 1; };
// Producer loop — dep-first, one `w6c -c -I` per package.
let oi: i32 = 0;
for (oi < norder) {
let pi: i32 = order[oi];
let unitf: *u8 = sepfname(g, pi, scratch, ".unit.ww");
let wwi: *u8 = sepfname(g, pi, scratch, ".wwi");
let asmf: *u8 = sepfname(g, pi, scratch, ".s");
let objf: *u8 = sepfname(g, pi, scratch, ".o");
if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, unitf) < 0) {
return 1;
};
{
let argv: []*u8 = alloc([], 8u64)!;
argv.len = 8;
argv[0] = "w6c\0".ptr;
argv[1] = "-c\0".ptr;
argv[2] = "-I\0".ptr;
argv[3] = wwi;
argv[4] = "-o\0".ptr;
argv[5] = asmf;
argv[6] = unitf;
argv[7] = nil;
if (procrun(c6, argv.ptr) != 0) {
cerr("ww --sep: w6c failed\n");
return 1;
};
};
{
let argv: []*u8 = alloc([], 5u64)!;
argv.len = 5;
argv[0] = "w6a\0".ptr;
argv[1] = "-o\0".ptr;
argv[2] = objf;
argv[3] = asmf;
argv[4] = nil;
if (procrun(a6, argv.ptr) != 0) {
cerr("ww --sep: w6a 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.
let nldirs: i32 = 0;
let nllibs: i32 = 0;
let ldirs: **u8 = nil;
let llibs: **u8 = nil;
if (lf != nil) {
nldirs = lf.nlibdirs;
nllibs = lf.nlibs;
ldirs = lf.libdirs;
llibs = lf.libs;
};
let total: i32 = 3 + norder + 1 + 2 * nldirs + 2 * nllibs + 1;
let largv: []*u8 = alloc([], total: u64)!;
largv.len = total;
largv[0] = "w6l\0".ptr;
largv[1] = "-o\0".ptr;
largv[2] = out;
let pos: i32 = 3;
let li: i32 = norder - 1;
for (li >= 0) {
largv[pos] = sepfname(g, order[li], scratch, ".o");
pos += 1;
li -= 1;
};
largv[pos] = libwwrt.ptr; pos += 1;
let k: i32 = 0;
for (k < nldirs) {
largv[pos] = "-L\0".ptr;
largv[pos + 1] = ldirs[k];
pos += 2;
k += 1;
};
k = 0;
for (k < nllibs) {
largv[pos] = "-l\0".ptr;
largv[pos + 1] = llibs[k];
pos += 2;
k += 1;
};
largv[pos] = nil;
if (procrun(l6, largv.ptr) != 0) {
cerr("ww --sep: w6l failed\n");
return 1;
};
return 0;
};
// buildone — compile `src` (file or directory) into the executable
// named `out`.
// selfdir: NUL-terminated dir containing this driver and the
@@ -1214,6 +1753,7 @@ fn defaultoutpath(src: *u8) *u8 = {
fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let src: *u8 = nil;
let wantsep: i32 = 0; // --sep: M3-tail separate-compilation path
let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3
let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!;
incs.len = ((os.PATH_MAX: u64) * 2u64): i32;
@@ -1232,7 +1772,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
for (i < argc) {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) { // '-'
if (p[1u64] == 73u8) { // '-I'
if (cstreqlit(p, "--sep")) { // M3-tail separate-compile
wantsep = 1;
} else { if (p[1u64] == 73u8) { // '-I'
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
dir = p + 2u64;
@@ -1300,7 +1842,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
} else {
cerr("ww build: unknown flag\n");
return 2;
}; }; }; };
}; }; }; }; };
} else {
if (src == nil) { src = p; };
};
@@ -1347,6 +1889,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibdirs = nlibdirs;
lf.libs = libs.ptr;
lf.nlibs = nlibs;
if (wantsep != 0) {
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf);
};
return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32);
};
@@ -1402,7 +1947,10 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
else {
let p: *u8 = argv[i];
if (p[0u64] == 45u8) {
if (p[1u64] == 73u8) {
if (cstreqlit(p, "--sep")) { // build-only; run rejects (rule 10)
cerr("ww run: --sep is only valid with build\n");
return 2;
} else { if (p[1u64] == 73u8) {
let dir: *u8 = nil;
if (p[2u64] != 0u8) {
dir = p + 2u64;
@@ -1469,7 +2017,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
} else {
cerr("ww run: unknown flag\n");
return 2;
}; }; }; };
}; }; }; }; };
i += 1;
} else {
if (src == nil) {