build: pass direct exports to package compilers

This commit is contained in:
2026-08-12 18:42:22 +09:00
parent b373c445bd
commit edd3f4ee83
13 changed files with 678 additions and 611 deletions

View File

@@ -69,6 +69,11 @@ export fn main(argc: i32, argv: **u8) i32 = {
let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary-
// only codegen (emit imported==0 decls
// only; treat `.wwi` deps as external)
let importpaths: []*u8 = alloc([], argc: u64)!;
importpaths.len = argc;
let importfiles: []*u8 = alloc([], argc: u64)!;
importfiles.len = argc;
let nimports: i32 = 0;
let i: i32 = 1;
for (i < argc) {
@@ -101,6 +106,16 @@ export fn main(argc: i32, argv: **u8) i32 = {
testsupport = argv[i];
} else { if (cstreq(a, "-c")) {
sepmode = 1i32;
} else { if (cstreq(a, "--import")) {
if (i + 2 >= argc) {
let m: str = "w6c: --import requires path and file\n";
os.write(2, m.ptr, m.len: u64);
return 2;
};
importpaths[nimports] = argv[i + 1];
importfiles[nimports] = argv[i + 2];
nimports += 1;
i += 2;
} else { if (a[0u64] == 45u8) {
let m: str = "w6c: unknown flag\n";
os.write(2, m.ptr, m.len: u64);
@@ -112,15 +127,35 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2;
};
src = a;
}; }; }; }; }; };
}; }; }; }; }; }; };
i += 1;
};
if (src == nil) {
let m: str = "usage: w6c_ww [-T] [-o out.s] file.ww\n";
let m: str = "usage: w6c_ww [-T] [-c] [-I out.wwi] [--import path dep.wwi]... [-o out.s] file.ww\n";
os.write(2, m.ptr, m.len: u64);
return 2;
};
if (nimports > 0 && sepmode == 0) {
let m: str = "w6c: --import requires -c\n";
os.write(2, m.ptr, m.len: u64);
return 2;
};
let importi: i32 = 0;
for (importi < nimports) {
if (importpaths[importi][0u64] == 0u8) {
let m: str = "w6c: --import path is empty\n";
os.write(2, m.ptr, m.len: u64);
return 2;
};
if (importi > 0 && strings.compare(pathstr(importpaths[importi - 1]),
pathstr(importpaths[importi])) >= 0) {
let m: str = "w6c: --import paths must be sorted and unique\n";
os.write(2, m.ptr, m.len: u64);
return 2;
};
importi += 1;
};
if (testsupport != nil && (sepmode == 0
|| (!cstreq(testsupport, "test")
&& !cstreq(testsupport, "__wwtest")))) {
@@ -129,6 +164,46 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2;
};
let importhead: *node = nil;
let importtail: *node = nil;
importi = 0;
for (importi < nimports) {
let ibuf: *u8;
let ilen: u64;
ibuf, ilen = slurp(importfiles[importi]);
if (ibuf == nil) {
let pre: str = "w6c: import ";
let mid: str = ": cannot read ";
let nl: str = "\n";
let ipath: str = pathstr(importpaths[importi]);
let ifile: str = pathstr(importfiles[importi]);
os.write(2, pre.ptr, pre.len: u64);
os.write(2, ipath.ptr, ipath.len: u64);
os.write(2, mid.ptr, mid.len: u64);
os.write(2, ifile.ptr, ifile.len: u64);
os.write(2, nl.ptr, nl.len: u64);
return 1;
};
let il: lex;
lexinit(&il, strings.dup(pathstr(importfiles[importi])), ibuf, ilen);
let ips: parser;
parserinit(&ips, &il);
let imod: str = pathstr(importpaths[importi]);
ips.pathmod = imod;
ips.curmod = imod;
if (testsupport != nil) { ips.testmodule = pathstr(testsupport); };
let imported: *node = parsefile(&ips);
if (il.errs > 0 || ips.errs > 0) { return 1; };
let d: *node = imported.list;
if (d != nil) {
if (importhead == nil) { importhead = d; }
else { importtail.next = d; };
for (d.next != nil) { d = d.next; };
importtail = d;
};
importi += 1;
};
let buf: *u8;
let blen: u64;
buf, blen = slurp(src);
@@ -137,10 +212,22 @@ export fn main(argc: i32, argv: **u8) i32 = {
os.write(2, m.ptr, m.len: u64);
return 1;
};
let l: lex;
lexinit(&l, strings.dup(pathstr(src)), buf, blen);
let ps: parser;
parserinit(&ps, &l);
if (testsupport != nil) { ps.testmodule = pathstr(testsupport); };
let f: *node = parsefile(&ps);
// Gate cgen on parse-stage errors. Mirrors cmd/w6c/main.c's
// `if (l.errs || p.errs) return 1;` — broken AST otherwise reaches
// cgen and emits junk asm with a zero exit (silent miscompile).
if (l.errs > 0 || ps.errs > 0) { return 1; };
if (importhead != nil) {
importtail.next = f.list;
f.list = importhead;
};
// Redirect fd 1 to the output file before any cgen emit runs.
// cgen.ww writes directly to fd 1; dup2 lets us reuse it without
// threading a file descriptor through the emit helpers.
// cgen writes directly to fd 1; dup2 keeps that implementation private.
if (out != nil) {
let ofd: i32 = os.open(pathstr(out),
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
@@ -158,24 +245,6 @@ export fn main(argc: i32, argv: **u8) i32 = {
os.close(ofd);
};
let nlen: u64 = cstrlen(src);
let view: str;
view.ptr = src;
view.len = nlen: i32;
let fname: str = strings.dup(view);
let l: lex;
lexinit(&l, fname, buf, blen);
let ps: parser;
parserinit(&ps, &l);
if (testsupport != nil) { ps.testmodule = pathstr(testsupport); };
let f: *node = parsefile(&ps);
// Gate cgen on parse-stage errors. Mirrors cmd/w6c/main.c's
// `if (l.errs || p.errs) return 1;` — broken AST otherwise reaches
// cgen and emits junk asm with a zero exit (silent miscompile).
if (l.errs > 0 || ps.errs > 0) { return 1; };
let testmodule: str;
if (testsupport != nil) { testmodule = pathstr(testsupport); };
let interfaceout: str;

View File

@@ -2,7 +2,7 @@
// ww-prototype rendering of a package's EXPORTED surface. Since the
// sep-compile flip (epic #22) this is the LIVE import path — the driver
// runs one `w6c -c -I` per package and feeds each dep's `.wwi` to its
// importers as import scope.
// importers through a separate canonical `--import` input.
//
// wwstage twin of cmd/w6c/wwi.c — byte-identical output is a rule-10
// requirement (`.wwi` is a cross-stage byte-id substrate). Specs:

View File

@@ -741,16 +741,12 @@ type lflags = struct {
// every package on its own (`w6c -c`), then flat-links the `.o` set.
// Separate compilation is the SOLE build path (E3-C1 flip, task #87).
//
// 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.
//
// Every direct dep is tagged by its FULL DOTTED import path on prepend
// (`//ww:module <path>`), so the definer's qualified symbol equals the
// consumer's qualified reference and the sep `.o`s link. A dependency's
// compiler-owned `.wwi` carries its reachable public foreign type facts;
// separately prepending transitive interfaces is neither necessary nor
// allowed. The unit composition is byte-identical to the cstage driver.
// Each w6c pass is BOTH consumer (reads each direct dep `.wwi` through a
// separate --import argument) AND producer (writes this package's `.wwi`
// via -I). Reverse-topo order guarantees a package's deps' `.wwi` exist.
// The canonical import path paired with each self-contained export preserves
// qualified symbol identity; transitive exports remain outside the compile
// action. Unit composition is byte-identical to the cstage driver.
def SEP_MAXPKG: i32 = 256;
@@ -775,6 +771,7 @@ type seppkg = struct {
failed: bool,
testsupport: bool,
loaded: bool,
exportchanged: bool,
emitcontext: i32,
contextstate: []u8,
bindings: []sepbind,
@@ -929,6 +926,7 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8,
g.pkg[g.n].failed = false;
g.pkg[g.n].testsupport = false;
g.pkg[g.n].loaded = false;
g.pkg[g.n].exportchanged = false;
g.pkg[g.n].emitcontext = -1;
let cslot: []u8 = alloc([], SEP_MAXCONTEXT: u64)!;
cslot.len = SEP_MAXCONTEXT;
@@ -1546,8 +1544,8 @@ fn sepvalidatemoduleclosure(g: *sepgraph, order: []i32, n: i32,
return 0;
};
// Imported source never enters this body: its direct export data was
// prepended above, while this package owns exactly its sorted source set.
// No imported source or export enters this body: the package owns exactly its
// sorted source set.
fn sepwriteall(fd: i32, buf: *u8, n: u64) bool = {
match (os.writeall(fd, buf, n)) {
case let wrote: i64 => return wrote == n: i64;
@@ -1589,12 +1587,9 @@ fn sepemitbody(fd: i32, path: *u8, modpath: *u8) i32 = {
return 0;
};
// Compose pi's sep-unit at `unitf`: its byte-sorted DIRECT dependency
// `.wwi`s, each tagged by its dotted path, then pi's own body under
// //ww:module-reset. Compiler exports are self-contained for public type
// facts; the linker separately retains the reachable archive closure.
fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8,
unitf: *u8) i32 = {
// Compose pi's sep-unit from only pi's byte-sorted sources. Direct exports are
// separate compiler inputs; the linker retains the reachable archive closure.
fn sepcomposeunit(g: *sepgraph, pi: i32, unitf: *u8) i32 = {
if (g.pkg[pi].emitcontext < 0
|| g.pkg[pi].emitcontext >= g.ncontext) { return -1; };
let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
@@ -1602,31 +1597,6 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8,
cerr("ww: cannot open unit\n");
return -1;
};
let k: i32 = 0;
for (k < g.pkg[pi].ndeps) {
let dj: i32 = g.pkg[pi].deps[k];
let wwi: *u8 = sepfname(g, dj, scratch, ".wwi");
let wb: *u8;
let wn: u64;
wb, wn = slurp(wwi);
if (wb == nil) {
cerr("ww: missing wwi\n");
os.close(u);
return -1;
};
let dm: str = "//ww:module ";
if (!sepwriteall(u, dm.ptr, dm.len: u64)
|| !sepwriteall(u, g.pkg[dj].path,
cstrlen(g.pkg[dj].path))
|| !sepwriteall(u, "\n".ptr, 1u64)
|| !sepwriteall(u, wb, wn)
|| !sepwriteall(u, "\n".ptr, 1u64)) {
cerr("ww: cannot compose package unit\n");
os.close(u);
return -1;
};
k += 1;
};
let bodyrc: i32 = 0;
if (g.pkg[pi].isdir != 0) {
let i: i32 = 0;
@@ -1720,7 +1690,7 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = {
};
// buildonesep — discover deps, reverse-topo,
// the transitive producer loop (one `w6c -c -I` per package, dep-first,
// the dependency-first producer loop (one `w6c -c -I` per package,
// each dependency `.o` wrapped in its own deterministic per-package `.a`), then a
// reverse-topo `w6l` of the root `.o` + dependency `.a` set + libwwrt.a.
// Side files land in a cold `<stem>.sepwork` scratch dir. Twin of cstage
@@ -1728,9 +1698,10 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = {
// A `-w DIR` workdir is a caller-owned persistent package-artifact tree
// that replaces the fresh `.sepwork` scratch. Staleness is pure content
// identity, never mtime: a package is reused only when its freshly
// composed unit byte-equals the committed unit AND the driver/tool copies
// recorded in the dir byte-equal the live executables — every decision is
// identity, never mtime: a package is reused only when its freshly composed
// unit byte-equals the committed unit, no direct dependency emitted a changed
// export, AND the driver/tool copies recorded in the dir byte-equal the live
// executables — every decision is
// reproducible by hand with cmp(1) against plain files. Artifacts commit
// via temp + rename with the unit renamed last, so a killed build can
// never leave a committed unit vouching for uncommitted artifacts. The
@@ -1840,14 +1811,14 @@ fn copyfileatomic(src: *u8, dst: *u8) i32 = {
fn workdirstamptext(istest: i32, emitasm: i32) str = {
if (istest != 0) {
if (emitasm != 0) {
return "ww workdir fmt 6 mode test asm 1\n";
return "ww workdir fmt 7 mode test asm 1\n";
};
return "ww workdir fmt 6 mode test asm 0\n";
return "ww workdir fmt 7 mode test asm 0\n";
};
if (emitasm != 0) {
return "ww workdir fmt 5 mode build asm 1\n";
return "ww workdir fmt 6 mode build asm 1\n";
};
return "ww workdir fmt 5 mode build asm 0\n";
return "ww workdir fmt 6 mode build asm 0\n";
};
fn stampmatches(path: *u8, want: str) bool = {
@@ -2338,15 +2309,23 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
};
let needsexport: bool = !g.pkg[pi].root || rootpackage;
let needsarchive: bool = !g.pkg[pi].root || rootpackage;
if (sepcomposeunit(g, pi, scratch, cu) < 0) {
if (sepcomposeunit(g, pi, cu) < 0) {
g.pkg[pi].failed = true;
anyfailed = true;
oi += 1;
continue;
};
let depschanged: bool = false;
let changedk: i32 = 0;
for (changedk < g.pkg[pi].ndeps) {
if (g.pkg[g.pkg[pi].deps[changedk]].exportchanged) {
depschanged = true;
};
changedk += 1;
};
let fresh: bool = false;
if (warm) {
if (!staleall) {
if (!staleall && !depschanged) {
fresh = fileequal(unitnew, unitf);
if (fresh) { fresh = fileisreg(asmf); };
if (fresh) {
@@ -2390,6 +2369,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
let alen: u64 = 8u64;
if (!needsexport) { alen = 6u64; if (roott) { alen = 9u64; }; };
if (supportt) { alen += 2u64; };
alen += (g.pkg[pi].ndeps: u64) * 3u64;
let argv: []str = alloc([], alen)!;
append(argv, "w6c");
if (roott) {
@@ -2402,6 +2382,14 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
append(argv, testsupportmodule);
};
append(argv, "-c");
let importk: i32 = 0;
for (importk < g.pkg[pi].ndeps) {
let dj: i32 = g.pkg[pi].deps[importk];
append(argv, "--import");
append(argv, pathstr(g.pkg[dj].path));
append(argv, pathstr(sepfname(g, dj, scratch, ".wwi")));
importk += 1;
};
if (needsexport) {
append(argv, "-I");
append(argv, pathstr(cw));
@@ -2430,6 +2418,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
continue;
};
};
if (needsexport) {
if (!warm || !fileequal(wwinew, wwi)) {
g.pkg[pi].exportchanged = true;
};
};
if (emitasm == 0) {
let argv: []str = alloc([], 4u64)!;
append(argv, "w6a");