// selfhost/cmd/wcc/cgen.ww — port of cmd/w6c/cgen.c. // // Status: GROWING. Each subsystem we add is verified by `wwdump_ww -c` // producing byte-identical output to C-side `w6c` for the same source, // then by assembling + linking + running the result. // // Current coverage: // - decls: nkind.N_FILE, nkind.N_FNDECL (params, frame for locals, prologue // + dual-epilogue suppression; FFI body-less fn skipped) // - stmts: nkind.N_BLOCK, nkind.N_RETURN, nkind.N_EXPRSTMT, nkind.N_LET (no init), // nkind.N_LET (int-literal / ident / call / nkind.N_BIN init), // nkind.N_IF (with optional else), nkind.N_FOR (cond-only and full // init/cond/post), nkind.N_BREAK, nkind.N_CONTINUE // - exprs: nkind.N_INTLIT, nkind.N_IDENT (local/param), nkind.N_BIN with full op // coverage (+/-/*/// %, &/|/^, <>, comparisons with // signed-vs-unsigned dispatch, &&/||), nkind.N_UN (- ! ~ & *), // nkind.N_CALL (recursive R-to-L push, pop into argregs L-to-R), // nkind.N_ASSIGN to local idents (plain and compound +=/-=) // // Type info is shallow — frame slots are 8 bytes per local, all loads // /stores are MOVQ. Programs that mix i8/i32/i64 locals work but spill // 8 bytes per local. Float, str, slice, struct, match, defer, alloc, // tagged-union return — none of those are wired yet. package wcc; import os; import ast; import tok; import typ; import sym; import strconv; import io; import memio; // Split files. Bundler pulls these in transitively so consumers only // need `use cgen;`. Order matters for the flat-bundle concat — utils // first so cgenexpr/stmt/decl can reference helpers defined here. import cgenutil; import cgenexpr; import cgenstmt; import cgendecl; // ---- typedef alias registry ----------------------------------------- // // `type error = str;` makes `error` a struct-shape alias. We track // alias→target so isstrtype / isslicetype / structlookup can // resolve through the chain. Only direct nkind.N_TNAME aliases are mapped; // `type p = struct {...}` is handled by collectstructs. type aliasent = struct { aname: str, amod: str, // originating module (`// MODULE: foo`), or empty target: *node, // the rhs type expr aanext: *aliasent, }; fn collectaliases(c: *cgen, file: *node) void = { c.aliases = nil; // #29: seed `type nomem = !void;` here AS WELL AS in check.ww's // seedprimitives. The two seeds aren't redundant: wwstage's check // owns c.top (used by name resolution); cgen owns its own // c.aliases chain (used by resolvetype / slotsize / TBANG checks). // Without this seed, resolvetype("nomem") returns the raw N_TNAME // — slotsize falls through to 8B without zero-init, diverging from // cstage's `let e: nomem;` MOVQ $0 emit on the slot (rule 10). // Inserted at the head so the user-decl loop below prepends; the // same-module / any-match passes in aliaslookup then let a local // `type nomem = !void;` shadow this fallback within its module. let empty: str; let tnvoid: *node = newnode(nkind.N_TNAME, empty, 0, 0); tnvoid.str = "void"; let bang: *node = newnode(nkind.N_TBANG, empty, 0, 0); bang.lhs = tnvoid; let nomemal: *aliasent = alloc(aliasent{aname="nomem", amod=empty, target=bang, aanext=nil})!; c.aliases = nomemal; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_TYPEDECL) { let body: *node = d.lhs; if (body != nil) { if (body.kind != nkind.N_TSTRUCT) { let a: *aliasent = alloc(aliasent{aname=d.str, amod=d.nmod, target=body, aanext=c.aliases})!; c.aliases = a; }; }; }; d = d.next; }; }; fn aliaslookup(c: *cgen, name: str) *node = { // Same-module first, then any. Mirrors cstage's scope_lookup_prefer // (cmd/wcc/check.c:65); without the prefer pass a bare `invalid` // in module M with `type invalid = !void;` can collapse onto a // strconv-style `type invalid = !i32;` registered earlier in // c.aliases (head-first walk). The leaf-collision then drives a // narrow MOVSXD load of a slot the let-decl zero-inits 8B-wide // (task #27 silent-correct-by-zero-init). let a: *aliasent = c.aliases; for (a != nil) { if (streq(a.aname, name)) { if (streq(a.amod, c.curmod)) { return a.target; }; }; a = a.aanext; }; a = c.aliases; for (a != nil) { if (streq(a.aname, name)) { return a.target; }; a = a.aanext; }; // Module-qualified form: `pkg.alias` → match the leaf name // scoped to its originating module. Mirrors check.c's module- // qualified type resolution; requiring `amod == pkg` is what // prevents two modules with same-leaf-name aliases from // collapsing into whichever entry appears first in the chain. let i: i32 = name.len - 1; for (i >= 0) { if (name[i] == 46u8) { // '.' let pkg: str; pkg.ptr = name.ptr; pkg.len = i; let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); let b: *aliasent = c.aliases; for (b != nil) { if (streq(b.aname, leaf)) { if (streq(b.amod, pkg)) { return b.target; }; }; b = b.aanext; }; i = -1; } else { i -= 1; }; }; return nil; }; // #223: same-module-ONLY alias resolution. aliaslookup's any-module // fallback can return a foreign same-leaf alias; the alias-peel in // cgdot needs to know whether THIS module defines the name as an alias // (so the peel continues) without that cross-module fallback. Returns // the alias target only when an alias of `name` lives in c.curmod. fn aliassamemod(c: *cgen, name: str) *node = { let a: *aliasent = c.aliases; for (a != nil) { if (streq(a.aname, name)) { if (streq(a.amod, c.curmod)) { return a.target; }; }; a = a.aanext; }; return nil; }; // ---- enum registry -------------------------------------------------- // // Mirrors cmd/wcc/check.c's enum resolution at collect time: walk // every `type Foo = enum [storage] { ... }`, pre-compute each // member's u64 value (supporting auto-increment and sibling refs), // and stash them so cgdot can fold `Foo.MEMBER` → MOVQ $value, AX. // foldintliteral — fold the literal subset usable for top-level // constant slots: int/rune literal, true/false/nil, and a unary // +/-/~ over the same (any depth). No sibling-ident, no binary op. // Shared between enumevalmember (literal leaves) and // emitdefconstants (top-level def rhs). // // Whitelist kept tight on purpose: anything richer (sibling refs, // arithmetic) belongs in enumevalmember, which calls this for its // literal leaves and handles the rest itself. fn foldintliteral(e: *node, out: *u64) bool = { if (e == nil) { return false; }; let k: nkind = e.kind; if (k == nkind.N_INTLIT) { *out = e.uval; return true; }; if (k == nkind.N_RUNELIT) { *out = e.uval; return true; }; if (k == nkind.N_TRUE) { *out = 1u64; return true; }; if (k == nkind.N_FALSE) { *out = 0u64; return true; }; if (k == nkind.N_NIL) { *out = 0u64; return true; }; if (k == nkind.N_UN) { let v: u64; if (!foldintliteral(e.lhs, &v)) { return false; }; let op: tkind = e.op; if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; if (op == tkind.TK_TILDE) { *out = ~v; return true; }; if (op == tkind.TK_PLUS) { *out = v; return true; }; return false; }; return false; }; fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = { if (e == nil) { return false; }; if (foldintliteral(e, out)) { return true; }; let k: nkind = e.kind; if (k == nkind.N_IDENT) { let m: *enummember = prev; for (m != nil) { if (streq(m.mname, e.str)) { *out = m.mval; return true; }; m = m.emnext; }; return false; }; if (k == nkind.N_BIN) { let a: u64; let b: u64; if (!enumevalmember(prev, e.lhs, &a)) { return false; }; if (!enumevalmember(prev, e.rhs, &b)) { return false; }; let op: tkind = e.op; if (op == tkind.TK_PLUS) { *out = a + b; return true; }; if (op == tkind.TK_MINUS) { *out = a - b; return true; }; if (op == tkind.TK_STAR) { *out = a * b; return true; }; if (op == tkind.TK_SLASH) { if (b == 0u64) { return false; }; *out = a / b; return true; }; if (op == tkind.TK_PERCENT) { if (b == 0u64) { return false; }; *out = a % b; return true; }; if (op == tkind.TK_AMP) { *out = a & b; return true; }; if (op == tkind.TK_PIPE) { *out = a | b; return true; }; if (op == tkind.TK_CARET) { *out = a ^ b; return true; }; if (op == tkind.TK_LSHIFT) { *out = a << b; return true; }; if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; }; return false; }; if (k == nkind.N_UN) { let v: u64; if (!enumevalmember(prev, e.lhs, &v)) { return false; }; let op: tkind = e.op; if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; if (op == tkind.TK_TILDE) { *out = ~v; return true; }; if (op == tkind.TK_PLUS) { *out = v; return true; }; return false; }; return false; }; fn collectenums(c: *cgen, file: *node) void = { c.enums = nil; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_TYPEDECL) { let body: *node = d.lhs; if (body != nil) { if (body.kind == nkind.N_TENUM) { let et: *enumtype = alloc(enumtype{ename=d.str, emod=d.nmod, storage=body.lhs, members=nil, etnext=nil})!; let prev: u64 = (-1i64): u64; let mhead: *enummember = nil; let mtail: *enummember = nil; let m: *node = body.list; for (m != nil) { let val: u64; if (m.lhs == nil) { val = prev + 1u64; } else { if (!enumevalmember(mhead, m.lhs, &val)) { val = prev + 1u64; }; }; prev = val; let em: *enummember = alloc(enummember{mname=m.str, mval=val, emnext=nil})!; if (mhead == nil) { mhead = em; mtail = em; } else { mtail.emnext = em; mtail = em; }; m = m.next; }; et.members = mhead; et.etnext = c.enums; c.enums = et; }; }; }; d = d.next; }; }; fn enumlookup(c: *cgen, name: str) *enumtype = { // Same-module first, then any. Trio-leaf graduation mirroring // aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without // the prefer pass a bare-leaf enum ident in module M can collapse // onto another module's same-leaf enum prepended earlier in // c.enums, silently folding `Foo.MEMBER` to the wrong constant. let e: *enumtype = c.enums; for (e != nil) { if (streq(e.ename, name)) { if (streq(e.emod, c.curmod)) { return e; }; }; e = e.etnext; }; e = c.enums; for (e != nil) { if (streq(e.ename, name)) { return e; }; e = e.etnext; }; // Module-qualified form embedded in name (`pkg.enum`): scope the // leaf to its originating module. The `emod == pkg` guard prevents // same-leaf enums in two modules from collapsing. let i: i32 = name.len - 1; for (i >= 0) { if (name[i] == 46u8) { // '.' let pkg: str; pkg.ptr = name.ptr; pkg.len = i; let leaf: str; leaf.ptr = name.ptr + ((i + 1): u64); leaf.len = name.len - (i + 1); let b: *enumtype = c.enums; for (b != nil) { if (streq(b.ename, leaf)) { if (streq(b.emod, pkg)) { return b; }; }; b = b.etnext; }; return nil; }; i -= 1; }; return nil; }; // enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER` // where the qualifier is an explicit N_IDENT module name. Mirrors // fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the // bare enumlookup so a missing or empty mod still finds the leaf. fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = { if (mod.len > 0) { let e: *enumtype = c.enums; for (e != nil) { if (streq(e.ename, name)) { if (streq(e.emod, mod)) { return e; }; }; e = e.etnext; }; }; return enumlookup(c, name); }; fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = { let m: *enummember = en.members; for (m != nil) { if (streq(m.mname, mname)) { *out = m.mval; return true; }; m = m.emnext; }; return false; }; // resolvetype — follow typedef alias chains to a "canonical" type // expr (str/slice/array/struct/...). Stops on cycles via depth limit. fn resolvetype(c: *cgen, t: *node) *node = { let cur: *node = t; let depth: i32 = 0; for (depth < 16) { if (cur == nil) { return nil; }; if (cur.kind != nkind.N_TNAME) { return cur; }; let nm: str = cur.str; let next: *node = aliaslookup(c, nm); if (next == nil) { return cur; }; cur = next; depth += 1; }; return cur; }; // ---- struct registry ------------------------------------------------ // // Per-file map from struct name → list of fields with computed offsets // and sizes. Built when cgfile walks nkind.N_TYPEDECL with nkind.N_TSTRUCT lhs. // nkind.N_DOT and nkind.N_ASSIGN consult this to resolve `s.field` for struct or // *struct bases. type fieldinfo = struct { fname: str, foff: i32, fsz: i32, tnode: *node, // the field type expr, for nested struct lookups finext: *fieldinfo, }; type structinfo = struct { sname: str, smod: str, // originating module (`// MODULE: foo`), or empty fields: *fieldinfo, totsize: i32, sinext: *structinfo, }; // ---- locals / frame -------------------------------------------------- type local = struct { name: str, off: i32, sz: i32, // allocated slot size; carried so @-prefix reuse can // fail-loud (rule 7) if a later site needs a larger // slot than the first allocation pinned. Per #15/#26c // size-strategy convergence — wwstage dropped its // scanlocals pre-pass, so @tagscr/@retscr/@sretscr/ // @tagbase are sized at first-use; subsequent uses // must fit. tnode: *node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil lnext: *local, }; // strlit — interned string literal record. Emitted as a DATA directive // after all functions; cgexpr nkind.N_STRLIT loads (LEAQ ptr, MOVQ len). type strlit = struct { label: str, // "_S_" bytes: str, slnext: *strlit, }; // ffi — `@symbol("name")` mapping. Body-less fn `foo` with this attr // gets its CALL target rewritten to `name`. type ffi = struct { ident: str, symbol: str, fnext: *ffi, }; // enummember — one (name, value) pair belonging to a registered enum. // Values are pre-computed at collect time (Hare allows sibling refs // like `RDWR = READ | WRITE`, so we walk the value expr against the // already-resolved siblings). Lookup is linear; enum cardinality is // usually small. type enummember = struct { mname: str, mval: u64, emnext: *enummember, }; type enumtype = struct { ename: str, emod: str, // originating module (`// MODULE: foo`), or empty storage: *node, // AST type expr for the storage type (i32 by default) members: *enummember, etnext: *enumtype, }; def LOOP_MAX: i32 = 16; def DEFER_MAX: i32 = 16; type cgen = struct { locals: *local, // atlocals — persistent registry of `@`-prefix scratch slots // for the current fn. cgblock save/restores c.locals to scope // inner shadows (post-#27); a return/cgindex/cgwidentaggedstore // inside one block must not reallocate @retscr/@tagscr when a // sibling block uses them again. cgblock leaves atlocals alone // so the slot offsets survive. localadd checks here first for // @-prefix names; localfind falls back here when c.locals misses // an @-name. Pre-#15 this was a handful of named offsets on the // cgen (c.retscroff / c.sretargoff / c.sretscroff); post-#15 // every @-name flows through the same registry. atlocals: *local, frame: i32, lastwasreturn: i32, labelseq: i32, strlitseq: i32, strlits: *strlit, ffis: *ffi, defs: *defent, fnrets: *fnret, aliases: *aliasent, structs: *structinfo, enums: *enumtype, mods: *modent, // fn (any export status) + non-exported // let/def/type decls → originating module lets: *letvar, // top-level mutable scalar `let` bindings fnname: str, curmod: str, // current fn's `// MODULE: foo` directive (len=0 // when the fn is in the primary file). Drives // bare-IDENT call mangling — `frob()` from // inside lib/foo binds to `foo.frob` even when // other modules also export `frob`. Set in cgfn // before walking the body. fnret: *node, // declared return type of current fn (or nil) looptop: i32, loopendbuf: []str, // stack of end labels for break loopcontbuf: []str, // stack of cont labels for continue yieldtop: i32, yieldbuf: []str, // stack of match end labels for yield defertop: i32, deferbuf: []*node, // stack of deferred exprs (LIFO at return) // Variadic-call gather state. cgcall bumps this on each gather // emit and uses it to mint `@vararg_d_N` / `@vararg_sl_N` per // callsite; mirrors cstage's mklabel("vararg_d/sl") freshness // so two variadic callsites with different arities in one fn // get distinct slots (the shared slot fail-louds under #15's // @-prefix grow-on-pin discipline). varargseq: i32, // System V AMD64 sret discipline (#23). Plain TY_STRUCT returns // with size > 24B are passed via a hidden first-arg pointer // (RDI) to a caller-prealloc dest; the callee writes through // that pointer and returns it in RAX. // // sretdestoff — caller-side dest BP offset, propagated from a // receive site (cglet / cgassign ident) to the // nested cgexpr → cgcall so the call emits // `LEAQ off(BP), DI` instead of allocating a // scratch. 0 means no receiver wired. // sretforward — set by cgreturn `return f();` from an sret callee to // signal cgcall: source RDI for inner from outer's // saved @sretarg (MOVQ) instead of LEAQ'ing a local // dest. Inner writes into outer's caller-prealloc; // inner's RAX (the dest pointer) is already outer's // return value. Cleared after cgcall consumes it. // // The single-slot caches for @sretarg / @sretscr / @retscr that // used to live here are gone: localadd's `@`-prefix dedup against // c.locals (fail-loud on size grow) is the SSoT now. cgenstmt / // cgenexpr resolve `@sretarg` via localfind when they need the // saved RDI. sretdestoff: i32, // #220: sret receive into a GLOBAL lvalue. A BP-relative i32 // (sretdestoff) can't name a top-level let, so the lhs IDENT node // is carried and emitted as `LEAQ name(SB), DI`. nil means no // global receiver wired; mutually exclusive with sretdestoff. sretdestnode: *node, sretforward: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. // Populated alongside modents; consulted by cgassign, cgdot, cgident // and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot // instead of being silently dropped. tnode is the declared type AST // node — needed to distinguish scalar (8B) from str (16B) globals // when picking the load/store sequence. type letvar = struct { name: str, tnode: *node, lvnext: *letvar, }; fn cgeninit(c: *cgen) void = { c.locals = nil; c.atlocals = nil; c.frame = 0; c.lastwasreturn = 0; c.labelseq = 0; c.varargseq = 0; c.sretdestoff = 0; c.sretdestnode = nil; c.sretforward = 0; // Note: strlit_seq, strlits, ffis are *not* reset here; they // persist across cgfn calls within one file. cgfile resets them // at the start of each compilation unit. c.looptop = 0; let loopendbuf: []str = alloc([], LOOP_MAX: u64)!; c.loopendbuf = loopendbuf; let loopcontbuf: []str = alloc([], LOOP_MAX: u64)!; c.loopcontbuf = loopcontbuf; c.yieldtop = 0; let yieldbuf: []str = alloc([], LOOP_MAX: u64)!; c.yieldbuf = yieldbuf; c.defertop = 0; let deferbuf: []*node = alloc([], DEFER_MAX: u64)!; c.deferbuf = deferbuf; }; // localalloc — append a slot for `name` without dedup. Used for // match-arm bindings, which cstage allocates via cgexpr's by-value // `locals` list — so two separate matches each get fresh slots even // when their bind names collide. fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { let asz: i32 = sz; if (asz < 8) { asz = 8; }; if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; c.frame += asz; let off: i32 = 0 - c.frame; let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=c.locals})!; c.locals = l; return off; }; // localaddstack — register a param at a positive BP offset. Used for // args that overflow the 6 SysV int / 8 float reg windows; the caller // pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP), // etc. (after the saved RIP+BP). No spill instruction is emitted; the // slot IS the caller's stack slot. fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = { let l: *local = alloc(local{name=name, off=off, sz=0, tnode=tnode, lnext=c.locals})!; c.locals = l; }; fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // User-let path (post-#27): always allocate a fresh slot per // binding. Pre-fix this deduped by name to share one slot // across same-name lets in disjoint scopes — inherited from // cstage's localoff. Both stages had the same silent-stack- // corruption bug: an inner 8B `let a: i64` allocated first // would force a later outer `let a: [128]u8` onto the 8B slot, // and `a[127]` would write at +119(BP), past the saved RIP. // // `@`-prefix scratch slots (`@tagscr`, `@retscr`, `@tagbase`, // `@sretarg`, `@sretscr`, `@match_spill`, `@vararg_*`) share // one slot per name per fn. Post #15/#26c the slot is sized // at first use and reused by every later caller; a later // caller asking for a larger slot than the first allocation // pinned fatals (rule 7 — surface, don't silently corrupt // the frame: the pinned offset already neighbours other // locals so the slot can't grow in place). Mirrors cstage's // cg_tagscr / cg_retscr / cg_sretscr same-fn caches in // cmd/w6c/cgen.c (#26 / #15). if (name.len > 0) { if (name[0] == 64u8) { // '@' let asz: i32 = sz; if (asz < 8) { asz = 8; }; if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; let cur: *local = c.atlocals; for (cur != nil) { let cn: str = cur.name; if (streq(cn, name)) { if (asz > cur.sz) { // rule-7 surface, post-#15: pinned slot // offset can't grow in place. let msg: str = "localadd: @-prefix slot grew within fn\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; cur.tnode = tnode; return cur.off; }; cur = cur.lnext; }; // First use: allocate via localalloc (bumps c.frame + // pushes to c.locals so localfind sees it within this // block) and pin a parallel entry in c.atlocals so the // allocation survives cgblock save/restore. let off: i32 = localalloc(c, name, sz, tnode); let at: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=c.atlocals})!; c.atlocals = at; return off; }; }; return localalloc(c, name, sz, tnode); }; fn localfindnode(c: *cgen, name: str) *local = { let l: *local = c.locals; for (l != nil) { let ln: str = l.name; if (streq(ln, name)) { return l; }; l = l.lnext; }; // @-prefix scratch slots survive cgblock save/restore via // c.atlocals; a localfindnode from a sibling/outer block must // still resolve them. if (name.len > 0) { if (name[0] == 64u8) { let a: *local = c.atlocals; for (a != nil) { if (streq(a.name, name)) { return a; }; a = a.lnext; }; }; }; return nil; }; fn localfind(c: *cgen, name: str) i32 = { let l: *local = c.locals; for (l != nil) { let ln: str = l.name; if (ln.len == name.len) { let i: i32 = 0; let eq: bool = true; for (i < name.len) { if (ln[i] != name[i]) { eq = false; i = name.len; } else { i += 1; }; }; if (eq) { return l.off; }; }; l = l.lnext; }; if (name.len > 0) { if (name[0] == 64u8) { let a: *local = c.atlocals; for (a != nil) { if (streq(a.name, name)) { return a.off; }; a = a.lnext; }; }; }; return 0; }; // ---- emit helpers --------------------------------------------------- // Cgfn defers its prologue (TEXT / SUBQ) until after the body so the // frame size reflects every emit-time localadd — the scanlocals pre- // pass that previously pre-computed it was dropped per #15/#26c. The // body is captured into cgoutstate while cgoutmode != 0, then flushed // after the prologue is written to stdout. Module-level state so the // existing emitline/emitint/emitlabel/emitsymname callers don't have // to thread a *cgen they don't already hold. Mirrors cstage's deferred // Prog-chain emit (cmd/w6c/cgen.c cgfn allocates `subsp`/`text` up // front and patches `from.offset` after the body finishes). // // `cgoutinit` guards a one-shot [[memio.dynamic]] wiring so the // backing buffer is sticky across fns: [[cgout_flush]]'s // [[memio.reset]] rewinds `pos`/`len` without touching `cap`, so the // allocation amortises the same way the previous arena buffer did. // Re-init per fn would abandon the buffer (no [[io.close]] path → no // [[os.free]]) and re-grow from 0 via the 8→…→65536 ladder for every // function. Same idiom as lib/log/log.ww:124 `ensureinit`. let cgoutstream: memio.stream; let cgoutmode: i32 = 0; let cgoutinit: i32 = 0; fn cgout_enable() void = { if (cgoutinit == 0) { cgoutstream = memio.dynamic(); cgoutinit = 1; }; cgoutmode = 1; }; fn cgout_disable() void = { cgoutmode = 0; }; fn cgout_flush() void = { if (cgoutstream.pos > 0) { os.write(1, cgoutstream.ptr, cgoutstream.pos: u64); memio.reset(&cgoutstream); }; }; fn emitbytes(p: *u8, n: u64) void = { if (cgoutmode != 0) { let buf: []u8; buf.ptr = p; buf.len = n: i32; // io.write over the embedded vtable (&cgoutstream.vt = io.stream); // memio.dynamicwrite never errors. Bare-discard mirrors // lib/log/log.ww stdprintln. #94 fold-eFinal. io.write(&cgoutstream.vt, buf); } else { os.write(1, p, n); }; }; fn emitline(s: str) void = { emitbytes(s.ptr, s.len: u64); }; fn emitint(v: i64) void = { let s: str = strconv.i64tos(v, strconv.base.DEC); emitbytes(s.ptr, s.len: u64); }; fn emituint(v: u64) void = { let s: str = strconv.u64tos(v, strconv.base.DEC); emitbytes(s.ptr, s.len: u64); }; // emitdispreg — print "disp(reg)" or "(reg)" when disp == 0, the // way Plan 9 6c/6a do. fn emitdispreg(off: i64, reg: str) void = { if (off != 0i64) { emitint(off); }; emitline("("); emitline(reg); emitline(")"); }; // emitmovqload — `MOVQ off(base), dst`, the per-word unit of a // 3-word slice/str header load (cgslicehdr). fn emitmovqload(off: i64, base: str, dst: str) void = { emitline("\tMOVQ\t"); emitdispreg(off, base); emitline(", "); emitline(dst); emitline("\n"); }; // emitoff — print an integer offset, suppressing it entirely when 0. // Use before any emitline("(BP)...") or emitline("(SB)...") sequence. // Plan 9 cc convention: "(BP)" not "0(BP)". fn emitoff(v: i64) void = { if (v != 0i64) { emitint(v); }; }; // mklabel — fresh label ".__" (bare // "_..." when curmod is empty). Returns an arena-owned str. // Mirrors C cgen's mklabel so diffs match. Module-qualified to // avoid cross-module same-leaf collisions (task #13); w6a accepts // '.' in label-cont (lex.c:18). fn mklabel(c: *cgen, prefix: str) str = { let buf: [128]u8; let i: i32 = 0; let mname: str = c.curmod; let j: i32 = 0; for (j < mname.len) { buf[i] = mname[j]; i += 1; j += 1; }; if (mname.len > 0) { buf[i] = 46u8; i += 1; }; // '.' let fname: str = c.fnname; j = 0; for (j < fname.len) { buf[i] = fname[j]; i += 1; j += 1; }; buf[i] = 95u8; i += 1; // '_' j = 0; for (j < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; buf[i] = 95u8; i += 1; // '_' let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; }; c.labelseq += 1; let total: i32 = i + n; let p: []u8 = alloc([], (total: u64) + 1u64)!; let k: i32 = 0; for (k < total) { p[k] = buf[k]; k += 1; }; p[total] = 0u8; let r: str; r.ptr = p.ptr; r.len = total; return r; }; fn emitlabel(s: str) void = { emitbytes(s.ptr, s.len: u64); emitline(":\n"); }; // mkscratchname — fresh local-slot name "._". Used for // compiler-synthesised slots (switch scrutinee, forrange index/len) // that need to be unique per use site but are never referenced by user // code. Increments labelseq so the same source position lines up with // C cgen's labelseq stream. fn mkscratchname(c: *cgen, prefix: str) str = { let buf: [128]u8; let i: i32 = 0; buf[i] = 46u8; i += 1; // '.' let j: i32 = 0; for (j < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; buf[i] = 95u8; i += 1; // '_' let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; }; c.labelseq += 1; let total: i32 = i + n; let p: []u8 = alloc([], (total: u64) + 1u64)!; let k: i32 = 0; for (k < total) { p[k] = buf[k]; k += 1; }; p[total] = 0u8; let r: str; r.ptr = p.ptr; r.len = total; return r; }; // ---- string interning ------------------------------------------------ // // streq is provided by sym.ww and reused here. // internstrlit — return a stable label for `bytes`. Dedups by content // so identical literals share storage. fn internstrlit(c: *cgen, bytes: str) str = { let s: *strlit = c.strlits; for (s != nil) { let bs: str = s.bytes; if (streq(bs, bytes)) { return s.label; }; s = s.slnext; }; // New label "_S_". let buf: [32]u8; buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_" let ns: str = strconv.i64tos(c.strlitseq: i64, strconv.base.DEC); let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { buf[3 + dk] = ns.ptr[dk]; dk += 1; }; c.strlitseq += 1; let total: i32 = 3 + n; let p: []u8 = alloc([], (total: u64) + 1u64)!; let i: i32 = 0; for (i < total) { p[i] = buf[i]; i += 1; }; p[total] = 0u8; let lab: str; lab.ptr = p.ptr; lab.len = total; let nw: *strlit = alloc(strlit{label=lab, bytes=bytes, slnext=c.strlits})!; c.strlits = nw; return lab; }; // letscalarprim — recognise the bare type-name keywords whose values // fit in an 8-byte .data slot and load back with a plain MOVQ. Float // types are handled separately by letfloatprim — they need MOVSS/MOVSD // and use 4-byte (f32) or 8-byte (f64) slots. fn letscalarprim(nm: str) bool = { if (streq(nm, "bool")) { return true; }; if (streq(nm, "rune")) { return true; }; if (streq(nm, "i8")) { return true; }; if (streq(nm, "i16")) { return true; }; if (streq(nm, "i32")) { return true; }; if (streq(nm, "i64")) { return true; }; if (streq(nm, "u8")) { return true; }; if (streq(nm, "u16")) { return true; }; if (streq(nm, "u32")) { return true; }; if (streq(nm, "u64")) { return true; }; if (streq(nm, "int")) { return true; }; if (streq(nm, "uint")) { return true; }; if (streq(nm, "uintptr")) { return true; }; if (streq(nm, "size")) { return true; }; return false; }; // letfloatprim — float type-name keywords. f32 → 4B slot, f64 → 8B. // Returns the slot size or 0 if not a float type. fn letfloatprim(nm: str) i32 = { if (streq(nm, "f32")) { return 4; }; if (streq(nm, "f64")) { return 8; }; return 0; }; // letemitsize — slot size in bytes for a top-level `let`, or 0 if // the type isn't yet supported as a writable global. Walks type // aliases so byte output matches C cgen, which resolves Type kinds. // 4 → f32 (literal init supported) // 8 → scalar or f64 (literal init supported) // 16 → str (only zero-init / nil / "" supported) // 24 → slice (only zero-init supported) // varies → struct (zero-init only; field reads/scalar-field writes) fn letemitsize(c: *cgen, d: *node) i32 = { if (d == nil) { return 0; }; let t: *node = d.lhs; for (t != nil) { if (t.kind == nkind.N_TPTR) { return 8; }; if (t.kind == nkind.N_TSLICE) { return tyslicesize(): i32; }; if (t.kind == nkind.N_TARRAY) { let lenn: *node = t.rhs; let elemn: *node = t.lhs; let alen: i32 = 1; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i32; }; }; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let ps: i32 = primsize(elemn.str); if (ps > 0) { esz = ps; }; }; }; return alen * esz; }; if (t.kind != nkind.N_TNAME) { return 0; }; let nm: str = t.str; if (letscalarprim(nm)) { return 8; }; let fsz: i32 = letfloatprim(nm); if (fsz > 0) { return fsz; }; if (streq(nm, "str")) { return primtypesize("str"): i32; }; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si.totsize; }; let next: *node = aliaslookup(c, nm); if (next == nil) { return 0; }; t = next; }; return 0; }; fn collectlets(c: *cgen, file: *node) void = { c.lets = nil; if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { if (letemitsize(c, d) > 0) { let lv: *letvar = alloc(letvar{name=nm, tnode=d.lhs, lvnext=c.lets})!; c.lets = lv; }; }; }; d = d.next; }; }; fn isletvar(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { return true; }; lv = lv.lvnext; }; return false; }; // letvarisstr — is the named top-level let a str global? Resolves // aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/ // cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare // MOVQ scalar load. // letvartnode — direct lookup of a top-level let's tnode. Used by // cgindex / cgassign to detect global `[N]T` arrays and `*T` // pointers, where the addressing path needs LEAQ name(SB) (array) // or MOVQ name(SB) (pointer) and the element size from T. fn letvartnode(c: *cgen, name: str) *node = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { return lv.tnode; }; lv = lv.lvnext; }; return nil; }; fn letvarisstr(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return false; }; let nm: str = t.str; if (streq(nm, "str")) { return true; }; let nx: *node = aliaslookup(c, nm); if (nx == nil) { return false; }; t = nx; }; return false; }; lv = lv.lvnext; }; return false; }; // letvarisslice — is the named top-level let a slice global? // Slice headers are 24 bytes; the ABI flows as (AX, BX, CX) so the // load sequence ends with `MOVQ 16(CX), CX` (overwrites the // address holder with the cap). Mirrors C cgen's `let_isslice`. fn letvarisslice(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; if (t == nil) { return false; }; if (t.kind == nkind.N_TSLICE) { return true; }; return false; }; lv = lv.lvnext; }; return false; }; // letvarisfloat — slot size for a named float global, or 0 if not // a float-typed let. Walks aliases so the byte-identity contract // matches C cgen's `let_isfloat` (which resolves Type kinds). fn letvarisfloat(c: *cgen, name: str) i32 = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return 0; }; let fsz: i32 = letfloatprim(t.str); if (fsz > 0) { return fsz; }; let nx: *node = aliaslookup(c, t.str); if (nx == nil) { return 0; }; t = nx; }; return 0; }; lv = lv.lvnext; }; return 0; }; // letvarisstruct — is the named top-level let a struct global? // Struct globals use LEAQ name(SB), CX as the field-access base; the // cgdot read and cgassign write paths branch on this to skip the // frame-relative addressing they use for locals. fn letvarisstruct(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return false; }; let nm: str = t.str; if (structlookup(c, nm) != nil) { return true; }; let nx: *node = aliaslookup(c, nm); if (nx == nil) { return false; }; t = nx; }; return false; }; lv = lv.lvnext; }; return false; }; // letvarstructinfo — for a struct global, return its structinfo // so the cgdot/cgassign paths can look up fields. nil if the let // isn't a struct (or wasn't found). fn letvarstructinfo(c: *cgen, name: str) *structinfo = { let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, name)) { let t: *node = lv.tnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return nil; }; let nm: str = t.str; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si; }; let nx: *node = aliaslookup(c, nm); if (nx == nil) { return nil; }; t = nx; }; return nil; }; lv = lv.lvnext; }; return nil; }; // defvarstructinfo — sister of letvarstructinfo for top-level struct // `def`s. #129 A.2 adds DATA storage for struct-typed defs; the // LOAD-side cgdot direct-struct-global branch needs to resolve the // def's structinfo the same way it resolves a let's, so the field- // offset arithmetic + LEAQ name(SB) routing fires. Walks c.defs and // the type-spec node (defent.dtnode), aliaslookup-chasing TY_NAMED // through to the underlying struct name. Returns nil for non-struct // defs (int/float/str — those use the existing emitsymname-based // paths). fn defvarstructinfo(c: *cgen, name: str) *structinfo = { let e: *defent = c.defs; for (e != nil) { if (streq(e.dname, name)) { let t: *node = e.dtnode; for (t != nil) { if (t.kind != nkind.N_TNAME) { return nil; }; let nm: str = t.str; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si; }; let nx: *node = aliaslookup(c, nm); if (nx == nil) { return nil; }; t = nx; }; return nil; }; e = e.dnext; }; return nil; }; // defvartnode — sister of letvartnode for top-level `def`s. Returns // the type-spec node (defent.dtnode) for the named def, or nil. #129 // A.3 uses it in cgindex's array-base resolution so a `def: [N]T` // resolves through the same N_TARRAY-detect → LEAQ name(SB) shape as // a let array. Parallel to defvarstructinfo (#129 A.2) at the LOAD // side widening. fn defvartnode(c: *cgen, name: str) *node = { let e: *defent = c.defs; for (e != nil) { if (streq(e.dname, name)) { return e.dtnode; }; e = e.dnext; }; return nil; }; // emitdatawbyte — write one byte of an asm string literal using // the same escape rules as emitdefconstants / emitdatasection. fn emitdatawbyte(b: u8) void = { if (b == 34u8) { emitline("\\\""); return; }; if (b == 92u8) { emitline("\\\\"); return; }; if (b < 32u8) { emitline("\\x"); let hi: u8 = b >> 4u8; let lo: u8 = b & 15u8; let bb: [2]u8; if (hi < 10u8) { bb[0] = hi + 48u8; } else { bb[0] = (hi - 10u8) + 97u8; }; if (lo < 10u8) { bb[1] = lo + 48u8; } else { bb[1] = (lo - 10u8) + 97u8; }; emitbytes( bb.ptr, 2u64); return; }; if (b >= 127u8) { emitline("\\x"); let hi: u8 = b >> 4u8; let lo: u8 = b & 15u8; let bb: [2]u8; if (hi < 10u8) { bb[0] = hi + 48u8; } else { bb[0] = (hi - 10u8) + 97u8; }; if (lo < 10u8) { bb[1] = lo + 48u8; } else { bb[1] = (lo - 10u8) + 97u8; }; emitbytes( bb.ptr, 2u64); return; }; let bb: [1]u8; bb[0] = b; emitbytes( bb.ptr, 1u64); }; // letpreintern — intern strlits referenced from top-level str-let // initialisers BEFORE emitdatasection runs. Mirrors cmd/w6c/cgen.c // let_pre_intern: emitletdataw later looks up the same label, and // emitdatasection emits the DATA row in the same .s file. Running // emitletdataw after emitdatasection would flip the (DATA strlits, // DATAW lets) section order and break byte-identity. export fn letpreintern(c: *cgen, file: *node) void = { if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let sz: i32 = letemitsize(c, d); // #43: route the str-let gate through primtypesize so // #1 doesn't desync this with emitletdataw's matching // `sz == primtypesize("str"): i32` strlit-init branch. if (sz == primtypesize("str"): i32) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; if (r != nil) { if (r.kind == nkind.N_STRLIT) { if (r.str.len > 0) { internstrlit(c, r.str); }; }; }; }; }; d = d.next; }; }; // emitletdataw — DATAW directive per top-level `let` global. // 8B scalar with int/rune/bool/nil literal init (or no init). // 16B str — no init / `nil` / `""` → 16 zero bytes; or non-empty // strlit init → 8 zero placeholder + 8 LE len bytes plus a // DATAR slot+0,strlit reloc that the linker patches at load. // sz struct — zero only. // Non-literal scalar inits and unsupported shapes are skipped so the // link surfaces an undefined-symbol error if the binding is used. // Emit a (DATA|DATAW) row for a float-typed top-level let/def with a // FLOATLIT rhs (optionally wrapped in N_CAST or N_UN(±,...)). Shared // SSoT for emitletdataw float arm + emitdefconstants float arm (#129 // Phase A.1, rule-12 sea-of-stars). The N_UN(MINUS/PLUS) peel mirrors // foldintliteral's MINUS/TILDE/PLUS peel (#24); the float arm had // never been given the same treatment so `let g: f64 = -1.5;` // silently fell through to no-emit + undef-ref at link. Negation is // an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling // f64/f32 bitcast helpers into cgen. Returns true on emit, false if // rhs doesn't reduce to a foldable float literal. fn emitfloatlitdata(c: *cgen, directive: str, name: str, sz: i32, rhs: *node) bool = { let isf32: bool = (sz == 4); let bits: u64 = 0u64; let neg: bool = false; if (rhs != nil) { let r: *node = rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; if (r != nil) { if (r.kind == nkind.N_UN) { if (r.op == tkind.TK_MINUS) { neg = true; r = r.lhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; } else { if (r.op == tkind.TK_PLUS) { r = r.lhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; };}; }; }; if (r == nil) { return false; }; if (r.kind != nkind.N_FLOATLIT) { return false; }; // r.uval holds f64 bits regardless of literal suffix (lexer // stores the pre-narrow bits). f32 needs an explicit // (double→float) narrowing at emit time — mirrors cstage's // `union { float f; u32 u; } x; x.f = (float)r->fval` // (cgen.c:8436). Pre-#129 wwstage truncated the low 4 bytes // of the f64 bits, which silently emitted 0 for f32 lits; // the bug never bit because no current consumer has a f32 // let-init (surfaced by the consolidation gate). bits = r.uval; if (isf32) { let dv: f64 = *((&bits): *f64); let fv: f32 = (dv: f32); let uv: u32 = *((&fv): *u32); bits = uv: u64; }; }; emitline(directive); emitline(" "); emitsymname(c, name); emitline("(SB),\""); // IEEE-754 sign-bit XOR for negation happens INSIDE the emit // loop on the top byte only — equivalent to a whole-u64 XOR with // 2^63 but never materialises that constant. Avoids strconv's // i64tos-on-i64-MIN bug (#144) and any future cstage const-fold // of `1 << 63` back to the i64-MIN immediate, either of which // would break cs==ww byte-id on the cgen.ww self-rebuild (995). let i: i32 = 0; let nb: u64 = bits; for (i < sz) { let b: u8 = (nb & 255u64): u8; if (neg) { if (i == sz - 1) { b = b ^ 128u8; }; }; emitdatawbyte(b); nb = nb >> 8u64; i += 1; }; emitline("\"\n"); return true; }; // emitstructlitbytes — payload of a struct-typed top-level let/def // with N_STRUCTLIT rhs. Walks structt.fields, zero-fills padding via // the per-field offset (rule 13), dispatches per field type: // foldintliteral for int/bool/nil, inline bitcast+sign-XOR for float, // recursive call for nested struct. Other field kinds (str / slice / // ptr-with-address / array) are out of #129 A.2 scope — rule-7 aborts // loud rather than silently emitting wrong bytes. Mirror of cstage // emit_struct_lit_bytes. `base` offsets the field-start computation // so the recursive call walks an inner struct's fields within its // outer parent's byte stream. fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node, base: u64) bool = { let su: *tinfo = structt; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; if (su == nil) { return false; }; if (su.kind != tykind.TY_STRUCT) { return false; }; let pos: u64 = base; let f: *tfield = su.fields; for (f != nil) { let fstart: u64 = base + f.offset; for (pos < fstart) { emitdatawbyte(0u8); pos = pos + 1u64; }; let v: *node = nil; if (rhs != nil) { let fnod: *node = rhs.list; for (fnod != nil) { if (streq(fnod.str, f.name)) { v = fnod.lhs; break; }; fnod = fnod.next; }; }; let fsz: i32 = f.type_.size: i32; if (v == nil) { let i: i32 = 0; for (i < fsz) { emitdatawbyte(0u8); i = i + 1; }; pos = fstart + fsz: u64; f = f.tnext; continue; }; let vr: *node = v; for (vr != nil && vr.kind == nkind.N_CAST) { vr = vr.lhs; }; let fu: *tinfo = f.type_; for (fu != nil && fu.kind == tykind.TY_NAMED) { fu = fu.under; }; if (fu != nil && fu.kind == tykind.TY_STRUCT) { if (vr == nil) { let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (vr.kind != nkind.N_STRUCTLIT) { let m: str = "emitstructlitbytes: nested struct rhs not N_STRUCTLIT (#129 A.2)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; emitstructlitbytes(c, f.type_, vr, fstart); pos = fstart + fsz: u64; f = f.tnext; continue; }; // #129 A.3: array-typed field with N_ARRLIT rhs (the shape // parked in A.2). Recurses through emitarraylitbytes for // element-kind dispatch. Rule-7 stops loudly if rhs shape // doesn't match. if (fu != nil && fu.kind == tykind.TY_ARRAY) { if (vr == nil) { let m: str = "emitstructlitbytes: array field rhs nil (#129 A.3)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (vr.kind != nkind.N_ARRLIT) { let m: str = "emitstructlitbytes: array field rhs not N_ARRLIT (#129 A.3)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (!emitarraylitbytes(c, f.type_, vr, 1)) { let m: str = "emitstructlitbytes: array field rhs has non-reducible elements (#129 A.3)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; pos = fstart + fsz: u64; f = f.tnext; continue; }; if (typeisfloat(f.type_)) { let isf32: bool = (fsz == 4); let neg: bool = false; let fr: *node = vr; if (fr != nil) { if (fr.kind == nkind.N_UN) { if (fr.op == tkind.TK_MINUS) { neg = true; fr = fr.lhs; for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; }; } else { if (fr.op == tkind.TK_PLUS) { fr = fr.lhs; for (fr != nil && fr.kind == nkind.N_CAST) { fr = fr.lhs; }; };}; };}; if (fr == nil) { let m: str = "emitstructlitbytes: float field rhs nil (#129 A.2)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (fr.kind != nkind.N_FLOATLIT) { let m: str = "emitstructlitbytes: float field rhs not FLOATLIT (#129 A.2)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; let bits: u64 = fr.uval; if (isf32) { let dv: f64 = *((&bits): *f64); let fv: f32 = (dv: f32); let uv: u32 = *((&fv): *u32); bits = uv: u64; }; let i: i32 = 0; let nb: u64 = bits; for (i < fsz) { let b: u8 = (nb & 255u64): u8; if (neg) { if (i == fsz - 1) { b = b ^ 128u8; }; }; emitdatawbyte(b); nb = nb >> 8u64; i = i + 1; }; pos = fstart + fsz: u64; f = f.tnext; continue; }; let iv: u64 = 0u64; if (!foldintliteral(vr, &iv)) { let m: str = "emitstructlitbytes: field rhs not foldable (str/slice/ptr/array out of #129 A.2 scope)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; let i: i32 = 0; let nb: u64 = iv; for (i < fsz) { emitdatawbyte((nb & 255u64): u8); nb = nb >> 8u64; i = i + 1; }; pos = fstart + fsz: u64; f = f.tnext; }; let endpos: u64 = base + structt.size; for (pos < endpos) { emitdatawbyte(0u8); pos = pos + 1u64; }; return true; }; // emitstructdata — top-level wrapper. Opens the DATA/DATAW directive // then delegates to emitstructlitbytes. Shared between emitletdataw // struct arm and emitdefconstants struct arm (#129 A.2). fn emitstructdata(c: *cgen, directive: str, name: str, structt: *tinfo, rhs: *node) bool = { let su: *tinfo = structt; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; if (su == nil) { return false; }; if (su.kind != tykind.TY_STRUCT) { return false; }; emitline(directive); emitline(" "); emitsymname(c, name); emitline("(SB),\""); emitstructlitbytes(c, structt, rhs, 0u64); emitline("\"\n"); return true; }; // emitarraylitbytes — emit alen * esz bytes for an [N]T top-level let/ // def with N_ARRLIT rhs. Mirrors cstage emit_array_lit_bytes. Per- // element dispatch: // - int (covers bool/rune/typed-int/N_UN-int): foldintliteral per // element. Existing pre-#129-A.3 emitletdataw array arm logic // preserved byte-for-byte so bootstrap consumers (lib/os, lib/ // bufio, lib/strings, lib/encoding/utf8, lib/strconv/stof_data) // don't shift. // - float (f32/f64): peel N_CAST/N_UN(±), bitcast magnitude via // pointer-cast round-trip (mirror emitfloatlitdata), sign-XOR // top byte of each element inline. No 2^63 immediate. // - struct: per element call emitstructlitbytes (#129 A.2 helper). // - other element kinds (ptr/nested-array): returns false — caller // falls through to zero-init. // // Two-pass validate-then-emit (`emit_phase=0` validate-only, `=1` // actually emit) keeps emit-on-failure from emitting partial bytes // into an open DATA literal. fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node, emit_phase: i32) bool = { let au: *tinfo = arrt; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; if (au == nil) { return false; }; if (au.kind != tykind.TY_ARRAY) { return false; }; let esz: i32 = au.sub.size: i32; let alen: i32 = au.alen: i32; let eu: *tinfo = au.sub; for (eu != nil && eu.kind == tykind.TY_NAMED) { eu = eu.under; }; if (eu != nil && eu.kind == tykind.TY_STRUCT) { // Validate: every element must be N_STRUCTLIT (after N_CAST). let idx: i32 = 0; let last_ev: *node = nil; let e: *node = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (ev.kind != nkind.N_STRUCTLIT) { return false; }; last_ev = ev; idx += 1; e = e.next; }; if (emit_phase == 0) { return true; }; idx = 0; let repeat: bool = false; e = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; emitstructlitbytes(c, au.sub, ev, 0u64); idx += 1; e = e.next; }; for (idx < alen) { if (repeat && last_ev != nil) { emitstructlitbytes(c, au.sub, last_ev, 0u64); } else { let bb: i32 = 0; for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; }; idx += 1; }; return true; }; // #129 A.3 capstone (PREREQ-1, #156): nested-array element [M]T // inside [N][M]T. Mirror of the TY_STRUCT-element arm above and of // the TY_ARRAY-field-in-struct arm in emitstructlitbytes — recurse // into emitarraylitbytes per element; recursion bottoms out at // scalar (int/float) elements. esz = au.sub.size gives the per- // element stride (rule 13). The `...` repeat marker with nested- // array elements is rejected loud (rule 7): no consumer needs it // (powers_of_ten is fully enumerated). if (eu != nil && eu.kind == tykind.TY_ARRAY) { let idx: i32 = 0; let e: *node = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { let m: str = "emitarraylitbytes: '...' repeat with nested-array elements unsupported (#129 A.3, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (ev.kind != nkind.N_ARRLIT) { return false; }; if (!emitarraylitbytes(c, au.sub, ev, 0)) { return false; }; idx += 1; e = e.next; }; if (emit_phase == 0) { return true; }; idx = 0; e = rhs.list; for (e != nil && idx < alen) { let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; emitarraylitbytes(c, au.sub, ev, 1); idx += 1; e = e.next; }; for (idx < alen) { let bb: i32 = 0; for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; idx += 1; }; return true; }; if (typeisfloat(au.sub)) { let isf32: bool = typeisf32(au.sub); // Validate. let idx: i32 = 0; let e: *node = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev != nil) { if (ev.kind == nkind.N_UN) { if (ev.op == tkind.TK_MINUS) { ev = ev.lhs; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; } else { if (ev.op == tkind.TK_PLUS) { ev = ev.lhs; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; };}; };}; if (ev == nil) { return false; }; if (ev.kind != nkind.N_FLOATLIT) { return false; }; idx += 1; e = e.next; }; if (emit_phase == 0) { return true; }; idx = 0; let last_bits: u64 = 0u64; let last_neg: bool = false; let repeat: bool = false; e = rhs.list; for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; let neg: bool = false; if (ev != nil) { if (ev.kind == nkind.N_UN) { if (ev.op == tkind.TK_MINUS) { neg = true; ev = ev.lhs; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; } else { if (ev.op == tkind.TK_PLUS) { ev = ev.lhs; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; };}; };}; let bits: u64 = ev.uval; if (isf32) { let dv: f64 = *((&bits): *f64); let fv: f32 = (dv: f32); let uv: u32 = *((&fv): *u32); bits = uv: u64; }; let bb: i32 = 0; let nb: u64 = bits; for (bb < esz) { let byt: u8 = (nb & 255u64): u8; if (neg) { if (bb == esz - 1) { byt = byt ^ 128u8; }; }; emitdatawbyte(byt); nb = nb >> 8u64; bb += 1; }; last_bits = bits; last_neg = neg; idx += 1; e = e.next; }; for (idx < alen) { if (repeat) { let bb: i32 = 0; let nb: u64 = last_bits; for (bb < esz) { let byt: u8 = (nb & 255u64): u8; if (last_neg) { if (bb == esz - 1) { byt = byt ^ 128u8; }; }; emitdatawbyte(byt); nb = nb >> 8u64; bb += 1; }; } else { let bb: i32 = 0; for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; }; idx += 1; }; return true; }; // Int-element path — preserved byte-for-byte from the pre-A.3 // emitletdataw in-place arm so bootstrap consumers (u8/i8/u16 // arrays) don't shift. let idx: i32 = 0; let e: *node = rhs.list; let last: u64 = 0u64; let repeat: bool = false; // Validate first. for (e != nil && idx < alen) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; break; }; }; let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (!foldintliteral(ev, &last)) { return false; }; idx += 1; e = e.next; }; if (emit_phase == 0) { return true; }; idx = 0; last = 0u64; repeat = false; e = rhs.list; let inrepeat: bool = false; for (idx < alen) { let v: u64 = last; if (!inrepeat && e != nil) { if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { inrepeat = true; } else { e = e.next; }; } else { let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (!foldintliteral(ev, &v)) { v = 0u64; }; last = v; e = e.next; }; }; let nb: u64 = v; let bb: i32 = 0; for (bb < esz) { emitdatawbyte((nb & 255u64): u8); nb = nb >> 8u64; bb += 1; }; idx += 1; }; return true; }; // emitarraydata — top-level wrapper. Two-pass validate-then-emit // avoids partial-byte corruption if the rhs shape can't reduce. // nil rhs is the "no-rhs zero-init" shape (e.g. `let buf: [N]u8;` // in lib/strconv/strconv.ww:287, lib/os/os.ww:92, etc.) — emit // alen*esz zero bytes. This was the implicit pre-A.3 emitletdataw // behavior (the old loop emitted zeros when `elems` was nil); the // refactor would have skipped emit entirely without this branch, // causing `undefined reference to strconv.f64tos_buf` at link. fn emitarraydata(c: *cgen, directive: str, name: str, arrt: *tinfo, rhs: *node) bool = { let au: *tinfo = arrt; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; if (au == nil) { return false; }; if (au.kind != tykind.TY_ARRAY) { return false; }; if (rhs == nil) { let total: u64 = arrt.size; emitline(directive); emitline(" "); emitsymname(c, name); emitline("(SB),\""); let i: u64 = 0u64; for (i < total) { emitdatawbyte(0u8); i = i + 1u64; }; emitline("\"\n"); return true; }; if (!emitarraylitbytes(c, arrt, rhs, 0)) { return false; }; emitline(directive); emitline(" "); emitsymname(c, name); emitline("(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); return true; }; fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { let sz: i32 = letemitsize(c, d); let issg: bool = letvarisstruct(c, nm); let fsz: i32 = letvarisfloat(c, nm); if (fsz > 0) { // Float global: routes through the // emitfloatlitdata SSoT helper, shared // with emitdefconstants's float arm // (#129 Phase A.1, rule-12). Bare-call // discards the bool return (mirrors // cgen.ww:723 fmt.fprintln pattern). emitfloatlitdata(c, "DATAW", nm, fsz, d.rhs); }; // #129 A.2: struct-typed let with N_STRUCTLIT rhs // routes through the emitstructdata SSoT helper. // Pre-A.2 emitletdataw had no struct arm, so the // declaration fell out of the .data section and // the link surfaced an undefined-symbol error. if (issg) { let r: *node = d.rhs; if (r != nil) { if (r.kind == nkind.N_STRUCTLIT) { let st: *tinfo = d.lhs.type_: *tinfo; emitstructdata(c, "DATAW", nm, st, r); }; }; }; // Skip the scalar 8B path when the global is a // fixed-size array that just happens to sum to 8 // bytes (e.g. [4]u16, [8]u8) — the array path // below handles it and the duplicate DATAW would // otherwise differ across stages on user code. let isarr8: bool = false; if (d.lhs != nil) { if (d.lhs.kind == nkind.N_TARRAY) { isarr8 = true; }; }; if (sz == 8 && !issg && fsz == 0 && !isarr8) { let v: u64 = 0u64; let ok: bool = true; if (d.rhs != nil) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; // Same helper as emitdefconstants (#24) // — widens the gate so N_UN over an // int leaf folds. `let x: i8 = -1i8;` // arrives as N_UN(TK_MINUS, N_INTLIT) // after the typed-AST cast peel. ok = foldintliteral(r, &v); }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; for (i < 8) { let b: u8 = (n & 255u64): u8; n = n >> 8u64; emitdatawbyte(b); i += 1; }; emitline("\"\n"); }; }; if (sz == primtypesize("str"): i32 && !issg) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; // str-literal init (non-empty): emit // the 16B payload as 8 placeholder zero // bytes + 8 LE bytes of length, then a // DATAR reloc to patch the ptr half with // the strlit's runtime VA. let strlitinit: bool = false; if (r != nil) { if (r.kind == nkind.N_STRLIT) { if (r.str.len > 0) { strlitinit = true; }; }; }; if (strlitinit) { let lab: str = internstrlit(c, r.str); let v: u64 = r.str.len: u64; emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; i = 0; let nv: u64 = v; for (i < 8) { emitdatawbyte((nv & 255u64): u8); nv = nv >> 8u64; i += 1; }; emitline("\"\n"); emitline("DATAR "); emitsymname(c, nm); emitline("+0(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); } else { // zero-init: accept no rhs, nil, // or empty strlit. let ok: bool = true; if (d.rhs != nil) { ok = false; if (r != nil) { if (r.kind == nkind.N_NIL) { ok = true; }; if (r.kind == nkind.N_STRLIT) { if (r.str.len == 0) { ok = true; }; }; }; }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; let szstr: i32 = primtypesize("str"): i32; for (i < szstr) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); }; }; }; if (sz == tyslicesize(): i32 && !issg) { // Slice: zero-init only (no slice-literal // syntax to honour). Any rhs other than // `nil` is skipped → undefined symbol at // link. let ok: bool = true; if (d.rhs != nil) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; ok = false; if (r != nil) { if (r.kind == nkind.N_NIL) { ok = true; }; }; }; if (ok) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; let szsl: i32 = tyslicesize(): i32; for (i < szsl) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); }; }; // Struct globals — any size, zero-init only. // A struct literal init isn't compile-time // evaluated yet; skip and the link will surface // an undefined-symbol error if referenced. if (issg) { if (d.rhs == nil) { emitline("DATAW "); emitsymname(c, nm); emitline("(SB),\""); let i: i32 = 0; for (i < sz) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); }; }; // #129 A.3: array global routes through the // emitarraydata SSoT helper. Int-elem path is // byte-for-byte preserved (bootstrap consumers in // lib/os, lib/bufio, lib/strings, lib/encoding/ // utf8, lib/strconv/stof_data don't shift). Float/ // struct elements gain emit via element-kind // dispatch. Helper validates pre-emit so partial // fold-failures don't corrupt the DATA literal. // No-rhs arrays (e.g. `let buf: [N]u8;`) go through // the same helper with rhs=nil → zero-fill branch. if (d.lhs != nil) { if (d.lhs.kind == nkind.N_TARRAY) { let rh: *node = d.rhs; let route: bool = false; if (rh == nil) { route = true; }; if (rh != nil) { if (rh.kind == nkind.N_ARRLIT) { route = true; }; }; if (route) { let at: *tinfo = d.lhs.type_: *tinfo; emitarraydata(c, "DATAW", nm, at, rh); }; }; }; }; }; d = d.next; }; }; // emitdefconstants — DATA directive per top-level fold-to-literal // `def`. 8 bytes little-endian to match what the C cgen emits. // foldintliteral gates: int/rune literal, true/false/nil, and a // unary +/-/~ over the same. `def NEG: i32 = -100;` arrives as // N_UN(TK_MINUS, N_INTLIT) — the unary peel is exactly what the // gate is for. fn emitdefconstants(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_DEF) { let r: *node = d.rhs; let v: u64 = 0u64; let ok: bool = false; if (r != nil) { ok = foldintliteral(r, &v); }; if (!ok) { // Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) // rhs: route through the same SSoT helper as // emitletdataw's float arm. Pre-#129 this fell // through to no-emit + undef-ref at link. Type-size // walk mirrors letvarisfloat (#129 Phase A.1). let dfsz: i32 = 0; let dt: *node = d.lhs; for (dt != nil) { if (dt.kind != nkind.N_TNAME) { dfsz = 0; break; }; let fsz: i32 = letfloatprim(dt.str); if (fsz > 0) { dfsz = fsz; break; }; let nx: *node = aliaslookup(c, dt.str); if (nx == nil) { dfsz = 0; break; }; dt = nx; }; if (dfsz > 0) { emitfloatlitdata(c, "DATA", d.str, dfsz, d.rhs); } else { // #129 A.2: struct-typed def with N_STRUCTLIT // rhs. The checker stamps d.lhs.type_ with the // struct's tinfo; helper peels TY_NAMED. Parallel // to emitletdataw struct arm; uses DATA (read- // only) directive. if (r != nil) { if (r.kind == nkind.N_STRUCTLIT) { let st: *tinfo = d.lhs.type_: *tinfo; let su: *tinfo = st; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; if (su != nil) { if (su.kind == tykind.TY_STRUCT) { emitstructdata(c, "DATA", d.str, st, r); }; }; };}; // #129 A.3: array-typed def with N_ARRLIT rhs. // Parallel to emitletdataw array arm; uses DATA. if (r != nil) { if (r.kind == nkind.N_ARRLIT) { let at: *tinfo = d.lhs.type_: *tinfo; let au: *tinfo = at; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; if (au != nil) { if (au.kind == tykind.TY_ARRAY) { emitarraydata(c, "DATA", d.str, at, r); }; }; };}; }; }; if (ok) { // #127: route DATA-emit through the SAME emitsymname // SSoT that LOAD/CALL sites use. Replaces the prior // 8-line d.exported/d.nmod prefix logic with a single // modlookup-based mangle, removing duplicate logic // (rule-12 sea-of-stars). Mirrors cstage emit_defs at // cmd/w6c/cgen.c:8494 (mod_mangle). Bootstrap-neutral // post-90d31c5 (the PATH_MAX duplicate-def consumer // that motivated the divergence is gone), so the asm // surface is unchanged on the corpus. emitline("DATA "); emitsymname(c, d.str); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; for (i < 8) { let b: u8 = (n & 255u64): u8; n = n >> 8u64; // C emit_defs only special-cases " and \; // every other non-printable goes as \xHH. if (b == 34u8) { emitline("\\\""); } else { if (b == 92u8) { emitline("\\\\"); } else { if (b < 32u8) { emitline("\\x"); let hi: u8 = b >> 4u8; let lo: u8 = b & 15u8; let bb: [2]u8; if (hi < 10u8) { bb[0] = hi + 48u8; } else { bb[0] = (hi - 10u8) + 97u8; }; if (lo < 10u8) { bb[1] = lo + 48u8; } else { bb[1] = (lo - 10u8) + 97u8; }; emitbytes( bb.ptr, 2u64); } else { if (b >= 127u8) { emitline("\\x"); let hi: u8 = b >> 4u8; let lo: u8 = b & 15u8; let bb: [2]u8; if (hi < 10u8) { bb[0] = hi + 48u8; } else { bb[0] = (hi - 10u8) + 97u8; }; if (lo < 10u8) { bb[1] = lo + 48u8; } else { bb[1] = (lo - 10u8) + 97u8; }; emitbytes( bb.ptr, 2u64); } else { let bb: [1]u8; bb[0] = b; emitbytes( bb.ptr, 1u64); }; }; };}; i += 1; }; emitline("\"\n"); }; }; d = d.next; }; }; // emitdatasection — DATA directives for every interned strlit. // Trailing NUL appended so .ptr can be used as a C string by syscalls. fn emitdatasection(c: *cgen) void = { let s: *strlit = c.strlits; for (s != nil) { emitline("DATA "); let lab: str = s.label; emitbytes( lab.ptr, lab.len: u64); emitline("(SB),\""); let bs: str = s.bytes; let i: i32 = 0; for (i < bs.len) { let b: u8 = bs[i]; if (b == 34u8) { emitline("\\\""); } // " else { if (b == 92u8) { emitline("\\\\"); } // \ else { if (b == 10u8) { emitline("\\n"); } else { if (b == 9u8) { emitline("\\t"); } else { if (b == 13u8) { emitline("\\r"); } else { if (b < 32u8) { emitline("\\x"); let hi: u8 = b >> 4u8; let lo: u8 = b & 15u8; let bb: [2]u8; if (hi < 10u8) { bb[0] = hi + 48u8; } else { bb[0] = (hi - 10u8) + 97u8; }; if (lo < 10u8) { bb[1] = lo + 48u8; } else { bb[1] = (lo - 10u8) + 97u8; }; emitbytes( bb.ptr, 2u64); } else { if (b >= 127u8) { emitline("\\x"); let hi: u8 = b >> 4u8; let lo: u8 = b & 15u8; let bb: [2]u8; if (hi < 10u8) { bb[0] = hi + 48u8; } else { bb[0] = (hi - 10u8) + 97u8; }; if (lo < 10u8) { bb[1] = lo + 48u8; } else { bb[1] = (lo - 10u8) + 97u8; }; emitbytes( bb.ptr, 2u64); } else { let bb: [1]u8; bb[0] = b; emitbytes( bb.ptr, 1u64); }; }; };};};};}; i += 1; }; emitline("\\x00\"\n"); s = s.slnext; }; }; // ---- fn return-type map --------------------------------------------- // // Per-file: ident → ret-type-node. Used to decide whether to shuffle // (AX, DX) → (AX, BX) after a CALL — needed for str-returning fns so // the value flows through cgen as the canonical (AX, BX) str pair. type fnret = struct { fname: str, fmod: str, rtype: *node, params: *node, frnext: *fnret, }; fn collectfnrets(c: *cgen, file: *node) void = { c.fnrets = nil; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { let f: *fnret = alloc(fnret{fname=d.str, fmod=d.nmod, rtype=d.lhs, params=d.list, frnext=c.fnrets})!; c.fnrets = f; }; d = d.next; }; }; // fnretlookup — declared return-type node for a fn by leaf name, or nil // if the name isn't a registered fn. Same-module-first walk before the // head-walk fallback. Eighth and final leaf of the trio graduation (#4e) // mirroring aliaslookup (#27), fnret/fnparamslookupmod (#28/#31), // enum/struct/deflookup (#4a/#4b/#4c), fnparamslookup (#4d): without // the prefer pass a bare-leaf `foo()` call site in module M (N_IDENT // callee) silently picks another module's same-leaf `foo` from the // head of c.fnrets, then every downstream consumer keying on the // return type (str-pair shuffle, tagged-union ABI, tuple destructure, // float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires // against the wrong-module shape. // fnretlookup — the called fn's declared return type, keyed by NAME // (same-module-first, then first leaf match). The receive sites that // re-derive a call's result SHAPE from this (cglet tagged-store, // cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned // arms) are correct only when the leaf name uniquely picks the callee. // // #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a // VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive // sites keyed on the field leaf `f` with the receiver VARIABLE name as // the "module" (not a real module), so this lookup mis-binds a same-named // GLOBAL fn. When that global's register shape differs from the field's // (scalar global vs tagged field), the slot is stored with the wrong ABI // shape → cstage≠wwstage asm, silent miscompile. The sound fix derives // the result from the FIELD's fn type / the checker-stamped n.type_ (as // cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is // added here: same-shape leaf collisions resolve by name legitimately // today, and a discriminating guard would need the shape-compare that IS // the fix. Masked until #208 landed (the checker rejected the shape // before cgen ran). test/wcc/782 pins the cstage-correct runtime // (cstage-only) and graduates to STAGE_WW on #211 close. fn fnretlookup(c: *cgen, name: str) *node = { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { if (streq(f.fmod, c.curmod)) { return f.rtype; }; }; f = f.frnext; }; f = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { return f.rtype; }; f = f.frnext; }; return nil; }; // fnretlookupmod — same-module-first walk. Module-qualified `mod.fn(...)` // callees route here so a leaf collision (same fn name exported from // multiple modules) resolves to the explicit module. Falls back to the // first leaf match if no matching module is registered. Mirror of // fnparamslookupmod (#28); without this, matchscrutt's N_DOT branch // picks the last-declared `next` regardless of qualifier, so a 4-arm // `match (utf8.next(d))` inside a `fn next() (rune | done)` resolves // the scrutinee tagged type to `(rune | done)` — flatvariantidx then // can't see arms 2/3 and collapses them onto tag 0 (task #31). fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = { if (mod.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { if (streq(f.fmod, mod)) { return f.rtype; }; }; f = f.frnext; }; }; return fnretlookup(c, name); }; // fnparamslookup — head of the declared param-list for a fn, or nil // if the name isn't a registered fn. Same-module-first walk before the // head-walk fallback. Trio-leaf graduation (#4d) mirroring aliaslookup // (#27), fnret/fnparamslookupmod (#28/#31), enum/struct/deflookup // (#4a/#4b/#4c): without the prefer pass a bare-leaf `foo(x)` call in // module M (callee N_IDENT) silently picks another module's same-leaf // `foo` from the head of c.fnrets, then pushargsrev's widening // detection fires (or doesn't) against the wrong param-type — `foo(7)` // against a same-leaf `(i32 | void)` param re-layouts 7 into a 2-word // tagged slot vs the same-module `i32` param's single push. fn fnparamslookup(c: *cgen, name: str) *node = { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { if (streq(f.fmod, c.curmod)) { return f.params; }; }; f = f.frnext; }; f = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { return f.params; }; f = f.frnext; }; return nil; }; // samemodfn — true iff `name` is registered as a fn in c.curmod. Used // by cgcall to suppress the bare-name Hare-style builtins (`alloc(x)`, // future free/append/len audits) when the current module declares its // own decl by that name. Mirrors cstage's same-module check at // cmd/wcc/check.c (alloc gate, task #23) — `scope_lookup_prefer` over // the flat scope would also match `use os;`-imported decls in a primary, // suppressing the builtin spuriously; the same-module-tag filter here // (and `c.curmod && ...` on the cstage side) keeps the gate strict. fn samemodfn(c: *cgen, name: str) bool = { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { if (streq(f.fmod, c.curmod)) { return true; }; }; f = f.frnext; }; return false; }; // fnparamslookupmod — same-module-first leaf walk. Module-qualified // `mod.fn(...)` calls go through this so a leaf collision (multiple // modules export the same name, e.g. `os.read` and `io.read`) resolves // to the explicit module. Falls back to the first leaf match if no // matching module is registered — mirrors aliaslookup's two-pass shape // (cgen.ww:75, fixed in #27). fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = { if (mod.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (streq(f.fname, name)) { if (streq(f.fmod, mod)) { return f.params; }; }; f = f.frnext; }; }; return fnparamslookup(c, name); }; // ---- def-constant registry ------------------------------------------ // // `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an // ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at // file load and consult on nkind.N_IDENT lookup. type defent = struct { dname: str, dmod: str, // originating module (`// MODULE: foo`), or empty drhs: *node, dtnode: *node, // #129 A.2: type-spec node (d.lhs); needed for // struct-def structinfo lookup at the cgdot // LOAD-side widening site. dnext: *defent, }; fn collectdefs(c: *cgen, file: *node) void = { c.defs = nil; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_DEF) { let e: *defent = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dtnode=d.lhs, dnext=c.defs})!; c.defs = e; }; d = d.next; }; }; // Same-module-first walk, then any. Trio-leaf graduation mirroring // aliaslookup (#27) and enum/structlookup (#4a/#4b): bool answer is // invariant either way, but the structural shape mirrors deflookuprhs // where the entry's drhs IS module-sensitive. fn deflookup(c: *cgen, name: str) bool = { let e: *defent = c.defs; for (e != nil) { if (streq(e.dname, name)) { if (streq(e.dmod, c.curmod)) { return true; }; }; e = e.dnext; }; e = c.defs; for (e != nil) { if (streq(e.dname, name)) { return true; }; e = e.dnext; }; return false; }; // Returns the rhs init node for a top-level `def`, or nil if `name` // doesn't name a def. Same-module-first walk: without the prefer pass // `MSG.ptr`/`MSG.len` in module M can collapse onto another module's // same-leaf `def MSG: str = ...` sitting at the head of c.defs and // inline the wrong strlit. Used by cgdot to inline `.ptr`/`.len` on // `def NAME: str = "..."` — those aren't laid out in memory. fn deflookuprhs(c: *cgen, name: str) *node = { let e: *defent = c.defs; for (e != nil) { if (streq(e.dname, name)) { if (streq(e.dmod, c.curmod)) { return e.drhs; }; }; e = e.dnext; }; e = c.defs; for (e != nil) { if (streq(e.dname, name)) { return e.drhs; }; e = e.dnext; }; return nil; }; // deflookuprhsmod — same-module-first walk for `mod.NAME` references. // Trio-leaf *mod variant mirroring fnretlookupmod (#31) / fnparamslookupmod // (#28) / enumlookupmod (#4a). Module-qualified `alpha.MSG` from a third // module needs the explicit alpha hint; deflookuprhs prefers c.curmod // (which doesn't match either source module on a 3rd-module qualifier) // and falls back to head-pick, possibly inlining beta.MSG's strlit when // both alpha and beta declare same-leaf str defs. cgdot's mod-qualified // str-def value-load routes here so a cross-module N_DOT collision // resolves to the explicit module. Falls back to deflookuprhs's bare- // leaf two-pass when no module matches. fn deflookuprhsmod(c: *cgen, name: str, mod: str) *node = { if (mod.len > 0) { let e: *defent = c.defs; for (e != nil) { if (streq(e.dname, name)) { if (streq(e.dmod, mod)) { return e.drhs; }; }; e = e.dnext; }; }; return deflookuprhs(c, name); }; // #149: rhs peels (N_CAST / unary ±) to a float literal — the exact // shape emitfloatlitdata (cgen.ww) emits a DATA symbol for. The scalar- // float address-of gate must equal that emission set, or `&def` LEAQs a // symbol the data pass never wrote. Keep in sync with emitfloatlitdata's // peel. fn floatlitleaf(rhs: *node) bool = { let r: *node = rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; if (r != nil) { if (r.kind == nkind.N_UN) { if (r.op == tkind.TK_MINUS) { r = r.lhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; } else { if (r.op == tkind.TK_PLUS) { r = r.lhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; r = r.lhs; }; }; }; }; }; if (r == nil) { return false; }; return r.kind == nkind.N_FLOATLIT; }; // #149/#147: a top-level def is addressable for `&def` iff emitdefs emits // a DATA symbol for it — struct, array, scalar int (foldintliteral), or // scalar float whose rhs peels to a FLOATLIT. Gate held identical to // cstage def_is{struct,array,scalar}def so the addressable set matches // byte-for-byte (rule 10). str defs and computed-rhs floats (#147 // `def NAN = 0.0/0.0`) have no symbol and are excluded → routed to the // loud error, never a LEAQ of a missing symbol. `opnd` is the `&`-operand // N_IDENT; its checker-stamped type_ carries the def's type (same as the // cgident float-def read at cgenexpr.ww). fn defisaddressable(c: *cgen, opnd: *node) bool = { let nm: str = opnd.str; if (defvarstructinfo(c, nm) != nil) { return true; }; let dtn: *node = defvartnode(c, nm); if (dtn != nil) { if (dtn.kind == nkind.N_TARRAY) { return true; }; }; let drhs: *node = deflookuprhs(c, nm); if (drhs == nil) { return false; }; let v: u64 = 0u64; if (foldintliteral(drhs, &v)) { return true; }; if (isfloattype(c, opnd)) { if (floatlitleaf(drhs)) { return true; }; }; return false; }; // ---- module-private symbol map -------------------------------------- // // Every non-FFI top-level fn decl lives in its module's namespace — // cgen mangles the leaf to `.` at the def site (TEXT) // and at every call/load site, so cross-module same-leaf fns (lib/os // `read` vs lib/io `read`, both exported) coexist at link time. // Non-fn decls (let/def/type) stick to the older "non-exported only" // rule: their export-side namespace is the user-facing data ABI and // mangling them changes the surface. FFI-bound decls (@symbol) keep // their explicit C symbol regardless of kind. // // Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export` // for fns. Both stages must match exactly — ww2/ww3/ww4 byte-identity // depends on it. type modent = struct { mname: str, // the bare ident as it appears in source nmod: str, // the originating module (`// MODULE: foo`) mnext: *modent, }; fn collectmods(c: *cgen, file: *node) void = { c.mods = nil; if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { // Mirror collectfnrets' shape exactly (plain prepend in one // branch). Earlier nested-if/early-return variants tickled a // wwstage cgen bug that dropped most prepends. if (d.kind == nkind.N_FNDECL) { // Fns mangle regardless of export status — covers // lib/os.read vs lib/io.read collision. if (d.nmod.len > 0) { let isffi: bool = false; let a: *node = d.attr; for (a != nil) { if (a.kind == nkind.N_ATTR) { let an: str = a.str; if (streq(an, "symbol")) { isffi = true; }; }; a = a.next; }; if (!isffi) { if (!streq(d.str, "main")) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; }; }; if (d.kind == nkind.N_DEF) { if (d.exported == 0) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; }; if (d.kind == nkind.N_TYPEDECL) { if (d.exported == 0) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; }; if (d.kind == nkind.N_LET) { if (d.exported == 0) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, mnext=c.mods})!; c.mods = m; }; }; }; d = d.next; }; }; fn modlookup(c: *cgen, name: str) str = { let m: *modent = c.mods; for (m != nil) { if (streq(m.mname, name)) { return m.nmod; }; m = m.mnext; }; let empty: str; empty.ptr = nil; empty.len = 0; return empty; }; // modlookupforfn — hint-aware lookup for fn names. Walks c.mods // preferring entries where module matches `hint`; falls back to the // first leaf-name match when nothing matches the hint (legacy single- // owner shape, also covers lookups with hint.len==0). Needed because // multiple modules can now register the same fn leaf — bare `lookup` // would otherwise grab whichever module was prepended last. fn modlookupforfn(c: *cgen, name: str, hint: str) str = { let m: *modent = c.mods; let first: str; first.ptr = nil; first.len = 0; for (m != nil) { if (streq(m.mname, name)) { if (hint.len > 0 && m.nmod.len > 0 && streq(m.nmod, hint)) { return m.nmod; }; if (first.len == 0 && first.ptr == nil) { first = m.nmod; }; }; m = m.mnext; }; return first; }; // emitsymname — write the asm symbol name for `ident`. Honours, in // order: FFI mapping (@symbol), module mangling (private decls), bare // name. Use everywhere a top-level non-fn name is emitted before `(SB)` // — DATA labels for top-level lets/defs, address-of-let, etc. Fn names // (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates // cross-module same-leaf fn exports. fn emitsymname(c: *cgen, ident: str) void = { let resolved: str = ffiresolve(c, ident); if (resolved.ptr != ident.ptr) { // FFI hit — emit the mapped linker symbol verbatim. emitbytes( resolved.ptr, resolved.len: u64); return; }; let mod: str = modlookup(c, ident); if (mod.len > 0) { emitbytes( mod.ptr, mod.len: u64); emitbytes( ".".ptr, 1u64); }; emitbytes( ident.ptr, ident.len: u64); }; // emitfnname — write the asm symbol name for a fn `ident`, threading // `hint` (the explicit module from a `mod.fn` use site, or c.curmod // for bare-IDENT calls) through modlookupforfn. Same FFI override // semantics as emitsymname; same dot-separator format. Use at every // CALL / LEAQ-of-fn / TEXT-def site. fn emitfnname(c: *cgen, ident: str, hint: str) void = { let resolved: str = ffiresolve(c, ident); if (resolved.ptr != ident.ptr) { emitbytes( resolved.ptr, resolved.len: u64); return; }; let mod: str = modlookupforfn(c, ident, hint); if (mod.len > 0) { emitbytes( mod.ptr, mod.len: u64); emitbytes( ".".ptr, 1u64); }; emitbytes( ident.ptr, ident.len: u64); }; // ---- FFI map --------------------------------------------------------- fn fficollect(c: *cgen, file: *node) void = { c.ffis = nil; if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { let a: *node = d.attr; for (a != nil) { if (a.kind == nkind.N_ATTR) { let aname: str = a.str; if (streq(aname, "symbol")) { let symnode: *node = a.list; if (symnode != nil) { if (symnode.kind == nkind.N_STRLIT) { let f: *ffi = alloc(ffi{ident=d.str, symbol=symnode.str, fnext=c.ffis})!; c.ffis = f; }; }; }; }; a = a.next; }; }; d = d.next; }; }; fn ffiresolve(c: *cgen, ident: str) str = { let f: *ffi = c.ffis; for (f != nil) { let id: str = f.ident; if (streq(id, ident)) { return f.symbol; }; f = f.fnext; }; return ident; }; // ---- ABI argreg helpers --------------------------------------------- fn argregname(i: i32) str = { if (i == 0) { return "DI"; }; if (i == 1) { return "SI"; }; if (i == 2) { return "DX"; }; if (i == 3) { return "CX"; }; if (i == 4) { return "R8"; }; if (i == 5) { return "R9"; }; return "?"; }; // fargregname — XMM scalar-float arg registers (SysV: X0..X7). // Parallel to argregname / sysv_argregs; float args advance their // own counter so int and float arg slots don't conflict. export fn fargregname(i: i32) str = { if (i == 0) { return "X0"; }; if (i == 1) { return "X1"; }; if (i == 2) { return "X2"; }; if (i == 3) { return "X3"; }; if (i == 4) { return "X4"; }; if (i == 5) { return "X5"; }; if (i == 6) { return "X6"; }; if (i == 7) { return "X7"; }; return "?"; };