compiler: separate package exports from entry roots

This commit is contained in:
2026-08-12 21:59:51 +09:00
parent a88078faf8
commit fc4bde703e
15 changed files with 420 additions and 190 deletions

View File

@@ -1,5 +1,6 @@
// wwi.ww — `.wwi` export-data producer (w6c_ww -I): a re-parseable
// ww-prototype rendering of a package's EXPORTED surface. Since the
// ww-prototype rendering of a package's exported surface and compiler-private
// closure. 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 through a separate canonical `--import` input.
@@ -10,9 +11,9 @@
//
// - Unparse walks the AST type-expr subtree (the N_T* nodes), NOT the
// tinfo — tinfo collapses nominal pkg.Name identity.
// - check_exported_type rides the producer entry (flag-gated), off on
// the normal `.s` path. It rejects exactly one thing: an exported
// signature naming a non-exported nominal type.
// - Reachable owner-private nominal types are encoded without `export`.
// Consumers reconstruct public signatures from them, but source cannot
// qualify those private spellings.
// - A `.wwi` is ONE package's self-contained interface. Its primary
// section is followed by compiler-owned origin sections containing the
// exported foreign type/const facts reachable from the public surface.
@@ -196,28 +197,8 @@ fn wwichecktype(c: *checker, owner: str, d: *syntax.node, t: *syntax.node) i32 =
if (t.kind == syntax.nkind.N_TPARAM) { return wwichecktype(c, owner, d, t.lhs); };
let bad: i32 = 0;
if (t.kind == syntax.nkind.N_TNAME) {
let s: *syntax.sym = wwitypesym(c, owner, t.str);
// sym.exported is vestigial (never set); the nominal's export
// status lives on its decl node, parser-set.
if (s != nil) {
if (s.decl != nil) {
if (s.decl.kind == syntax.nkind.N_TYPEDECL) {
// file.len == 0 ⇒ a checkinit-synthesized
// predeclared builtin (the ONLY empty-source
// decls: nomemdecl check.ww:126, the `void`
// TNAME check.ww:122), not a user nominal — ww's
// analogue of harec's STORAGE_NOMEM leaf-arm, and
// why cstage (lookup_builtin, no scope SK_TYPE)
// needs no such guard.
if (s.decl.file.len > 0) {
if (s.decl.exported == 0) {
wwireject(d, t.str);
bad = 1;
};
};
};
};
};
// Nominal references, including private ones, are collected into
// the self-contained fact closure below.
} else { if (
t.kind == syntax.nkind.N_TPTR ||
t.kind == syntax.nkind.N_TSLICE ||
@@ -513,10 +494,9 @@ fn wwitype(fd: i32, t: *syntax.node) void = {
// fficollect — dropping it makes sep-compile emit `CALL malloc` for a
// `@symbol("rt_malloc")` fn). Named for the class so @align/@offset would
// slot in here IF ww ever grows field-layout attributes — it has none
// today (task #47 report). @test never reaches a `.wwi` (test fns are not
// export-marked), so it needs no exclusion arm.
// today (task #47 report). @test is compiler-private package-test metadata.
fn wwiattrrelevant(nm: str) bool = {
return syntax.streq(nm, "symbol");
return syntax.streq(nm, "symbol") || syntax.streq(nm, "test");
};
fn wwiattrs(fd: i32, d: *syntax.node) void = {
@@ -544,7 +524,8 @@ fn wwiattrs(fd: i32, d: *syntax.node) void = {
fn wwidecl(fd: i32, d: *syntax.node) void = {
if (d.kind == syntax.nkind.N_FNDECL) {
wwiattrs(fd, d);
wputs(fd, "export fn ");
if (d.exported != 0) { wputs(fd, "export fn "); }
else { wputs(fd, "fn "); };
wputs(fd, d.str);
wputb(fd, '(');
let p: *syntax.node = d.list;
@@ -557,13 +538,15 @@ fn wwidecl(fd: i32, d: *syntax.node) void = {
wwitype(fd, d.lhs);
wputs(fd, ";\n");
} else { if (d.kind == syntax.nkind.N_TYPEDECL) {
wputs(fd, "export type ");
if (d.exported != 0) { wputs(fd, "export type "); }
else { wputs(fd, "type "); };
wputs(fd, d.str);
wputs(fd, " = ");
wwitype(fd, d.lhs);
wputs(fd, ";\n");
} else { if (d.kind == syntax.nkind.N_DEF) {
wputs(fd, "export def ");
if (d.exported != 0) { wputs(fd, "export def "); }
else { wputs(fd, "def "); };
wputs(fd, d.str);
wputs(fd, ": ");
wwitype(fd, d.lhs);
@@ -585,7 +568,8 @@ fn wwidecl(fd: i32, d: *syntax.node) void = {
wputs(fd, ";\n");
};
} else { if (d.kind == syntax.nkind.N_LET) {
wputs(fd, "export let ");
if (d.exported != 0) { wputs(fd, "export let "); }
else { wputs(fd, "let "); };
wputs(fd, d.str);
wputs(fd, ": ");
if (d.lhs == nil) {
@@ -609,6 +593,17 @@ fn wwiisdecl(d: *syntax.node) bool = {
d.kind == syntax.nkind.N_DEF || d.kind == syntax.nkind.N_LET;
};
fn wwihasattr(d: *syntax.node, name: str) bool = {
let a: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR && syntax.streq(a.str, name)) {
return true;
};
a = a.next;
};
return false;
};
// Deterministic ordering (rob §3): byte-lexicographic, mirror C strcmp
// sign (<0/0/>0). Both stages key the sort identically, so the `.wwi`
// order is deterministic.
@@ -648,6 +643,9 @@ fn wwisortdecls(keys: []str, nodes: []*syntax.node, n: i32) void = {
// representation narrow and make the deterministic ordering explicit.
type wwifactset = struct {
c: *checker,
privatenodes: []*syntax.node,
privatekeys: []str,
nprivate: i32,
factnodes: []*syntax.node,
factmods: []str,
factranks: []i32,
@@ -730,8 +728,21 @@ fn wwicollectdecl(fs: *wwifactset, owner: str, d: *syntax.node) void = {
fs.seenranks[fs.nseen] = rank;
fs.nseen += 1;
if (owner.len > 0) {
if (d.exported == 0) {
if (owner.len == 0 && d.exported == 0) {
// checkinit's synthetic nomem/void declarations have no source file.
// They are builtins, not owner-private package facts; cstage resolves
// these through lookup_builtin and therefore never collects them.
if (d.file.len == 0) { return; };
if (d.kind != syntax.nkind.N_TYPEDECL) {
wwifactreject(d, "def", d.str);
fs.bad = 1;
return;
};
fs.privatenodes[fs.nprivate] = d;
fs.privatekeys[fs.nprivate] = d.str;
fs.nprivate += 1;
} else { if (owner.len > 0) {
if (d.exported == 0 && d.kind != syntax.nkind.N_TYPEDECL) {
let kind: str = "def";
if (d.kind == syntax.nkind.N_TYPEDECL) { kind = "type"; };
wwifactreject(d, kind, d.str);
@@ -742,7 +753,7 @@ fn wwicollectdecl(fs: *wwifactset, owner: str, d: *syntax.node) void = {
fs.factmods[fs.nfacts] = owner;
fs.factranks[fs.nfacts] = rank;
fs.nfacts += 1;
};
}; };
if (d.kind == syntax.nkind.N_FNDECL) {
let p: *syntax.node = d.list;
@@ -925,12 +936,15 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = {
if (nall == 0) { nall = 1; };
let fs: wwifactset;
fs.c = c;
let privatenodes: []*syntax.node = alloc([], nall: u64)!; privatenodes.len = nall;
let privatekeys: []str = alloc([], nall: u64)!; privatekeys.len = nall;
let factnodes: []*syntax.node = alloc([], nall: u64)!; factnodes.len = nall;
let factmods: []str = alloc([], nall: u64)!; factmods.len = nall;
let factranks: []i32 = alloc([], nall: u64)!; factranks.len = nall;
let seennodes: []*syntax.node = alloc([], nall: u64)!; seennodes.len = nall;
let seenmods: []str = alloc([], nall: u64)!; seenmods.len = nall;
let seenranks: []i32 = alloc([], nall: u64)!; seenranks.len = nall;
fs.privatenodes = privatenodes; fs.privatekeys = privatekeys;
fs.factnodes = factnodes; fs.factmods = factmods; fs.factranks = factranks;
fs.seennodes = seennodes; fs.seenmods = seenmods; fs.seenranks = seenranks;
let primary: str;
@@ -942,6 +956,7 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = {
d = d.next;
};
if (fs.bad != 0) { return 1i32; };
wwisortdecls(fs.privatekeys, fs.privatenodes, fs.nprivate);
wwisortfacts(&fs);
let fd: i32 = os.open(path,
@@ -982,7 +997,7 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = {
// #11: a decl-less / export-less primary body carries no
// module-tagged decl, so the scan above finds nothing; fall back to
// the primary module identity stamped on the N_FILE node at parse
// time. A real root `package main` arrives via a bare module-reset
// time. A raw single-file `package main` root arrives via a bare reset
// and leaves file.nmod empty, so it stays "main". The detector is
// scan-miss (found==0), NOT pkg=="main", to match cstage byte-for-
// byte (rule 10) when a tagged decl legitimately leafs to "main".
@@ -1042,6 +1057,27 @@ fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = {
};
};
// Compiler-private owner nominals precede public declarations. They are
// available to export decoding but remain absent from source visibility.
let pri: i32 = 0;
for (pri < fs.nprivate) {
wwidecl(fd, fs.privatenodes[pri]);
pri += 1;
};
// Package-test metadata is private and deterministic. Only the distinct
// generated-main package consumes these declarations through --import.
if (c.istestpackage != 0) {
d = file.list;
for (d != nil) {
if (wwiprimary(d) && d.kind == syntax.nkind.N_FNDECL
&& d.exported == 0 && wwihasattr(d, "test")) {
wwidecl(fd, d);
};
d = d.next;
};
};
// decls — exported primary, byte-sorted by symbol name.
let ndecl: i32 = 0;
d = file.list;