// 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) ------------------ // The lexer's rune escapes stop at \xHH (no \u/\U — task #50), so a // codepoint above 0xff can't render as a re-parseable rune literal; fail // loud rather than emit a malformed one. No exported def names such a rune // today (RUNE_MAX is written as an int-cast for the same reason). fn wwirune(fd: i32, cp: u64) void = { if (cp > 255u64) { wputs(2, "wwi: rune codepoint exceeds \\xHH (task #50)\n"); os.exit(1); }; let c: u8 = cp: u8; wputb(fd, '\''); 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 || c >= 127u8) { 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); };};};};}; wputb(fd, '\''); }; 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_RUNELIT) { // A bare rune literal in a const-expr (types.RUNE_MIN = '\0'); // casts never reach here — the checker folds a const cast to an // integer literal before the producer runs (RUNE_MAX // `0x10ffff: rune` emits as the plain int 1114111). wwirune(fd, e.uval); } 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); };};};};};};};};};};}; }; // Only codegen/link-relevant attributes round-trip into the `.wwi`. Today // that is exactly @symbol (the FFI link-symbol override, read back at cgen // 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. fn wwiattrrelevant(nm: str) bool = { return streq(nm, "symbol"); }; fn wwiattrs(fd: i32, d: *node) void = { let a: *node = d.attr; for (a != nil) { if (a.kind == nkind.N_ATTR && wwiattrrelevant(a.str)) { wputb(fd, '@'); wputs(fd, a.str); if (a.list != nil) { wputb(fd, '('); let arg: *node = a.list; for (arg != nil) { if (arg != a.list) { wputs(fd, ", "); }; wwiexpr(fd, arg); arg = arg.next; }; wputb(fd, ')'); }; wputb(fd, 32u8); }; a = a.next; }; }; fn wwidecl(fd: i32, d: *node) void = { if (d.kind == nkind.N_FNDECL) { wwiattrs(fd, d); 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 ` // 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; };