wcc/ww: .wwi export-data producer + check_exported_type (#22 M2)

New `w6c -I <out.wwi>` flag (both stages) writes a re-parseable
ww-prototype rendering of a package's EXPORTED surface. M2 dead-code:
nothing consumes .wwi yet (combined.ww stays the live path); the flag is
off on every existing invocation, so the 990-997 byte-id gates and all
prior tests are unperturbed.

The unparse walks the AST type-expr subtree (N_T* nodes), not the
tinfo Type* (which collapses nominal pkg.Name identity). Deterministic
output: package line, byte-sorted imports, byte-sorted decls — a pure
function of the exported API. cmd/w6c/wwi.c + selfhost/cmd/wcc/wwi.ww
emit byte-identical .wwi (new cross-stage byte-id substrate, rule 10).

check_exported_type (drew) rides the producer entry, flag-gated: an
exported signature naming a non-exported nominal is loud-rejected before
any byte is written, identically on both stages. Ports harec
check.c:4092-4168, recursing the type-AST and gating on the resolved
SK_TYPE sym's decl export flag (Sym.exported is vestigial in both
stages; the predeclared synthetic `nomem` decl carries no source
position and is treated as a builtin leaf — cstage parity).

Two wwstage checker AST-mutations are normalized to cstage's pristine
view for byte-id: the N_TPARAM tuple-element wrapper (unwrapped) and the
variadic `T...`→`[]T` param desugar (peeled).

Gate 989_m2wwi_run: ascii/strings/getopt each produce a .wwi that
re-parses (wwdump -a) and is cs==ww byte-identical; a private-type-leak
fixture is rejected identically by both stages (non-vacuous check).
This commit is contained in:
2026-06-15 20:16:21 +09:00
parent e9c11cb5ae
commit e8d3d89fef
8 changed files with 1965 additions and 4 deletions

View File

@@ -45288,6 +45288,564 @@ export fn fargregname(i: i32) str = {
return "?";
};
//ww:module-reset
// wwi.ww — `.wwi` export-data producer (w6c_ww -I). M2 DEAD-CODE: writes
// a re-parseable ww-prototype rendering of a package's EXPORTED surface.
// Nothing consumes `.wwi` yet (combined.ww stays the live path); the only
// caller is the new -I flag, off on every existing invocation.
//
// wwstage twin of cmd/w6c/wwi.c — byte-identical output is a rule-10
// requirement (`.wwi` is a new cross-stage byte-id substrate). Specs:
// .ai/rob-M2-spec.md + .ai/drew-M2-checkexported.md.
//
// - 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), so M2
// stays dead on the normal `.s` path. It rejects exactly one thing:
// an exported signature naming a non-exported nominal type.
// - A `.wwi` is ONE package's interface; the M2 gate feeds a driver-
// combined unit, so the emit filters to PRIMARY decls (imported==0).
// Imports mirror check.ww (os/tok/strconv only): node/nkind/sym/skind/
// scopelookuptype/streq/checker/tkind resolve bare in the selfhost's flat
// bundled scope, exactly as check.ww references them.
package wcc;
import os;
import tok;
import strconv;
// --- byte writers ------------------------------------------------------
fn wputs(fd: i32, s: str) void = {
os.write(fd, s.ptr, s.len: u64);
};
fn wputb(fd: i32, b: u8) void = {
let buf: [1]u8;
buf[0] = b;
os.write(fd, buf.ptr, 1u64);
};
fn wquote(fd: i32, s: str) void = {
wputb(fd, '"');
let i: i32 = 0;
for (i < s.len) {
let c: u8 = s[i];
if (c == '"') {
wputs(fd, "\\\"");
} else { if (c == '\\') {
wputs(fd, "\\\\");
} else { if (c == '\n') {
wputs(fd, "\\n");
} else { if (c == '\t') {
wputs(fd, "\\t");
} else { if (c < 32u8) {
let hi: u8 = c >> 4u8;
let lo: u8 = c & 15u8;
let h: u8 = 0u8;
let l: u8 = 0u8;
if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; };
if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; };
let buf: [4]u8;
buf[0] = 92u8;
buf[1] = 120u8;
buf[2] = h;
buf[3] = l;
os.write(fd, buf.ptr, 4u64);
} else {
wputb(fd, c);
};};};};};
i += 1;
};
wputb(fd, '"');
};
// --- check_exported_type (drew) ---------------------------------------
//
// Resolve an N_TNAME to its type sym via the public sym helpers (mirror
// of cstage wwi_typesym / scope_lookup_type) WITHOUT the side effects of
// the checker's aliassym (no on-demand resolve, no double error). A
// primitive/keyword resolves to no SK_TYPE → leaf. By producer time
// checkfile has finished and c.cur == c.top.
fn wwitypesym(c: *checker, nm: str) *sym = {
let empty: str;
let s: *sym = scopelookuptype(c.cur, empty, nm);
if (s == nil) {
let dotidx: i32 = -1;
let i: i32 = 0;
for (i < nm.len) {
if (nm[i] == 46u8) { dotidx = i; };
i += 1;
};
if (dotidx >= 0) {
let leaf: str;
leaf.ptr = nm.ptr + ((dotidx + 1): u64);
leaf.len = nm.len - dotidx - 1;
s = scopelookuptype(c.cur, empty, leaf);
};
};
if (s == nil) { return nil; };
if (s.skind != skind.SK_TYPE) { return nil; };
return s;
};
fn wwireject(d: *node, nm: str) void = {
wputs(2, d.file);
wputs(2, ":");
wputs(2, strconv.u64tos(d.line: u64, strconv.base.DEC));
wputs(2, ":");
wputs(2, strconv.u64tos(d.col: u64, strconv.base.DEC));
wputs(2, ": error: exported declaration references unexported type '");
wputs(2, nm);
wputs(2, "'\n");
};
// Returns 1 if a non-exported nominal was named (loud), else 0.
fn wwichecktype(c: *checker, d: *node, t: *node) i32 = {
if (t == nil) { return 0; };
// rule-10: the wwstage checker wraps N_TTUPLE.list elements in
// N_TPARAM (ast.ww:101); cstage keeps the type-AST pristine. Unwrap
// transparently so the recursion sees the same shape cstage walks.
if (t.kind == nkind.N_TPARAM) { return wwichecktype(c, d, t.lhs); };
let bad: i32 = 0;
if (t.kind == nkind.N_TNAME) {
let s: *sym = wwitypesym(c, 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 == 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;
};
};
};
};
};
} else { if (
t.kind == nkind.N_TPTR ||
t.kind == nkind.N_TSLICE ||
t.kind == nkind.N_TBANG ||
t.kind == nkind.N_TCHAN
) {
bad = bad | wwichecktype(c, d, t.lhs);
} else { if (t.kind == nkind.N_TARRAY) {
// element only; the length is a const-expr, not a type.
bad = bad | wwichecktype(c, d, t.lhs);
} else { if (t.kind == nkind.N_TFN) {
let p: *node = t.list;
for (p != nil) {
bad = bad | wwichecktype(c, d, p.lhs);
p = p.next;
};
bad = bad | wwichecktype(c, d, t.lhs);
} else { if (t.kind == nkind.N_TSTRUCT) {
let f: *node = t.list;
for (f != nil) {
bad = bad | wwichecktype(c, d, f.lhs);
f = f.next;
};
} else { if (t.kind == nkind.N_TTAGGED || t.kind == nkind.N_TTUPLE) {
let e: *node = t.list;
for (e != nil) {
bad = bad | wwichecktype(c, d, e);
e = e.next;
};
} else { if (t.kind == nkind.N_TENUM) {
// the inline enum body is the definition, not a reference;
// recurse only its storage type (members are values).
bad = bad | wwichecktype(c, d, t.lhs);
};};};};};};};
return bad;
};
fn wwicheckdecl(c: *checker, d: *node) i32 = {
let bad: i32 = 0;
if (d.kind == nkind.N_FNDECL) {
let p: *node = d.list;
for (p != nil) {
bad = bad | wwichecktype(c, d, p.lhs);
p = p.next;
};
bad = bad | wwichecktype(c, d, d.lhs); // ret
} else { if (d.kind == nkind.N_TYPEDECL) {
bad = bad | wwichecktype(c, d, d.lhs);
} else { if (d.kind == nkind.N_DEF) {
// the declared type; the rhs const value is not a type.
bad = bad | wwichecktype(c, d, d.lhs);
} else { if (d.kind == nkind.N_LET) {
bad = bad | wwichecktype(c, d, d.lhs);
};};};};
return bad;
};
// --- type-expr + const-expr unparser (rob §2.2/§2.4) ------------------
fn wwiexpr(fd: i32, e: *node) void = {
if (e == nil) { return; };
if (e.kind == nkind.N_INTLIT) {
wputs(fd, strconv.u64tos(e.uval, strconv.base.DEC));
if (e.tsuffix.len > 0) { wputs(fd, e.tsuffix); };
} else { if (e.kind == nkind.N_IDENT || e.kind == nkind.N_TNAME) {
wputs(fd, e.str);
} else { if (e.kind == nkind.N_DOT) {
wwiexpr(fd, e.lhs);
wputb(fd, '.');
wputs(fd, e.str);
} else { if (e.kind == nkind.N_TRUE) {
wputs(fd, "true");
} else { if (e.kind == nkind.N_FALSE) {
wputs(fd, "false");
} else { if (e.kind == nkind.N_NIL) {
wputs(fd, "nil");
} else { if (e.kind == nkind.N_STRLIT) {
wquote(fd, e.str);
} else { if (e.kind == nkind.N_BIN) {
wwiexpr(fd, e.lhs);
wputb(fd, 32u8);
wputs(fd, tokname(e.op));
wputb(fd, 32u8);
wwiexpr(fd, e.rhs);
} else { if (e.kind == nkind.N_UN) {
wputs(fd, tokname(e.op));
wwiexpr(fd, e.lhs);
} else {
wputs(2, "wwi: unhandled const-expr node kind\n");
os.exit(1);
};};};};};};};};};
};
fn wwiparam(fd: i32, p: *node) void = {
if (streq(p.str, "...")) { // C-style FFI `...`
wputs(fd, "...");
return;
};
if (p.str.len > 0) {
wputs(fd, p.str);
wputs(fd, ": ");
};
// rule-10: the wwstage checker desugars a Hare variadic `T...` param
// in place to `[]T` (lhs becomes N_TSLICE); cstage leaves lhs == T.
// Peel the inserted slice so both stages emit the surface `T...`.
if (p.op == tkind.TK_ELLIPSIS && p.lhs != nil && p.lhs.kind == nkind.N_TSLICE) {
wwitype(fd, p.lhs.lhs);
} else {
wwitype(fd, p.lhs);
};
if (p.op == tkind.TK_ELLIPSIS) { // Hare `T...` variadic
wputs(fd, "...");
};
};
fn wwitype(fd: i32, t: *node) void = {
if (t == nil) { // absent return type spells void
wputs(fd, "void");
return;
};
// rule-10: unwrap the wwstage-only N_TPARAM tuple-element wrapper
// (ast.ww:101) so the unparse matches cstage's pristine type-AST.
if (t.kind == nkind.N_TPARAM) { wwitype(fd, t.lhs); return; };
if (t.kind == nkind.N_TNAME) {
if (t.str.len > 0) { wputs(fd, t.str); } else { wputs(fd, "void"); };
} else { if (t.kind == nkind.N_TPTR) {
wputb(fd, '*');
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TSLICE) {
wputs(fd, "[]");
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TARRAY) {
wputb(fd, '[');
if (t.rhs != nil) { wwiexpr(fd, t.rhs); } else { wputb(fd, '_'); };
wputb(fd, ']');
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TBANG) {
wputb(fd, '!');
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TCHAN) {
wputs(fd, "chan ");
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TFN) {
wputs(fd, "fn(");
let p: *node = t.list;
for (p != nil) {
if (p != t.list) { wputs(fd, ", "); };
wwiparam(fd, p);
p = p.next;
};
wputs(fd, ") ");
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TSTRUCT) {
wputs(fd, "struct { ");
let f: *node = t.list;
for (f != nil) {
if (f != t.list) { wputs(fd, ", "); };
if (f.str.len > 0) {
wputs(fd, f.str);
wputs(fd, ": ");
};
wwitype(fd, f.lhs);
f = f.next;
};
wputs(fd, " }");
} else { if (t.kind == nkind.N_TTUPLE) {
wputb(fd, '(');
let e: *node = t.list;
for (e != nil) {
if (e != t.list) { wputs(fd, ", "); };
wwitype(fd, e);
e = e.next;
};
wputb(fd, ')');
} else { if (t.kind == nkind.N_TTAGGED) {
wputb(fd, '(');
let e: *node = t.list;
for (e != nil) {
if (e != t.list) { wputs(fd, " | "); };
wwitype(fd, e);
e = e.next;
};
wputb(fd, ')');
} else { if (t.kind == nkind.N_TENUM) {
wputs(fd, "enum ");
if (t.lhs != nil) {
wwitype(fd, t.lhs);
wputb(fd, 32u8);
};
wputs(fd, "{ ");
let m: *node = t.list;
for (m != nil) {
if (m != t.list) { wputs(fd, ", "); };
wputs(fd, m.str);
if (m.lhs != nil) {
wputs(fd, " = ");
wwiexpr(fd, m.lhs);
};
m = m.next;
};
wputs(fd, " }");
} else {
wputs(2, "wwi: unhandled type node kind\n");
os.exit(1);
};};};};};};};};};};};
};
fn wwidecl(fd: i32, d: *node) void = {
if (d.kind == nkind.N_FNDECL) {
wputs(fd, "export fn ");
wputs(fd, d.str);
wputb(fd, '(');
let p: *node = d.list;
for (p != nil) {
if (p != d.list) { wputs(fd, ", "); };
wwiparam(fd, p);
p = p.next;
};
wputs(fd, ") ");
wwitype(fd, d.lhs);
wputs(fd, ";\n");
} else { if (d.kind == nkind.N_TYPEDECL) {
wputs(fd, "export type ");
wputs(fd, d.str);
wputs(fd, " = ");
wwitype(fd, d.lhs);
wputs(fd, ";\n");
} else { if (d.kind == nkind.N_DEF) {
wputs(fd, "export def ");
wputs(fd, d.str);
wputs(fd, ": ");
wwitype(fd, d.lhs);
wputs(fd, " = ");
wwiexpr(fd, d.rhs);
wputs(fd, ";\n");
} else { if (d.kind == nkind.N_LET) {
wputs(fd, "export let ");
wputs(fd, d.str);
wputs(fd, ": ");
if (d.lhs == nil) {
wputs(2, "wwi: exported let has no declared type\n");
os.exit(1);
};
wwitype(fd, d.lhs);
wputs(fd, ";\n");
};};};};
};
// --- deterministic ordering (rob §3) ----------------------------------
fn wwiprimary(n: *node) bool = {
// imported==1 marks a decl reached through a `//ww:module <path>`
// boundary (an imported module's concatenated section).
if (n == nil) { return false; };
return n.imported == 0;
};
fn wwiisdecl(d: *node) bool = {
return d.kind == nkind.N_FNDECL || d.kind == nkind.N_TYPEDECL ||
d.kind == nkind.N_DEF || d.kind == nkind.N_LET;
};
// strcmp — byte lexicographic, mirror C strcmp sign (<0/0/>0). Both
// stages key the sort identically, so the `.wwi` order is deterministic.
fn wwistrcmp(a: str, b: str) i32 = {
let i: i32 = 0;
for (i < a.len && i < b.len) {
let ca: i32 = a[i]: i32;
let cb: i32 = b[i]: i32;
if (ca != cb) { return ca - cb; };
i += 1;
};
return a.len - b.len;
};
// Selection sort over parallel (key, node) arrays. Total order keyed on
// the symbol name; ties broken by original index — so the result is
// stable regardless of any same-name collision, matching cstage's qsort
// + idx tiebreak.
fn wwisortdecls(keys: []str, nodes: []*node, n: i32) void = {
let i: i32 = 0;
for (i < n) {
let best: i32 = i;
let j: i32 = i + 1;
for (j < n) {
let r: i32 = wwistrcmp(keys[j], keys[best]);
if (r < 0) { best = j; };
j += 1;
};
if (best != i) {
let tk: str = keys[i]; keys[i] = keys[best]; keys[best] = tk;
let tn: *node = nodes[i]; nodes[i] = nodes[best]; nodes[best] = tn;
};
i += 1;
};
};
export fn wwiemit(c: *checker, file: *node, path: str) i32 = {
// §5: check_exported_type FIRST, before any byte — a producer
// without it can emit a dangling `.wwi`.
let bad: i32 = 0;
let d: *node = file.list;
for (d != nil) {
if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) {
bad = bad | wwicheckdecl(c, d);
};
d = d.next;
};
if (bad != 0) { return 1i32; };
let fd: i32 = os.open(path,
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (fd < 0) {
wputs(2, "w6c: cannot open ");
wputs(2, path);
wputs(2, "\n");
return 1i32;
};
// package line: leaf of the first primary decl's module tag.
let pkg: str = "main";
let pd: *node = file.list;
for (pd != nil) {
if (wwiprimary(pd) && pd.nmod.len > 0) {
let dotidx: i32 = -1;
let i: i32 = 0;
for (i < pd.nmod.len) {
if (pd.nmod[i] == 46u8) { dotidx = i; };
i += 1;
};
if (dotidx >= 0) {
let leaf: str;
leaf.ptr = pd.nmod.ptr + ((dotidx + 1): u64);
leaf.len = pd.nmod.len - dotidx - 1;
pkg = leaf;
} else {
pkg = pd.nmod;
};
pd = nil;
} else {
pd = pd.next;
};
};
wputs(fd, "package ");
wputs(fd, pkg);
wputs(fd, ";\n");
// imports — primary N_USE, byte-sorted by import path.
let nuse: i32 = 0;
let u: *node = file.list;
for (u != nil) {
if (u.kind == nkind.N_USE && wwiprimary(u)) { nuse += 1; };
u = u.next;
};
if (nuse > 0) {
let upaths: []str = alloc([], nuse: u64)!;
upaths.len = nuse;
let unodes: []*node = alloc([], nuse: u64)!;
unodes.len = nuse;
let k: i32 = 0;
u = file.list;
for (u != nil) {
if (u.kind == nkind.N_USE && wwiprimary(u)) {
if (u.usepath.len > 0) { upaths[k] = u.usepath; } else { upaths[k] = u.str; };
unodes[k] = u;
k += 1;
};
u = u.next;
};
wwisortdecls(upaths, unodes, nuse);
let i: i32 = 0;
for (i < nuse) {
wputs(fd, "import ");
wputs(fd, upaths[i]);
wputs(fd, ";\n");
i += 1;
};
};
// decls — exported primary, byte-sorted by symbol name.
let ndecl: i32 = 0;
d = file.list;
for (d != nil) {
if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { ndecl += 1; };
d = d.next;
};
if (ndecl > 0) {
let dkeys: []str = alloc([], ndecl: u64)!;
dkeys.len = ndecl;
let dnodes: []*node = alloc([], ndecl: u64)!;
dnodes.len = ndecl;
let k: i32 = 0;
d = file.list;
for (d != nil) {
if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) {
dkeys[k] = d.str;
dnodes[k] = d;
k += 1;
};
d = d.next;
};
wwisortdecls(dkeys, dnodes, ndecl);
let i: i32 = 0;
for (i < ndecl) {
wwidecl(fd, dnodes[i]);
i += 1;
};
};
os.close(fd);
return 0i32;
};
//ww:module-reset
// selfhost/cmd/w6c/main.ww — port of cmd/w6c/main.c.
//
@@ -45314,6 +45872,7 @@ import typ;
import sym;
import check;
import cgen;
import wwi;
fn cstreq(a: *u8, lit: str) bool = {
let n: u64 = lit.len: u64;
@@ -45370,6 +45929,7 @@ fn slurp(path: *u8) (*u8, u64) = {
export fn main(argc: i32, argv: **u8) i32 = {
let src: *u8 = nil;
let out: *u8 = nil;
let wwiout: *u8 = nil; // -I <out.wwi>: M2 export-data producer
let testmode: i32 = 0i32; // #15: `-T` test-mode
let i: i32 = 1;
@@ -45383,6 +45943,14 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2;
};
out = argv[i];
} else { if (cstreq(a, "-I")) {
i += 1;
if (i >= argc) {
let m: str = "w6c: -I requires arg\n";
os.write(2, m.ptr, m.len: u64);
return 2;
};
wwiout = argv[i];
} else { if (cstreq(a, "-T")) {
testmode = 1i32;
} else { if (a[0u64] == 45u8) {
@@ -45396,7 +45964,7 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2;
};
src = a;
}; }; };
}; }; }; };
i += 1;
};
@@ -45466,6 +46034,13 @@ export fn main(argc: i32, argv: **u8) i32 = {
checkfile(&ck, f);
if (ck.errs > 0) { return 1; };
// M2 export-data: write the `.wwi` after a clean check, before cgen.
// Dead on the live path (no existing invocation passes -I); the
// producer's check_exported_type may reject a dangling export.
if (wwiout != nil) {
if (wwiemit(&ck, f, pathstr(wwiout)) != 0) { return 1; };
};
let cg: cgen;
cgeninit(&cg);
cgfile(&cg, f);

View File

@@ -23,6 +23,7 @@ import typ;
import sym;
import check;
import cgen;
import wwi;
fn cstreq(a: *u8, lit: str) bool = {
let n: u64 = lit.len: u64;
@@ -79,6 +80,7 @@ fn slurp(path: *u8) (*u8, u64) = {
export fn main(argc: i32, argv: **u8) i32 = {
let src: *u8 = nil;
let out: *u8 = nil;
let wwiout: *u8 = nil; // -I <out.wwi>: M2 export-data producer
let testmode: i32 = 0i32; // #15: `-T` test-mode
let i: i32 = 1;
@@ -92,6 +94,14 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2;
};
out = argv[i];
} else { if (cstreq(a, "-I")) {
i += 1;
if (i >= argc) {
let m: str = "w6c: -I requires arg\n";
os.write(2, m.ptr, m.len: u64);
return 2;
};
wwiout = argv[i];
} else { if (cstreq(a, "-T")) {
testmode = 1i32;
} else { if (a[0u64] == 45u8) {
@@ -105,7 +115,7 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 2;
};
src = a;
}; }; };
}; }; }; };
i += 1;
};
@@ -175,6 +185,13 @@ export fn main(argc: i32, argv: **u8) i32 = {
checkfile(&ck, f);
if (ck.errs > 0) { return 1; };
// M2 export-data: write the `.wwi` after a clean check, before cgen.
// Dead on the live path (no existing invocation passes -I); the
// producer's check_exported_type may reject a dangling export.
if (wwiout != nil) {
if (wwiemit(&ck, f, pathstr(wwiout)) != 0) { return 1; };
};
let cg: cgen;
cgeninit(&cg);
cgfile(&cg, f);

556
selfhost/cmd/wcc/wwi.ww Normal file
View File

@@ -0,0 +1,556 @@
// wwi.ww — `.wwi` export-data producer (w6c_ww -I). M2 DEAD-CODE: writes
// a re-parseable ww-prototype rendering of a package's EXPORTED surface.
// Nothing consumes `.wwi` yet (combined.ww stays the live path); the only
// caller is the new -I flag, off on every existing invocation.
//
// wwstage twin of cmd/w6c/wwi.c — byte-identical output is a rule-10
// requirement (`.wwi` is a new cross-stage byte-id substrate). Specs:
// .ai/rob-M2-spec.md + .ai/drew-M2-checkexported.md.
//
// - 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), so M2
// stays dead on the normal `.s` path. It rejects exactly one thing:
// an exported signature naming a non-exported nominal type.
// - A `.wwi` is ONE package's interface; the M2 gate feeds a driver-
// combined unit, so the emit filters to PRIMARY decls (imported==0).
// Imports mirror check.ww (os/tok/strconv only): node/nkind/sym/skind/
// scopelookuptype/streq/checker/tkind resolve bare in the selfhost's flat
// bundled scope, exactly as check.ww references them.
package wcc;
import os;
import tok;
import strconv;
// --- byte writers ------------------------------------------------------
fn wputs(fd: i32, s: str) void = {
os.write(fd, s.ptr, s.len: u64);
};
fn wputb(fd: i32, b: u8) void = {
let buf: [1]u8;
buf[0] = b;
os.write(fd, buf.ptr, 1u64);
};
fn wquote(fd: i32, s: str) void = {
wputb(fd, '"');
let i: i32 = 0;
for (i < s.len) {
let c: u8 = s[i];
if (c == '"') {
wputs(fd, "\\\"");
} else { if (c == '\\') {
wputs(fd, "\\\\");
} else { if (c == '\n') {
wputs(fd, "\\n");
} else { if (c == '\t') {
wputs(fd, "\\t");
} else { if (c < 32u8) {
let hi: u8 = c >> 4u8;
let lo: u8 = c & 15u8;
let h: u8 = 0u8;
let l: u8 = 0u8;
if (hi < 10u8) { h = hi + 48u8; } else { h = (hi - 10u8) + 97u8; };
if (lo < 10u8) { l = lo + 48u8; } else { l = (lo - 10u8) + 97u8; };
let buf: [4]u8;
buf[0] = 92u8;
buf[1] = 120u8;
buf[2] = h;
buf[3] = l;
os.write(fd, buf.ptr, 4u64);
} else {
wputb(fd, c);
};};};};};
i += 1;
};
wputb(fd, '"');
};
// --- check_exported_type (drew) ---------------------------------------
//
// Resolve an N_TNAME to its type sym via the public sym helpers (mirror
// of cstage wwi_typesym / scope_lookup_type) WITHOUT the side effects of
// the checker's aliassym (no on-demand resolve, no double error). A
// primitive/keyword resolves to no SK_TYPE → leaf. By producer time
// checkfile has finished and c.cur == c.top.
fn wwitypesym(c: *checker, nm: str) *sym = {
let empty: str;
let s: *sym = scopelookuptype(c.cur, empty, nm);
if (s == nil) {
let dotidx: i32 = -1;
let i: i32 = 0;
for (i < nm.len) {
if (nm[i] == 46u8) { dotidx = i; };
i += 1;
};
if (dotidx >= 0) {
let leaf: str;
leaf.ptr = nm.ptr + ((dotidx + 1): u64);
leaf.len = nm.len - dotidx - 1;
s = scopelookuptype(c.cur, empty, leaf);
};
};
if (s == nil) { return nil; };
if (s.skind != skind.SK_TYPE) { return nil; };
return s;
};
fn wwireject(d: *node, nm: str) void = {
wputs(2, d.file);
wputs(2, ":");
wputs(2, strconv.u64tos(d.line: u64, strconv.base.DEC));
wputs(2, ":");
wputs(2, strconv.u64tos(d.col: u64, strconv.base.DEC));
wputs(2, ": error: exported declaration references unexported type '");
wputs(2, nm);
wputs(2, "'\n");
};
// Returns 1 if a non-exported nominal was named (loud), else 0.
fn wwichecktype(c: *checker, d: *node, t: *node) i32 = {
if (t == nil) { return 0; };
// rule-10: the wwstage checker wraps N_TTUPLE.list elements in
// N_TPARAM (ast.ww:101); cstage keeps the type-AST pristine. Unwrap
// transparently so the recursion sees the same shape cstage walks.
if (t.kind == nkind.N_TPARAM) { return wwichecktype(c, d, t.lhs); };
let bad: i32 = 0;
if (t.kind == nkind.N_TNAME) {
let s: *sym = wwitypesym(c, 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 == 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;
};
};
};
};
};
} else { if (
t.kind == nkind.N_TPTR ||
t.kind == nkind.N_TSLICE ||
t.kind == nkind.N_TBANG ||
t.kind == nkind.N_TCHAN
) {
bad = bad | wwichecktype(c, d, t.lhs);
} else { if (t.kind == nkind.N_TARRAY) {
// element only; the length is a const-expr, not a type.
bad = bad | wwichecktype(c, d, t.lhs);
} else { if (t.kind == nkind.N_TFN) {
let p: *node = t.list;
for (p != nil) {
bad = bad | wwichecktype(c, d, p.lhs);
p = p.next;
};
bad = bad | wwichecktype(c, d, t.lhs);
} else { if (t.kind == nkind.N_TSTRUCT) {
let f: *node = t.list;
for (f != nil) {
bad = bad | wwichecktype(c, d, f.lhs);
f = f.next;
};
} else { if (t.kind == nkind.N_TTAGGED || t.kind == nkind.N_TTUPLE) {
let e: *node = t.list;
for (e != nil) {
bad = bad | wwichecktype(c, d, e);
e = e.next;
};
} else { if (t.kind == nkind.N_TENUM) {
// the inline enum body is the definition, not a reference;
// recurse only its storage type (members are values).
bad = bad | wwichecktype(c, d, t.lhs);
};};};};};};};
return bad;
};
fn wwicheckdecl(c: *checker, d: *node) i32 = {
let bad: i32 = 0;
if (d.kind == nkind.N_FNDECL) {
let p: *node = d.list;
for (p != nil) {
bad = bad | wwichecktype(c, d, p.lhs);
p = p.next;
};
bad = bad | wwichecktype(c, d, d.lhs); // ret
} else { if (d.kind == nkind.N_TYPEDECL) {
bad = bad | wwichecktype(c, d, d.lhs);
} else { if (d.kind == nkind.N_DEF) {
// the declared type; the rhs const value is not a type.
bad = bad | wwichecktype(c, d, d.lhs);
} else { if (d.kind == nkind.N_LET) {
bad = bad | wwichecktype(c, d, d.lhs);
};};};};
return bad;
};
// --- type-expr + const-expr unparser (rob §2.2/§2.4) ------------------
fn wwiexpr(fd: i32, e: *node) void = {
if (e == nil) { return; };
if (e.kind == nkind.N_INTLIT) {
wputs(fd, strconv.u64tos(e.uval, strconv.base.DEC));
if (e.tsuffix.len > 0) { wputs(fd, e.tsuffix); };
} else { if (e.kind == nkind.N_IDENT || e.kind == nkind.N_TNAME) {
wputs(fd, e.str);
} else { if (e.kind == nkind.N_DOT) {
wwiexpr(fd, e.lhs);
wputb(fd, '.');
wputs(fd, e.str);
} else { if (e.kind == nkind.N_TRUE) {
wputs(fd, "true");
} else { if (e.kind == nkind.N_FALSE) {
wputs(fd, "false");
} else { if (e.kind == nkind.N_NIL) {
wputs(fd, "nil");
} else { if (e.kind == nkind.N_STRLIT) {
wquote(fd, e.str);
} else { if (e.kind == nkind.N_BIN) {
wwiexpr(fd, e.lhs);
wputb(fd, 32u8);
wputs(fd, tokname(e.op));
wputb(fd, 32u8);
wwiexpr(fd, e.rhs);
} else { if (e.kind == nkind.N_UN) {
wputs(fd, tokname(e.op));
wwiexpr(fd, e.lhs);
} else {
wputs(2, "wwi: unhandled const-expr node kind\n");
os.exit(1);
};};};};};};};};};
};
fn wwiparam(fd: i32, p: *node) void = {
if (streq(p.str, "...")) { // C-style FFI `...`
wputs(fd, "...");
return;
};
if (p.str.len > 0) {
wputs(fd, p.str);
wputs(fd, ": ");
};
// rule-10: the wwstage checker desugars a Hare variadic `T...` param
// in place to `[]T` (lhs becomes N_TSLICE); cstage leaves lhs == T.
// Peel the inserted slice so both stages emit the surface `T...`.
if (p.op == tkind.TK_ELLIPSIS && p.lhs != nil && p.lhs.kind == nkind.N_TSLICE) {
wwitype(fd, p.lhs.lhs);
} else {
wwitype(fd, p.lhs);
};
if (p.op == tkind.TK_ELLIPSIS) { // Hare `T...` variadic
wputs(fd, "...");
};
};
fn wwitype(fd: i32, t: *node) void = {
if (t == nil) { // absent return type spells void
wputs(fd, "void");
return;
};
// rule-10: unwrap the wwstage-only N_TPARAM tuple-element wrapper
// (ast.ww:101) so the unparse matches cstage's pristine type-AST.
if (t.kind == nkind.N_TPARAM) { wwitype(fd, t.lhs); return; };
if (t.kind == nkind.N_TNAME) {
if (t.str.len > 0) { wputs(fd, t.str); } else { wputs(fd, "void"); };
} else { if (t.kind == nkind.N_TPTR) {
wputb(fd, '*');
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TSLICE) {
wputs(fd, "[]");
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TARRAY) {
wputb(fd, '[');
if (t.rhs != nil) { wwiexpr(fd, t.rhs); } else { wputb(fd, '_'); };
wputb(fd, ']');
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TBANG) {
wputb(fd, '!');
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TCHAN) {
wputs(fd, "chan ");
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TFN) {
wputs(fd, "fn(");
let p: *node = t.list;
for (p != nil) {
if (p != t.list) { wputs(fd, ", "); };
wwiparam(fd, p);
p = p.next;
};
wputs(fd, ") ");
wwitype(fd, t.lhs);
} else { if (t.kind == nkind.N_TSTRUCT) {
wputs(fd, "struct { ");
let f: *node = t.list;
for (f != nil) {
if (f != t.list) { wputs(fd, ", "); };
if (f.str.len > 0) {
wputs(fd, f.str);
wputs(fd, ": ");
};
wwitype(fd, f.lhs);
f = f.next;
};
wputs(fd, " }");
} else { if (t.kind == nkind.N_TTUPLE) {
wputb(fd, '(');
let e: *node = t.list;
for (e != nil) {
if (e != t.list) { wputs(fd, ", "); };
wwitype(fd, e);
e = e.next;
};
wputb(fd, ')');
} else { if (t.kind == nkind.N_TTAGGED) {
wputb(fd, '(');
let e: *node = t.list;
for (e != nil) {
if (e != t.list) { wputs(fd, " | "); };
wwitype(fd, e);
e = e.next;
};
wputb(fd, ')');
} else { if (t.kind == nkind.N_TENUM) {
wputs(fd, "enum ");
if (t.lhs != nil) {
wwitype(fd, t.lhs);
wputb(fd, 32u8);
};
wputs(fd, "{ ");
let m: *node = t.list;
for (m != nil) {
if (m != t.list) { wputs(fd, ", "); };
wputs(fd, m.str);
if (m.lhs != nil) {
wputs(fd, " = ");
wwiexpr(fd, m.lhs);
};
m = m.next;
};
wputs(fd, " }");
} else {
wputs(2, "wwi: unhandled type node kind\n");
os.exit(1);
};};};};};};};};};};};
};
fn wwidecl(fd: i32, d: *node) void = {
if (d.kind == nkind.N_FNDECL) {
wputs(fd, "export fn ");
wputs(fd, d.str);
wputb(fd, '(');
let p: *node = d.list;
for (p != nil) {
if (p != d.list) { wputs(fd, ", "); };
wwiparam(fd, p);
p = p.next;
};
wputs(fd, ") ");
wwitype(fd, d.lhs);
wputs(fd, ";\n");
} else { if (d.kind == nkind.N_TYPEDECL) {
wputs(fd, "export type ");
wputs(fd, d.str);
wputs(fd, " = ");
wwitype(fd, d.lhs);
wputs(fd, ";\n");
} else { if (d.kind == nkind.N_DEF) {
wputs(fd, "export def ");
wputs(fd, d.str);
wputs(fd, ": ");
wwitype(fd, d.lhs);
wputs(fd, " = ");
wwiexpr(fd, d.rhs);
wputs(fd, ";\n");
} else { if (d.kind == nkind.N_LET) {
wputs(fd, "export let ");
wputs(fd, d.str);
wputs(fd, ": ");
if (d.lhs == nil) {
wputs(2, "wwi: exported let has no declared type\n");
os.exit(1);
};
wwitype(fd, d.lhs);
wputs(fd, ";\n");
};};};};
};
// --- deterministic ordering (rob §3) ----------------------------------
fn wwiprimary(n: *node) bool = {
// imported==1 marks a decl reached through a `//ww:module <path>`
// boundary (an imported module's concatenated section).
if (n == nil) { return false; };
return n.imported == 0;
};
fn wwiisdecl(d: *node) bool = {
return d.kind == nkind.N_FNDECL || d.kind == nkind.N_TYPEDECL ||
d.kind == nkind.N_DEF || d.kind == nkind.N_LET;
};
// strcmp — byte lexicographic, mirror C strcmp sign (<0/0/>0). Both
// stages key the sort identically, so the `.wwi` order is deterministic.
fn wwistrcmp(a: str, b: str) i32 = {
let i: i32 = 0;
for (i < a.len && i < b.len) {
let ca: i32 = a[i]: i32;
let cb: i32 = b[i]: i32;
if (ca != cb) { return ca - cb; };
i += 1;
};
return a.len - b.len;
};
// Selection sort over parallel (key, node) arrays. Total order keyed on
// the symbol name; ties broken by original index — so the result is
// stable regardless of any same-name collision, matching cstage's qsort
// + idx tiebreak.
fn wwisortdecls(keys: []str, nodes: []*node, n: i32) void = {
let i: i32 = 0;
for (i < n) {
let best: i32 = i;
let j: i32 = i + 1;
for (j < n) {
let r: i32 = wwistrcmp(keys[j], keys[best]);
if (r < 0) { best = j; };
j += 1;
};
if (best != i) {
let tk: str = keys[i]; keys[i] = keys[best]; keys[best] = tk;
let tn: *node = nodes[i]; nodes[i] = nodes[best]; nodes[best] = tn;
};
i += 1;
};
};
export fn wwiemit(c: *checker, file: *node, path: str) i32 = {
// §5: check_exported_type FIRST, before any byte — a producer
// without it can emit a dangling `.wwi`.
let bad: i32 = 0;
let d: *node = file.list;
for (d != nil) {
if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) {
bad = bad | wwicheckdecl(c, d);
};
d = d.next;
};
if (bad != 0) { return 1i32; };
let fd: i32 = os.open(path,
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (fd < 0) {
wputs(2, "w6c: cannot open ");
wputs(2, path);
wputs(2, "\n");
return 1i32;
};
// package line: leaf of the first primary decl's module tag.
let pkg: str = "main";
let pd: *node = file.list;
for (pd != nil) {
if (wwiprimary(pd) && pd.nmod.len > 0) {
let dotidx: i32 = -1;
let i: i32 = 0;
for (i < pd.nmod.len) {
if (pd.nmod[i] == 46u8) { dotidx = i; };
i += 1;
};
if (dotidx >= 0) {
let leaf: str;
leaf.ptr = pd.nmod.ptr + ((dotidx + 1): u64);
leaf.len = pd.nmod.len - dotidx - 1;
pkg = leaf;
} else {
pkg = pd.nmod;
};
pd = nil;
} else {
pd = pd.next;
};
};
wputs(fd, "package ");
wputs(fd, pkg);
wputs(fd, ";\n");
// imports — primary N_USE, byte-sorted by import path.
let nuse: i32 = 0;
let u: *node = file.list;
for (u != nil) {
if (u.kind == nkind.N_USE && wwiprimary(u)) { nuse += 1; };
u = u.next;
};
if (nuse > 0) {
let upaths: []str = alloc([], nuse: u64)!;
upaths.len = nuse;
let unodes: []*node = alloc([], nuse: u64)!;
unodes.len = nuse;
let k: i32 = 0;
u = file.list;
for (u != nil) {
if (u.kind == nkind.N_USE && wwiprimary(u)) {
if (u.usepath.len > 0) { upaths[k] = u.usepath; } else { upaths[k] = u.str; };
unodes[k] = u;
k += 1;
};
u = u.next;
};
wwisortdecls(upaths, unodes, nuse);
let i: i32 = 0;
for (i < nuse) {
wputs(fd, "import ");
wputs(fd, upaths[i]);
wputs(fd, ";\n");
i += 1;
};
};
// decls — exported primary, byte-sorted by symbol name.
let ndecl: i32 = 0;
d = file.list;
for (d != nil) {
if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) { ndecl += 1; };
d = d.next;
};
if (ndecl > 0) {
let dkeys: []str = alloc([], ndecl: u64)!;
dkeys.len = ndecl;
let dnodes: []*node = alloc([], ndecl: u64)!;
dnodes.len = ndecl;
let k: i32 = 0;
d = file.list;
for (d != nil) {
if (wwiprimary(d) && d.exported != 0 && wwiisdecl(d)) {
dkeys[k] = d.str;
dnodes[k] = d;
k += 1;
};
d = d.next;
};
wwisortdecls(dkeys, dnodes, ndecl);
let i: i32 = 0;
for (i < ndecl) {
wwidecl(fd, dnodes[i]);
i += 1;
};
};
os.close(fd);
return 0i32;
};