cmd: compile packages from direct exports

This commit is contained in:
2026-08-12 01:12:50 +09:00
parent 9b970eb16a
commit 0ac0fd68ec
4 changed files with 265 additions and 108 deletions

View File

@@ -331,14 +331,10 @@ enumerate_dir_ww(const char *dirpath, char ***out_files)
* definer's qualified symbol (#53) equals the consumer's qualified
* reference (#40) and the sep `.o`s link. A dep is NEVER bare-embedded.
*
* The prepend is the TRANSITIVE closure of a package's deps (lead-
* ratified, superseding rob-c3-spec §1.3 "direct deps"): a dep's public
* interface can name a transitive dep's type (os exposes time.instant),
* so the consuming unit needs the whole closure for name RESOLUTION —
* direct-deps-only does not type-check. This mirrors harec reading the
* transitive `.td` closure. The checker still scopes qualifier lookup to
* each source package's direct N_USE declarations, so carrying those type
* facts does not make a transitive package source-visible.
* Only DIRECT dependency artifacts are prepended. A `.wwi` relocates the
* recursively reachable public foreign type/const facts required by its own
* API, retaining their origin modules without turning them into source
* imports. Full transitive reachability remains a linker concern.
*/
#define SEP_MAXPKG 256
@@ -749,15 +745,6 @@ sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no,
return 0;
}
static void
sep_mark_deps(struct sepgraph *g, int pi, char *inset)
{
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int di = g->pkg[pi].deps[k];
if (!inset[di]) { inset[di] = 1; sep_mark_deps(g, di, inset); }
}
}
/* 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 of the
@@ -839,24 +826,21 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited,
return 0;
}
/* 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. Directory membership comes only from the
* package node loaded before planning. */
/* 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. */
static int
sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
const int *order, int norder, const char *searchpath, const char *unitf)
const char *searchpath, const char *unitf)
{
FILE *u = fopen(unitf, "wb");
if (u == NULL) {
fprintf(stderr, "ww: cannot open %s\n", unitf);
return -1;
}
char inset[SEP_MAXPKG] = {0};
sep_mark_deps(g, pi, inset);
for (int oi = 0; oi < norder; oi++) {
int dj = order[oi];
if (dj == pi || !inset[dj]) continue;
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
int dj = g->pkg[pi].deps[k];
char wwi[1024];
sep_fname(g, dj, scratch, ".wwi", wwi, sizeof wwi);
FILE *wf = fopen(wwi, "rb");
@@ -1043,7 +1027,7 @@ copy_file_atomic(const char *src, const char *dst)
static void
workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
{
snprintf(buf, bufsz, "ww workdir fmt 1 mode %s asm %d\n",
snprintf(buf, bufsz, "ww workdir fmt 2 mode %s asm %d\n",
is_test ? "test" : "build", emit_asm);
}
@@ -1222,7 +1206,7 @@ build_one_sep_impl(const char *src, int entry_is_dir,
const char *ca = warm ? anew : apath;
int needs_export = pi != root || root_package;
int needs_archive = pi != root || root_package;
if (sep_compose_unit(g, pi, scratch, order, norder, srcdir,
if (sep_compose_unit(g, pi, scratch, srcdir,
cu) < 0) { free(order); return 1; }
if (warm && !stale_all && file_equal(unitnew, unitf)
&& file_is_reg(asmf)

View File

@@ -2802,15 +2802,28 @@ the final component of its import path; two logical identities for one physical
directory are rejected rather than compiled twice.
Packages compile serially in dependency-first postorder. The compiler emits the
existing deterministic `.wwi` interface for every importable package and only
exported declarations enter that interface. A source qualifier is visible only
when its owning package directly imports it; private members and transitive-only
qualifiers are compiler errors. For compatibility with public signatures that
name deeper types, composed compiler units still carry transitive interface type
facts for internal resolution, but those facts do not create source-visible
bare names, package qualifiers, or value bindings. Replacing that
source-like closure with a self-contained typed export encoding remains part of
the later export-format work, not package-loader semantics.
existing deterministic `.wwi` interface for every importable package. Its
primary section contains that package's byte-sorted direct imports and exported
declarations. The compiler then appends byte-sorted, origin-tagged sections for
only the exported foreign type and constant facts recursively reachable from
the primary public signatures. This makes each direct dependency interface
self-contained for the public type information its consumers need while
retaining the deeper declarations' original package identity. Checked fixed
array dimensions are emitted as numeric type facts, so a public layout never
requires exposing the private constant spelling that produced its length.
A package compilation unit contains one `.wwi` for each byte-sorted **direct**
import and no separately injected transitive interface. Origin-tagged facts
inside those direct artifacts are compiler data, not source imports: a source
qualifier is visible only when its owning package directly imports it, and
private members, transitive-only qualifiers, bare values, and bare types remain
compiler errors. In `-c` package mode the compiler coalesces repeated exported
type/constant facts with the same origin, kind, and name, preserving one nominal
type identity across diamonds; raw non-package `w6c` retains its existing
duplicate behavior. The source-like `.wwi` syntax remains a transitional export
encoding pending the binary `.wwe` format described above, but the direct-input
ownership boundary is now live in production Cstage and WWstage compilers and
drivers.
An ordinary root is linked with the full reachable object closure into the
requested executable (legacy WW programs may use a package name other than
@@ -2821,7 +2834,10 @@ identity (`ww build -p -I ROOT -o bar.a foo.bar` emits `foo.bar.*` symbols),
while a literal directory uses its declared leaf package. Package output
requires a directory and `-p` cannot be combined with assembly-only `-S`. Two
cold builds with identical inputs are required to produce byte-identical
requested products.
requested products. Compiler intrinsics keep their package-mode runtime ABI
independent of transitive source interfaces (for example, `alloc` lowers to the
runtime allocator without requiring an `rt.wwi` compiler input), while the
linker still receives every reachable package archive plus the runtime archive.
## 12. Candidate architectures and hard-gate decision

View File

@@ -624,16 +624,12 @@ type lflags = struct {
// 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. Qualifier lookup remains scoped to the direct
// imports owned by each source package, so those type facts do not expose
// a transitive package. The unit composition is byte-identical to the cstage
// driver (rule 10) so w6c/w6c_ww emit identical `.s`.
// 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.
def SEP_MAXPKG: i32 = 256;
@@ -670,7 +666,8 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = {
return i;
};
if (os.samefile(pathstr(g.pkg[i].entry), pathstr(entry))) {
cerr("ww: one package directory has identities ");
cerr("ww: package directory "); cerr(pathstr(entry));
cerr(" has identities ");
if (g.pkg[i].path[0u64] == 0u8) { cerr("(root)"); }
else { cerr(pathstr(g.pkg[i].path)); };
cerr(" and ");
@@ -1047,18 +1044,6 @@ fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32,
return 0;
};
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);
@@ -1165,27 +1150,20 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8,
return 0;
};
// 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. Directory membership comes only from the
// package node loaded before planning.
fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
norder: i32, searchpath: *u8, unitf: *u8) i32 = {
// 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,
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: 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 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;
@@ -1206,9 +1184,7 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32,
os.close(u);
return -1;
};
};
};
oi += 1;
k += 1;
};
let bv: expctx;
bv.out = u;
@@ -1429,14 +1405,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 1 mode test asm 1\n";
return "ww workdir fmt 2 mode test asm 1\n";
};
return "ww workdir fmt 1 mode test asm 0\n";
return "ww workdir fmt 2 mode test asm 0\n";
};
if (emitasm != 0) {
return "ww workdir fmt 1 mode build asm 1\n";
return "ww workdir fmt 2 mode build asm 1\n";
};
return "ww workdir fmt 1 mode build asm 0\n";
return "ww workdir fmt 2 mode build asm 0\n";
};
fn stampmatches(path: *u8, want: str) bool = {
@@ -1699,7 +1675,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
};
let needsexport: bool = (pi != root) || rootpackage;
let needsarchive: bool = (pi != root) || rootpackage;
if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, cu) < 0) {
if (sepcomposeunit(g, pi, scratch, searchpath.ptr, cu) < 0) {
return 1;
};
let fresh: bool = false;

View File

@@ -41,6 +41,17 @@ fn build(dir: str, driver: str, tag: str, target: str, out: str) i32 = {
return code(dir, strings.concat("build_", tag), av);
};
fn diagnosticbody(s: str) str = {
let p: i32 = testenv.pos(s, ": error: ");
// Driver-owned graph diagnostics have no source-location prefix. Compare
// those in full; strip only compiler paths/locations from source errors.
if (p < 0) { return s; };
let begin: i32 = p + 9;
let end: i32 = begin;
for (end < s.len && s[end] != '\n') { end += 1; };
return strings.sub(s, begin, end);
};
fn rejectstable(dir: str, label: str, target: str, needle: str) void = {
let drivers: []str = ["ww", "ww_ww", "ww", "ww_ww"];
let tags: []str = ["c1", "w1", "c2", "w2"];
@@ -74,6 +85,12 @@ fn rejectstable(dir: str, label: str, target: str, needle: str) void = {
if (!testenv.same(errors[1], errors[3])) {
fail(label, "WW diagnostic changed across repeated builds");
};
let cb: str = diagnosticbody(errors[0]);
let wb: str = diagnosticbody(errors[1]);
if (!testenv.same(cb, wb)) {
fail(label, strings.concat("C/WW diagnostic bodies differ: '", cb,
"' vs '", wb, "'"));
};
};
@test fn standalone_and_package_artifact() void = {
@@ -136,6 +153,162 @@ fn rejectstable(dir: str, label: str, target: str, needle: str) void = {
testenv.clean(td);
};
@test fn self_contained_direct_exports() void = {
let td: str = testenv.fresh();
let dimensions: str = strings.concat(td, "/dimensions");
let implementation: str = strings.concat(td, "/implementation");
let api: str = strings.concat(td, "/api");
let main: str = strings.concat(td, "/main");
mkdir(dimensions); mkdir(implementation); mkdir(api); mkdir(main);
let dimensionssrc: str = strings.concat(
"package dimensions;\n",
"export def WIDTH: i32 = 4;\n",
"export def UNUSED_WIDTH: i32 = 8;\n");
let implsrc: str = strings.concat(
"package implementation;\n",
"import dimensions;\n",
"def PRIVATE_WIDTH: i32 = 3;\n",
"export type Buffer = [PRIVATE_WIDTH]u8;\n",
"export type PublicBuffer = [WIDTH]u8;\n",
"export type CastBuffer = [(WIDTH: u64)]u8;\n",
"export type Detail = struct { code: i64, };\n",
"export type Member = struct { detail: Detail, payload: i64, };\n",
"export type Unused = struct { noise: i32, };\n",
"export def UNUSED: i32 = 99;\n",
"type private_type = struct { secret: i32, };\n",
"def private_value: i32 = 77;\n",
"export fn construct(v: i32) Member = { return Member{detail=Detail{code=(v: i64) + 1i64}, payload=(v: i64) + 2i64}; };\n",
"export fn unrelated() i32 = { return UNUSED; };\n",
"fn hidden() i32 = { return private_value; };\n");
let apisrc: str = strings.concat(
"package api;\n",
"import implementation;\n",
"export type Member = struct { local: i32, };\n",
"export fn produce(v: i32) implementation.Member = { return implementation.construct(v); };\n",
"export fn score(m: implementation.Member) i32 = { return (m.detail.code + m.payload): i32; };\n",
"export fn width_of(b: Buffer) i32 = { return len(b): i32; };\n",
"export fn public_width_of(b: implementation.PublicBuffer) i32 = { return len(b): i32; };\n",
"export fn cast_width_of(b: implementation.CastBuffer) i32 = { return len(b): i32; };\n");
let mainsrc: str = strings.concat(
"package main;\n",
"import api;\n",
"fn main() i32 = { let m = api.produce(19); let p: *i64 = alloc(api.score(m): i64)!; return (*p): i32; };\n");
testenv.writefile(strings.concat(dimensions, "/dimensions.ww"), dimensionssrc);
testenv.writefile(strings.concat(implementation, "/implementation.ww"),
implsrc);
testenv.writefile(strings.concat(api, "/api.ww"), apisrc);
testenv.writefile(strings.concat(main, "/main.ww"), mainsrc);
let outputs: []str = [strings.concat(td, "/direct-c1"),
strings.concat(td, "/direct-c2"), strings.concat(td, "/direct-w")];
let drivers: []str = ["ww", "ww", "ww_ww"];
let tags: []str = ["direct_c1", "direct_c2", "direct_w"];
let i: i32 = 0;
for (i < outputs.len) {
if (build(td, drivers[i], tags[i], main, outputs[i]) != 0) {
fail("self-contained", strings.concat(drivers[i], " build failed"));
};
let runav: []str = [outputs[i]];
if (code(td, strings.concat("run_", tags[i]), runav) != 41) {
fail("self-contained", "implementation archive was not linked");
};
i += 1;
};
if (!testenv.same(testenv.readfile(outputs[0]), testenv.readfile(outputs[1]))
|| !testenv.same(testenv.readfile(outputs[0]), testenv.readfile(outputs[2]))) {
fail("self-contained", "clean C/C/WW executable outputs differ");
};
let cwork: str = strings.concat(outputs[0], ".sepwork/");
let cwork2: str = strings.concat(outputs[1], ".sepwork/");
let wwork: str = strings.concat(outputs[2], ".sepwork/");
let apiiface: str = testenv.readfile(strings.concat(cwork, "api.wwi"));
if (!testenv.same(apiiface, testenv.readfile(strings.concat(cwork2,
"api.wwi"))) || !testenv.same(apiiface, testenv.readfile(strings.concat(
wwork, "api.wwi")))) {
fail("self-contained", "C/C/WW api exports differ");
};
if (!testenv.has(apiiface, "//ww:module implementation\n")
|| !testenv.has(apiiface, "export type Buffer = [3]u8;")
|| !testenv.has(apiiface, "export type PublicBuffer = [4]u8;")
|| !testenv.has(apiiface, "export type CastBuffer = [4]u8;")
|| !testenv.has(apiiface, "export type Detail = struct")
|| testenv.occurrences(apiiface, "export type Member = struct") != 2) {
fail("self-contained", "api export lacks the recursive public type facts");
};
if (testenv.has(apiiface, "//ww:module dimensions\n")
|| testenv.has(apiiface, "WIDTH")
|| testenv.has(apiiface, "export def UNUSED_WIDTH")
|| testenv.has(apiiface, "PRIVATE_WIDTH")
|| testenv.has(apiiface, "export type Unused")
|| testenv.has(apiiface, "export def UNUSED")
|| testenv.has(apiiface, "export fn construct")
|| testenv.has(apiiface, "unrelated")
|| testenv.has(apiiface, "private_type")
|| testenv.has(apiiface, "private_value")
|| testenv.has(apiiface, "hidden")) {
fail("self-contained", "api export leaked unrelated or private declarations");
};
let expectedunit: str = strings.concat("//ww:module api\n", apiiface,
"\n//ww:module-reset\n", mainsrc, "\n");
if (!testenv.same(expectedunit, testenv.readfile(strings.concat(cwork,
"__root.unit.ww"))) || !testenv.same(expectedunit,
testenv.readfile(strings.concat(wwork, "__root.unit.ww")))) {
fail("self-contained", "consumer compiler input was not direct-api-only");
};
if (!testenv.exists(strings.concat(cwork, "implementation.a"))
|| !testenv.exists(strings.concat(cwork, "dimensions.a"))) {
fail("self-contained", "reachable archives missing from link closure");
};
let packages: []str = [strings.concat(td, "/api-c1.a"),
strings.concat(td, "/api-c2.a"), strings.concat(td, "/api-w.a")];
i = 0;
for (i < packages.len) {
let pav: []str = [testenv.driver(drivers[i]), "build", "-p", "-I", td,
"-o", packages[i], api];
if (code(td, strings.concat("package_", tags[i]), pav) != 0) {
fail("self-contained", "ww build -p api failed");
};
i += 1;
};
if (!testenv.same(testenv.readfile(packages[0]), testenv.readfile(packages[1]))
|| !testenv.same(testenv.readfile(packages[0]), testenv.readfile(packages[2]))
|| !testenv.same(apiiface, testenv.readfile(strings.concat(packages[0],
".wwi"))) || !testenv.same(apiiface, testenv.readfile(strings.concat(
packages[1], ".wwi"))) || !testenv.same(apiiface,
testenv.readfile(strings.concat(packages[2], ".wwi")))) {
fail("self-contained", "deterministic package artifact/export mismatch");
};
testenv.writefile(strings.concat(td, "/deep-qualifier.ww"), strings.concat(
"package main;\nimport api;\n",
"fn main() i32 = { let m: implementation.Member; return 0; };\n"));
rejectstable(td, "deep-qualifier", strings.concat(td,
"/deep-qualifier.ww"), "unknown type 'implementation.Member'");
testenv.writefile(strings.concat(td, "/deep-value.ww"), strings.concat(
"package main;\nimport api;\n",
"fn main() i32 = { return construct(1); };\n"));
rejectstable(td, "deep-value", strings.concat(td, "/deep-value.ww"),
"undefined: construct");
testenv.writefile(strings.concat(td, "/deep-const.ww"), strings.concat(
"package main;\nimport api;\n",
"fn main() i32 = { return WIDTH; };\n"));
rejectstable(td, "deep-const", strings.concat(td, "/deep-const.ww"),
"undefined: WIDTH");
testenv.writefile(strings.concat(td, "/deep-type.ww"), strings.concat(
"package main;\nimport api;\n",
"fn main() i32 = { let d: Detail; return 0; };\n"));
rejectstable(td, "deep-type", strings.concat(td, "/deep-type.ww"),
"unknown type 'Detail'");
testenv.writefile(strings.concat(td, "/private-direct.ww"), strings.concat(
"package main;\nimport implementation;\n",
"fn main() i32 = { return implementation.hidden(); };\n"));
rejectstable(td, "private-direct", strings.concat(td,
"/private-direct.ww"), "has no exported declaration 'hidden'");
testenv.clean(td);
};
fn writediamond(td: str, reverse: bool) str = {
let shared: str = strings.concat(td, "/shared");
let left: str = strings.concat(td, "/left");
@@ -159,13 +332,13 @@ fn writediamond(td: str, reverse: bool) str = {
};
testenv.writefile(strings.concat(left, "/left.ww"), strings.concat(
"package left;\nimport shared;\nimport shared;\n",
"export fn value() i32 = { return shared.base() + 1; };\n"));
"export fn make() shared.token = { return shared.token{value=shared.base() + 1}; };\n"));
testenv.writefile(strings.concat(right, "/right.ww"), strings.concat(
"package right;\nimport shared;\n",
"export fn value() i32 = { return shared.base() + 2; };\n"));
"export fn value(t: shared.token) i32 = { return t.value + shared.base() + 2; };\n"));
testenv.writefile(strings.concat(main, "/main.ww"), strings.concat(
"package main;\nimport right;\nimport left;\n",
"fn main() i32 = { return left.value() + right.value(); };\n"));
"fn main() i32 = { return right.value(left.make()); };\n"));
return main;
};
@@ -209,9 +382,17 @@ fn writediamond(td: str, reverse: bool) str = {
};
let leftiface: str = testenv.readfile(strings.concat(scratch,
"left.wwi"));
let rightiface: str = testenv.readfile(strings.concat(scratch,
"right.wwi"));
if (testenv.occurrences(leftiface, "import shared;") != 1) {
fail("diamond", "duplicate import escaped into export data");
};
if (testenv.occurrences(leftiface, "export type token") != 1
|| testenv.occurrences(rightiface, "export type token") != 1
|| testenv.occurrences(unit, "export type token") != 2
|| testenv.occurrences(unit, "//ww:module shared\n") != 2) {
fail("diamond", "origin fact closure did not merge deterministically");
};
i += 1;
};
let shuffledout: str = strings.concat(td, "/diamond-c-shuffled");