// Port of cmd/w6c/cgen.c. package wcc; import os; import syntax; import strconv; import strings; 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. // 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: *syntax.node, // the rhs type expr aanext: *aliasent, }; fn collectaliases(c: *cgen, file: *syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, empty, 0, 0); tnvoid.str = "void"; let bang: *syntax.node = syntax.newnode(syntax.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: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.nkind.N_TYPEDECL) { let body: *syntax.node = d.lhs; if (body != nil) { if (body.kind != syntax.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) *syntax.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 (syntax.streq(a.aname, name)) { if (syntax.streq(a.amod, c.curmod)) { return a.target; }; }; a = a.aanext; }; a = c.aliases; for (a != nil) { if (syntax.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] == '.') { 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); // M1 #22: map the embedded use ALIAS (`utf8`) to the // dotted import PATH the decl's module now carries. let pkgmod: str = usehint(c, pkg); let b: *aliasent = c.aliases; for (b != nil) { if (syntax.streq(b.aname, leaf)) { if (syntax.streq(b.amod, pkgmod)) { 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) *syntax.node = { let a: *aliasent = c.aliases; for (a != nil) { if (syntax.streq(a.aname, name)) { if (syntax.streq(a.amod, c.curmod)) { return a.target; }; }; a = a.aanext; }; return nil; }; // Enum member values are pre-computed at collect time (auto-increment // + sibling refs) so cgdot can fold `Foo.MEMBER` → MOVQ $value, AX. // Mirrors cmd/wcc/check.c's enum resolution. // 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: *syntax.node, out: *u64) bool = { if (e == nil) { return false; }; let k: syntax.nkind = e.kind; if (k == syntax.nkind.N_INTLIT) { *out = e.uval; return true; }; if (k == syntax.nkind.N_RUNELIT) { *out = e.uval; return true; }; if (k == syntax.nkind.N_TRUE) { *out = 1u64; return true; }; if (k == syntax.nkind.N_FALSE) { *out = 0u64; return true; }; if (k == syntax.nkind.N_NIL) { *out = 0u64; return true; }; if (k == syntax.nkind.N_UN) { let v: u64; if (!foldintliteral(e.lhs, &v)) { return false; }; let op: syntax.tkind = e.op; if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; if (op == syntax.tkind.TK_PLUS) { *out = v; return true; }; return false; }; return false; }; fn enumevalmember(prev: *enummember, e: *syntax.node, out: *u64) bool = { if (e == nil) { return false; }; if (foldintliteral(e, out)) { return true; }; let k: syntax.nkind = e.kind; if (k == syntax.nkind.N_IDENT) { let m: *enummember = prev; for (m != nil) { if (syntax.streq(m.mname, e.str)) { *out = m.mval; return true; }; m = m.emnext; }; return false; }; if (k == syntax.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: syntax.tkind = e.op; if (op == syntax.tkind.TK_PLUS) { *out = a + b; return true; }; if (op == syntax.tkind.TK_MINUS) { *out = a - b; return true; }; if (op == syntax.tkind.TK_STAR) { *out = a * b; return true; }; if (op == syntax.tkind.TK_SLASH) { if (b == 0u64) { return false; }; *out = a / b; return true; }; if (op == syntax.tkind.TK_PERCENT) { if (b == 0u64) { return false; }; *out = a % b; return true; }; if (op == syntax.tkind.TK_AMP) { *out = a & b; return true; }; if (op == syntax.tkind.TK_PIPE) { *out = a | b; return true; }; if (op == syntax.tkind.TK_CARET) { *out = a ^ b; return true; }; if (op == syntax.tkind.TK_LSHIFT) { *out = a << b; return true; }; if (op == syntax.tkind.TK_RSHIFT) { *out = a >> b; return true; }; return false; }; if (k == syntax.nkind.N_UN) { let v: u64; if (!enumevalmember(prev, e.lhs, &v)) { return false; }; let op: syntax.tkind = e.op; if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; }; if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; }; if (op == syntax.tkind.TK_PLUS) { *out = v; return true; }; return false; }; return false; }; fn collectenums(c: *cgen, file: *syntax.node) void = { c.enums = nil; let d: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.nkind.N_TYPEDECL) { let body: *syntax.node = d.lhs; if (body != nil) { if (body.kind == syntax.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: *syntax.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 (syntax.streq(e.ename, name)) { if (syntax.streq(e.emod, c.curmod)) { return e; }; }; e = e.etnext; }; e = c.enums; for (e != nil) { if (syntax.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] == '.') { 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); // M1 #22: map the embedded use ALIAS (`utf8`) to the // dotted import PATH the decl's module now carries. let pkgmod: str = usehint(c, pkg); let b: *enumtype = c.enums; for (b != nil) { if (syntax.streq(b.ename, leaf)) { if (syntax.streq(b.emod, pkgmod)) { 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 (syntax.streq(e.ename, name)) { if (syntax.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 (syntax.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: *syntax.node) *syntax.node = { let cur: *syntax.node = t; let depth: i32 = 0; for (depth < 16) { if (cur == nil) { return nil; }; if (cur.kind != syntax.nkind.N_TNAME) { return cur; }; let nm: str = cur.str; let next: *syntax.node = aliaslookup(c, nm); if (next == nil) { return cur; }; cur = next; depth += 1; }; return cur; }; type fieldinfo = struct { fname: str, foff: i32, fsz: i32, tnode: *syntax.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, }; 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: *syntax.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: *syntax.node, // AST type expr for the storage type (i32 by default) members: *enummember, etnext: *enumtype, }; def LOOP_MAX: i32 = 16; def DEFER_MAX: i32 = 32; // #40: match cstage cgen.c DEFER_MAX (shared cap) // The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret // classifier (sretretsize over-cap-tuple arm) AND every emit/receive site // (cgreturn tuple SEND, cgmlet/cgmassign destructure, cgcall arg guard). // Classify and emit MUST agree on these, else a tuple gets classified // sret by one and in-reg by the other -> corruption. Mirrors cstage // cgen.c TUPLE_GPCAP/TUPLE_SSECAP (#10). def TUPLE_GPCAP: i32 = 4; // AX,DX,CX,R8 def TUPLE_SSECAP: i32 = 2; // X0,X1 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 uses: *modent, // M1 #22: N_USE alias → dotted import path, // for the qualified-ref codegen hint // (mname=alias, nmod=path) 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. cursource: i32, // lexical source-file scope of the current decl; // selects its own import bindings. fnret: *syntax.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: []*syntax.node, // stack of deferred exprs (LIFO at return) // 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: *syntax.node, sretforward: i32, // #22 M3 `-c`: separate-compile / primary-only codegen. Emit code+ // DATA ONLY for this package's own (imported==0) decls; treat every // `.wwi`-sourced (imported==1) dep decl as an external. Off on the // combined path (every existing invocation) so M3 is a pure addition. // NOT reset by cgeninit (which runs per-fn) — set once in main and // must survive to the post-loop emitletdataw/emitdefconstants pass, // like strlits/ffis. Symmetric with cstage Cg.sep_mode. sepmode: i32, // #99: this unit is a sep DEPENDENCY, not the root/link-entry unit. // The explicit entry mode is independent of `.wwi` production. It gates // the bare-`main` carve-out: a dep's `fn main` mangles like any decl; only the // root entry stays bare. Like sepmode, NOT reset by cgeninit (per-fn). // Symmetric with cstage Cg.sep_isdep. sepisdep: i32, initdispatchsymbol: str, // root-owned complete package-init schedule }; // 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: *syntax.node, lvnext: *letvar, }; fn cgeninit(c: *cgen) void = { c.locals = nil; c.atlocals = nil; c.frame = 0; c.lastwasreturn = 0; c.labelseq = 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: []*syntax.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: *syntax.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; }; // localreserve — localalloc minus the chain-link. #152: cglet reserves // the slot (frame bump + offset) before its initializer emits, then links // the binding into c.locals only AFTER, so a self-shadowing init // (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in // the outer scope: harec check.c clet runs cexpr before scope_define). fn localreserve(c: *cgen, name: str, sz: i32, tnode: *syntax.node) *local = { // #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) — // `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to // the old `max(8, round8(sz))` accumulation for every sz>0 (frame // stays 8-aligned, so a 1..8B slot still costs 8); the only change // is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0 // frame instead of over-reserving 8. local.sz is read only by the // @-prefix grow-check in localadd, never for user lets, so storing // the raw sz here is inert. c.frame = (c.frame + sz + 7) & ~7; let off: i32 = 0 - c.frame; let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!; return l; }; // 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: *syntax.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: *syntax.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; #44 sidesteps the // fatal for the tagged scratch by keying its NAME by size). // Mirrors cstage's cg_tagscr_slot table / cg_retscr / // cg_sretscr same-fn caches in cmd/w6c/cgen.c (#26 / #15 / #44). if (name.len > 0) { if (name[0] == '@') { 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 (syntax.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); }; // tagscradd — the ONLY alloc path for the per-fn tagged scratch (#44). // "@tagscr" keys localadd's @-prefix name-dedup by slot size, so a // fn mixing two tagged slot sizes smaller-first (regex compile(): 56B // append-element widen then 64B sret return) no longer trips the // #15/#26c grow-fatal — each distinct size pins its own first-use // slot, in source order in BOTH stages (byte-id). Mirrors cstage // cg_tagscr_slot (cmd/w6c/cgen.c). fn tagscradd(c: *cgen, sz: i32) i32 = { let buf: [32]u8; let pre: str = "@tagscr"; let i: i32 = 0; for (i < pre.len) { buf[i] = pre[i]; i += 1; }; let ns: str = strconv.i64tos(sz: i64, strconv.base.DEC); let n: i32 = ns.len; let k: i32 = 0; for (k < n) { buf[i + k] = ns.ptr[k]; k += 1; }; let total: i32 = i + n; let p: []u8 = alloc([], (total: u64) + 1u64)!; let j: i32 = 0; for (j < total) { p[j] = buf[j]; j += 1; }; p[total] = 0u8; let name: str; name.ptr = p.ptr; name.len = total; return localadd(c, name, sz, nil); }; fn localfindnode(c: *cgen, name: str) *local = { let l: *local = c.locals; for (l != nil) { let ln: str = l.name; if (syntax.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 (syntax.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 (strings.compare(ln, name) == 0) { return l.off; }; l = l.lnext; }; if (name.len > 0) { if (name[0] == 64u8) { let a: *local = c.atlocals; for (a != nil) { if (syntax.streq(a.name, name)) { return a.off; }; a = a.lnext; }; }; }; return 0; }; // 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; let cgwritefailed: i32 = 0; fn cgwrite(p: *u8, n: u64) void = { if (cgwritefailed != 0) { return; }; match (os.writeall(1i32, p, n)) { case let wrote: i64 => { if (wrote < 0 || (wrote: u64) != n) { cgwritefailed = 1; }; }; case let e: os.oserror => cgwritefailed = 1; }; }; 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) { cgwrite(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 { cgwrite(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 mname: str = c.curmod; let fname: str = c.fnname; let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC); c.labelseq += 1; let total: i32 = mname.len + fname.len + prefix.len + ns.len + 2; if (mname.len > 0) { total += 1; }; let p: []u8 = alloc([], (total: u64) + 1u64)!; let i: i32 = 0; let j: i32 = 0; for (j < mname.len) { p[i] = mname[j]; i += 1; j += 1; }; if (mname.len > 0) { p[i] = '.'; i += 1; }; j = 0; for (j < fname.len) { p[i] = fname[j]; i += 1; j += 1; }; p[i] = '_'; i += 1; j = 0; for (j < prefix.len) { p[i] = prefix[j]; i += 1; j += 1; }; p[i] = '_'; i += 1; let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { p[i + dk] = ns.ptr[dk]; dk += 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] = '.'; i += 1; let j: i32 = 0; for (j < prefix.len) { buf[i] = prefix[j]; i += 1; j += 1; }; buf[i] = '_'; 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; }; // 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 (syntax.streq(bs, bytes)) { return s.label; }; s = s.slnext; }; // New label "._S_" (bare "_S_" when curmod empty). // #49: per-unit prefix so two str-bearing packages don't both emit // `_S_0`.. and collide at w6l link. Pure function of the module path // (matching mklabel's spelling), so the self-host fixed-point holds. let mname: str = c.curmod; let ns: str = strconv.i64tos(c.strlitseq: i64, strconv.base.DEC); c.strlitseq += 1; let total: i32 = mname.len + ns.len + 3; if (mname.len > 0) { total += 1; }; let p: []u8 = alloc([], (total: u64) + 1u64)!; let i: i32 = 0; let j: i32 = 0; for (j < mname.len) { p[i] = mname[j]; i += 1; j += 1; }; if (mname.len > 0) { p[i] = '.'; i += 1; }; p[i] = 95u8; i += 1; p[i] = 83u8; i += 1; p[i] = 95u8; i += 1; let n: i32 = ns.len; let dk: i32 = 0; for (dk < n) { p[i + dk] = ns.ptr[dk]; dk += 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 (syntax.streq(nm, "bool")) { return true; }; if (syntax.streq(nm, "rune")) { return true; }; if (syntax.streq(nm, "i8")) { return true; }; if (syntax.streq(nm, "i16")) { return true; }; if (syntax.streq(nm, "i32")) { return true; }; if (syntax.streq(nm, "i64")) { return true; }; if (syntax.streq(nm, "u8")) { return true; }; if (syntax.streq(nm, "u16")) { return true; }; if (syntax.streq(nm, "u32")) { return true; }; if (syntax.streq(nm, "u64")) { return true; }; if (syntax.streq(nm, "int")) { return true; }; if (syntax.streq(nm, "uint")) { return true; }; if (syntax.streq(nm, "uintptr")) { return true; }; if (syntax.streq(nm, "size")) { return true; }; return false; }; fn letfloatprim(nm: str) i32 = { if (syntax.streq(nm, "f32")) { return 4; }; if (syntax.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. fn letemitsize(c: *cgen, d: *syntax.node) i32 = { if (d == nil) { return 0; }; let t: *syntax.node = d.lhs; for (t != nil) { if (t.kind == syntax.nkind.N_TPTR) { return 8; }; if (t.kind == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; }; if (t.kind == syntax.nkind.N_TARRAY) { let lenn: *syntax.node = t.rhs; let elemn: *syntax.node = t.lhs; let alen: i32 = 1; if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) { alen = lenn.uval: i32; } else { // #56: def/const dim — resolve from the stamped array // tinfo (rule-13), the letemitsize twin of the cgdot // .len fix. Pre-fix a non-N_INTLIT dim defaulted alen=1 // → array global mis-sized (one element's worth). let abt: *syntax.tinfo = tichase(t.type_: *syntax.tinfo); if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) { alen = abt.alen: i32; }; }; let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == syntax.nkind.N_TNAME) { let ps: i32 = aliasprimsize(c, elemn.str); if (ps > 0) { esz = ps; }; }; }; return alen * esz; }; // C-t3 (#48): tuple global — per-element slot sum (C-t0 // layout: a str/slice its header, everything else one 8B // eightbyte). Mirrors cstage let_emit_size TY_TUPLE (u->size, // the checker slot sum). Pre-C-t3 the 0 here kept tuple // globals out of collectlets entirely — no DATA emitted, and // the module-leaf fallback mis-emitted the field index as a // symbol (`MOVQ 0(SB), AX`). if (t.kind == syntax.nkind.N_TTUPLE) { let tsum: i32 = 0; let p: *syntax.node = t.list; for (p != nil) { let et: *syntax.node = p.lhs; if (isstrtype(c, et) || isslicetype(c, et)) { tsum += (tyslicesize(): i32); } else { tsum += 8; }; p = p.next; }; return tsum; }; // #87: non-nullable tagged-union global — box size (tag word + // max payload, mirror of the runtime local). Mirrors cstage // let_emit_size TY_TAGGED. if (t.kind == syntax.nkind.N_TTAGGED) { // #45 (silent→loud bridge, task #15): a nullable (*T|void) // GLOBAL has no storage path. Returning 0 here made // letcollect + emitletdataw silently skip the decl (no DATA, // no let-registration), so a later match/is/as resolved // 0(BP) or an undefined symbol — a silent miscompile in the // CSP handle-singleton substrate. Die loud at the size/ // storage layer so all three read paths hit one diagnostic; // the full storage + read-class arc is task #15 (CSP-prereq). if (isnullabletype(t)) { let mng: str = "nullable-global storage unimplemented (task #15)\n"; os.write(2, mng.ptr, mng.len: u64); os.exit(1); }; return slotsize(c, t); }; if (t.kind != syntax.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 (syntax.streq(nm, "str")) { return primtypesize("str"): i32; }; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si.totsize; }; let next: *syntax.node = aliaslookup(c, nm); if (next == nil) { return 0; }; t = next; }; return 0; }; // defaultinferredlets — #66(b-i)/#134-neg: an inferred module-global whose rhs // is an int literal (`let s = 42;`) or a single unary +/-/~ over one // (`let s = -42;`/`~42;`) is stamped by the checker with an // N_TNAME("untyped_int") annotation (d.lhs). letemitsize / emitletdataw / the // cgident global-read arm key on that annotation's name, which letscalarprim // doesn't recognise → the global is dropped from collectlets (no DATAW) and the // read falls to the silent module-leaf (no MOVQ, MOVSXD on stale AX → wrong). // cstage instead type_default's the untyped int to the 8B machine word BEFORE // emit (and folds the unary). Mirror that here at the single global-decl pass: // peel one unary +/-/~ over an N_INTLIT (operand `.lhs`, operator `.op`, as // foldintliteral) and rewrite the annotation to the concrete machine word `int`. // The inferred decl is then structurally the typed control (`let s: int = -42`), // so all three consumers fire on the existing typed-path code — byte-identical // to cstage. int (not i32) per [[project_int_machine_word_derived_limits]] — // i32 is the #108 truncation trap, opposite polarity. // Scope — INT literal operand ONLY: one unary level (covers -42/+42/~42); a // nested unary (`- -42`) is a #134-residual (cstage folds it via // foldintliteral's recursion, ww peels one level and leaves it silent) — not // widened here. A const-EXPR rhs (`let s = 7*6`, N_BIN) is #133 — a // SEPARATE loud both-stage gap (no DATA → link-fail) — and a unary over a // NON-literal (`let s = -x`) is not constant; both stay on their current route. // An inferred FLOAT global (`let s = 3.0;`) is the #134-float leg carved to // #135: cstage integer-types the inferred float at the USE site (MOVQ, not // MOVSD), so defaulting it ww-only here would emit MOVSD vs cstage's MOVQ = a // cs≠ww divergence (rule-10) — it ships only WITH the cstage float-use-site fix. // Runs before collectlets in cgfile so the mutated d.lhs is visible to // letpreintern + emitletdataw too. fn defaultinferredlets(c: *cgen, file: *syntax.node) void = { if (file == nil) { return; }; let d: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.nkind.N_LET) { if (d.lhs != nil && d.rhs != nil && d.lhs.kind == syntax.nkind.N_TNAME && syntax.streq(d.lhs.str, "untyped_int")) { let opnd: *syntax.node = d.rhs; if (opnd.kind == syntax.nkind.N_UN && (opnd.op == syntax.tkind.TK_PLUS || opnd.op == syntax.tkind.TK_MINUS || opnd.op == syntax.tkind.TK_TILDE)) { opnd = opnd.lhs; }; if (opnd != nil && opnd.kind == syntax.nkind.N_INTLIT) { d.lhs.str = "int"; }; }; // #135: the inferred-FLOAT twin, now unblocked. The carve- // out above (deferred to #135) feared a cs≠ww divergence // because cstage USED to integer-type an inferred float // (MOVQ); #150-B fixed cstage to type_default untyped_float // → f64 and load MOVSD, so defaulting here now CONVERGES. // Without it, letemitsize sees "untyped_float" (not in // letfloatprim) → 0 → the global is dropped from collectlets // (no DATAW) and the read falls to cgident's silent bare // return (X0 untouched). Mirror cstage check.c clet // type_default. if (d.lhs != nil && d.rhs != nil && d.lhs.kind == syntax.nkind.N_TNAME && syntax.streq(d.lhs.str, "untyped_float")) { let opnd: *syntax.node = d.rhs; if (opnd.kind == syntax.nkind.N_UN && (opnd.op == syntax.tkind.TK_PLUS || opnd.op == syntax.tkind.TK_MINUS)) { opnd = opnd.lhs; }; if (opnd != nil && opnd.kind == syntax.nkind.N_FLOATLIT) { d.lhs.str = "f64"; }; }; }; d = d.next; }; }; fn collectlets(c: *cgen, file: *syntax.node) void = { c.lets = nil; if (file == nil) { return; }; let d: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.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 (syntax.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) *syntax.node = { let lv: *letvar = c.lets; for (lv != nil) { if (syntax.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 (syntax.streq(lv.name, name)) { let t: *syntax.node = lv.tnode; for (t != nil) { if (t.kind != syntax.nkind.N_TNAME) { return false; }; let nm: str = t.str; if (syntax.streq(nm, "str")) { return true; }; let nx: *syntax.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`, // which resolves the declared type via type_unwrap — so an alias of // a slice IS a slice. Walks the N_TNAME alias chain exactly as the // sibling letvarisstr does (the structural N_TSLICE node is the // terminator, in place of letvarisstr's "str" name): without this, // a `type S = []T; let g: S = [...]` global misroutes to the str arm // and never reaches emitslicedata, diverging from cstage (#10). fn letvarisslice(c: *cgen, name: str) bool = { let lv: *letvar = c.lets; for (lv != nil) { if (syntax.streq(lv.name, name)) { let t: *syntax.node = lv.tnode; for (t != nil) { if (t.kind == syntax.nkind.N_TSLICE) { return true; }; if (t.kind != syntax.nkind.N_TNAME) { return false; }; let nx: *syntax.node = aliaslookup(c, t.str); if (nx == nil) { return false; }; t = nx; }; 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 (syntax.streq(lv.name, name)) { let t: *syntax.node = lv.tnode; for (t != nil) { if (t.kind != syntax.nkind.N_TNAME) { return 0; }; let fsz: i32 = letfloatprim(t.str); if (fsz > 0) { return fsz; }; let nx: *syntax.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 (syntax.streq(lv.name, name)) { let t: *syntax.node = lv.tnode; for (t != nil) { if (t.kind != syntax.nkind.N_TNAME) { return false; }; let nm: str = t.str; if (structlookup(c, nm) != nil) { return true; }; let nx: *syntax.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 (syntax.streq(lv.name, name)) { let t: *syntax.node = lv.tnode; for (t != nil) { if (t.kind != syntax.nkind.N_TNAME) { return nil; }; let nm: str = t.str; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si; }; let nx: *syntax.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 (syntax.streq(e.dname, name)) { let t: *syntax.node = e.dtnode; for (t != nil) { if (t.kind != syntax.nkind.N_TNAME) { return nil; }; let nm: str = t.str; let si: *structinfo = structlookup(c, nm); if (si != nil) { return si; }; let nx: *syntax.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) *syntax.node = { let e: *defent = c.defs; for (e != nil) { if (syntax.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); }; // preinternstrarray — SSoT for the #18 [N]str element-strlit intern // ORDER (element order, then `...` repeat-fill). Shared by letpreintern's // let arm and the #8/GAP-B def arm so both intern labels in the SAME // order emitstrarraydata references them by — a divergent order would // mis-pair the DATAR rows with their _S_ rodata. au is the chased // TY_ARRAY tinfo, r the N_ARRLIT rhs; caller verified the element is str. fn preinternstrarray(c: *cgen, au: *syntax.tinfo, r: *syntax.node) void = { let alen: i32 = au.alen: i32; let cnt: i32 = 0; let last_ev: *syntax.node = nil; let repeat: bool = false; let e: *syntax.node = r.list; for (e != nil && cnt < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { repeat = true; break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { break; }; if (ev.kind != syntax.nkind.N_STRLIT) { break; }; if (ev.str.len > 0) { internstrlit(c, ev.str); }; last_ev = ev; cnt += 1; e = e.next; }; if (repeat && last_ev != nil) { if (last_ev.str.len > 0) { for (cnt < alen) { internstrlit(c, last_ev.str); cnt += 1; }; }; }; }; // 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. fn letpreintern(c: *cgen, file: *syntax.node) void = { if (file == nil) { return; }; // #49: strlit labels allocated here (static-data initialisers) take // the OWNING decl's module prefix, not the stale last-fn curmod. // Save/restore so the later emit passes — which read curmod for // fn-ptr relocs — see the same value they did before; letpreintern // itself only interns, so driving curmod here has no other effect. let savedmod: str = c.curmod; let savedsource: i32 = c.cursource; let d: *syntax.node = file.list; for (d != nil) { c.curmod = d.nmod; c.cursource = d.sourceid; // #22 M3: skip imported deps so the strlit table (and its _S_ // sequence) is a pure function of THIS package's own decls. A // dep's body initializer would intern here, but its `.wwi` (init // stripped) would not — gating on imported keeps the // bodies-vs-.wwi `.s` byte-identical for P's own symbols. Cross- // module str-def splicing rides the def registry (interned at the // use site, not here), so it is unaffected. if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; // #8/GAP-B: a `def [N]str` needs the SAME element-strlit // pre-interning as the let [N]str arm (the #18 ordering // contract) so emitstrarraydata's DATAR rows find their _S_ // rodata. letpreintern walked only N_LET; a def's labels were // allocated too late (emitdefconstants pass) → dangling _S_. // Str-array ONLY — def tuple/slice/tagged/scalar-str stay out // of scope (#10/#270 / inline-Sdef). if (d.kind == syntax.nkind.N_DEF) { let dr: *syntax.node = d.rhs; for (dr != nil && dr.kind == syntax.nkind.N_CAST) { dr = dr.lhs; }; if (d.lhs != nil && dr != nil && dr.kind == syntax.nkind.N_ARRLIT) { let dau: *syntax.tinfo = tichase(d.lhs.type_: *syntax.tinfo); if (dau != nil && dau.kind == syntax.tykind.TY_ARRAY) { let deu: *syntax.tinfo = tichase(dau.sub); if (deu != nil && deu.kind == syntax.tykind.TY_STR) { preinternstrarray(c, dau, dr); }; }; }; }; if (d.kind == syntax.nkind.N_LET) { let r: *syntax.node = d.rhs; for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; // #18: `let xs: [N]str = […];` — pre-intern each // element's strlit in element order (then repeat-fill) // so emitstrarraydata's DATAR rows find an _S_ rodata // row. Must match that helper's interning order exactly // to keep labels stable. // g-fold #77: gate on the CHASED tinfo kind — the // N_TARRAY tnode test missed alias-typed [N]str // globals, desyncing label order vs cstage. let handled: bool = false; if (d.lhs != nil && r != nil) { let au: *syntax.tinfo = tichase(d.lhs.type_: *syntax.tinfo); if (au != nil && au.kind == syntax.tykind.TY_ARRAY && r.kind == syntax.nkind.N_ARRLIT) { let eu: *syntax.tinfo = tichase(au.sub); if (eu != nil && eu.kind == syntax.tykind.TY_STR) { handled = true; preinternstrarray(c, au, r); }; }; }; // C-t3 (#48): tuple global — pre-intern str-element // literals in element order so emitletdataw's tuple // arm's DATAR rows find their _S_ rodata rows (the // #18 array-arm pattern; cstage let_pre_intern twin). if (!handled && r != nil && d.lhs != nil) { if (r.kind == syntax.nkind.N_TUPLE) { let tlt: *syntax.node = d.lhs; for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { tlt = aliaslookup(c, tlt.str); }; if (tlt != nil) { if (tlt.kind == syntax.nkind.N_TTUPLE) { handled = true; let tp: *syntax.node = tlt.list; let e: *syntax.node = r.list; for (e != nil && tp != nil) { let et: *syntax.node = tp.lhs; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev != nil) { if (ev.kind == syntax.nkind.N_STRLIT && (isstrtype(c, et) || isslicetype(c, et))) { if (ev.str.len > 0) { internstrlit(c, ev.str); }; }; }; e = e.next; tp = tp.next; }; }; }; }; }; // #87: tagged global with a str/slice-variant literal init — // pre-intern so emittaggeddata's DATAR (ptr@+8) finds its _S_ // rodata row (the #48 tuple-arm pattern; cstage letpreintern twin). // #117: slice-of-tuple global — pre-intern each row's // str-element literals in row-then-element order so // emitslicedata's per-row DATAR patches find their _S_ // rodata rows (cstage letpreintern twin). Bounded to // inline N_TTUPLE element types. if (!handled && r != nil && d.lhs != nil) { if (r.kind == syntax.nkind.N_ARRLIT && d.lhs.kind == syntax.nkind.N_TSLICE) { let tupnode: *syntax.node = d.lhs.lhs; if (tupnode != nil && tupnode.kind == syntax.nkind.N_TTUPLE) { handled = true; let row: *syntax.node = r.list; for (row != nil) { let rw: *syntax.node = row; for (rw != nil && rw.kind == syntax.nkind.N_CAST) { rw = rw.lhs; }; if (rw != nil && rw.kind == syntax.nkind.N_TUPLE) { let tp: *syntax.node = tupnode.list; let e: *syntax.node = rw.list; for (e != nil && tp != nil) { let et: *syntax.node = tp.lhs; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev != nil) { if (ev.kind == syntax.nkind.N_STRLIT && (isstrtype(c, et) || isslicetype(c, et))) { if (ev.str.len > 0) { internstrlit(c, ev.str); }; }; }; e = e.next; tp = tp.next; }; }; row = row.next; }; }; }; }; if (!handled && r != nil && d.lhs != nil) { let tlt: *syntax.node = d.lhs; for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { tlt = aliaslookup(c, tlt.str); }; if (tlt != nil) { if (tlt.kind == syntax.nkind.N_TTAGGED && !isnullabletype(tlt)) { if (r.kind == syntax.nkind.N_STRLIT && r.str.len > 0 && (nodeisstr(c, r) || nodeisslice(c, r))) { handled = true; internstrlit(c, r.str); }; }; }; }; if (!handled) { 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) { if (r != nil) { if (r.kind == syntax.nkind.N_STRLIT) { if (r.str.len > 0) { internstrlit(c, r.str); }; }; }; }; }; }; d = d.next; }; c.curmod = savedmod; c.cursource = savedsource; }; // 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, module: str, sz: i32, rhs: *syntax.node) bool = { let isf32: bool = (sz == 4); let bits: u64 = 0u64; let neg: bool = false; if (rhs != nil) { let r: *syntax.node = rhs; for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; if (r != nil) { if (r.kind == syntax.nkind.N_UN) { if (r.op == syntax.tkind.TK_MINUS) { neg = true; r = r.lhs; for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; } else { if (r.op == syntax.tkind.TK_PLUS) { r = r.lhs; for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; };}; }; }; if (r == nil) { return false; }; if (r.kind != syntax.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(" "); emitfnname(c, name, module); 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: *syntax.tinfo, rhs: *syntax.node, base: u64) bool = { let su: *syntax.tinfo = structt; su = tichase(su); if (su == nil) { return false; }; if (su.kind != syntax.tykind.TY_STRUCT) { return false; }; let pos: u64 = base; let f: *syntax.tfield = su.fields; for (f != nil) { let fstart: u64 = base + f.offset; for (pos < fstart) { emitdatawbyte(0u8); pos = pos + 1u64; }; let v: *syntax.node = nil; if (rhs != nil) { let fnod: *syntax.node = rhs.list; for (fnod != nil) { if (syntax.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: *syntax.node = v; for (vr != nil && vr.kind == syntax.nkind.N_CAST) { vr = vr.lhs; }; let fu: *syntax.tinfo = f.type_; fu = tichase(fu); // #19 option A: a non-nullable tagged-union field rides the shared // (tag,payload) core at the field slot size, mirroring the scalar // tagged global — NOT the int emitter. Wide/struct/non-foldable // payload loud-rejects (task #30 sub-item, rule 7). v is non-nil // here (the absent-field zero-fill is handled above). if (fu != nil && fu.kind == syntax.tykind.TY_TAGGED && !syntax.typeisnullable(fu)) { if (!emittaggedbytes(c, f.type_, v, fsz, 1)) { let m: str = "emitstructlitbytes: tagged-union struct-field static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; pos = fstart + fsz: u64; f = f.tnext; continue; }; if (fu != nil && fu.kind == syntax.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 != syntax.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 == syntax.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 != syntax.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 (syntax.typeisfloat(f.type_)) { let isf32: bool = (fsz == 4); let neg: bool = false; let fr: *syntax.node = vr; if (fr != nil) { if (fr.kind == syntax.nkind.N_UN) { if (fr.op == syntax.tkind.TK_MINUS) { neg = true; fr = fr.lhs; for (fr != nil && fr.kind == syntax.nkind.N_CAST) { fr = fr.lhs; }; } else { if (fr.op == syntax.tkind.TK_PLUS) { fr = fr.lhs; for (fr != nil && fr.kind == syntax.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 != syntax.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, module: str, structt: *syntax.tinfo, rhs: *syntax.node) bool = { let su: *syntax.tinfo = structt; su = tichase(su); if (su == nil) { return false; }; if (su.kind != syntax.tykind.TY_STRUCT) { return false; }; emitline(directive); emitline(" "); emitfnname(c, name, module); 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: *syntax.tinfo, rhs: *syntax.node, emit_phase: i32) bool = { let au: *syntax.tinfo = arrt; au = tichase(au); if (au == nil) { return false; }; if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; let esz: i32 = au.sub.size: i32; let alen: i32 = au.alen: i32; let eu: *syntax.tinfo = au.sub; eu = tichase(eu); // #19 option A: a non-nullable tagged-union element rides the shared // (tag,payload) core (emittaggedbytes) at the full slot stride esz, // mirroring the scalar tagged global — NOT the int emitter, which // mis-folds the payload into the tag word. A wide/struct/non-foldable // payload element loud-rejects (task #30 sub-item, rule 7). A nullable // `(*T|void)` element is a 1-word fold, not a tag box — left to the // existing int path (task #15). if (eu != nil && eu.kind == syntax.tykind.TY_TAGGED && !syntax.typeisnullable(eu)) { let idx: i32 = 0; let last_ev: *syntax.node = nil; let e: *syntax.node = rhs.list; for (e != nil && idx < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { break; }; }; if (!emittaggedbytes(c, au.sub, e, esz, 0)) { let m: str = "emitarraylitbytes: tagged-union array element static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; last_ev = e; 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 == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { repeat = true; break; }; }; emittaggedbytes(c, au.sub, e, esz, 1); idx += 1; e = e.next; }; for (idx < alen) { if (repeat && last_ev != nil) { emittaggedbytes(c, au.sub, last_ev, esz, 1); } else { let bb: i32 = 0; for (bb < esz) { emitdatawbyte(0u8); bb += 1; }; }; idx += 1; }; return true; }; if (eu != nil && eu.kind == syntax.tykind.TY_STRUCT) { let idx: i32 = 0; let last_ev: *syntax.node = nil; let e: *syntax.node = rhs.list; for (e != nil && idx < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (ev.kind != syntax.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 == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { repeat = true; break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.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 == syntax.tykind.TY_ARRAY) { let idx: i32 = 0; let e: *syntax.node = rhs.list; for (e != nil && idx < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.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: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (ev.kind != syntax.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: *syntax.node = e; for (ev != nil && ev.kind == syntax.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 (syntax.typeisfloat(au.sub)) { let isf32: bool = syntax.typeisf32(au.sub); let idx: i32 = 0; let e: *syntax.node = rhs.list; for (e != nil && idx < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev != nil) { if (ev.kind == syntax.nkind.N_UN) { if (ev.op == syntax.tkind.TK_MINUS) { ev = ev.lhs; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; } else { if (ev.op == syntax.tkind.TK_PLUS) { ev = ev.lhs; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; };}; };}; if (ev == nil) { return false; }; if (ev.kind != syntax.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 == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { repeat = true; break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; let neg: bool = false; if (ev != nil) { if (ev.kind == syntax.nkind.N_UN) { if (ev.op == syntax.tkind.TK_MINUS) { neg = true; ev = ev.lhs; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; } else { if (ev.op == syntax.tkind.TK_PLUS) { ev = ev.lhs; for (ev != nil && ev.kind == syntax.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: *syntax.node = rhs.list; let last: u64 = 0u64; let repeat: bool = false; for (e != nil && idx < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { repeat = true; break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.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) { // #13: explicit elements fold normally; a `...` repeat replays the // LAST value; the tail PAST the explicit elements (no `...`) is // ZERO-filled. Pre-fix the default was `last`, so an under-length // literal (`[4]u64 = [1, 2]`) repeated the last value into the tail // instead of zero — cstage already zeroes (Hare: unspecified array // elements are zeroed; the #16-task zero-value ruling); this aligns // wwstage, a gate-blind cs!=ww divergence at the array-global path. let v: u64 = 0u64; if (inrepeat) { v = last; } else { if (e != nil) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { inrepeat = true; v = last; } else { e = e.next; v = last; }; } else { let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.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; }; // emitstrarraydata — module-level `let xs: [N]str = […];` static init // (#18). Mirror of cstage emit_strarray_data. A str element carries a // ptr→rodata relocation, not just bytes, so it can't ride // emitarraylitbytes (bytes-only); instead apply the scalar-str-global // pattern (DATAW header with a zero ptr placeholder + inline LE len, // then a per-element DATAR) at offset idx*esz. Each strlit was pre- // interned by letpreintern so its _S_ rodata row exists before this // row's DATAR references it. Always emits into DATAW (writable): A_DATAR // requires a DATAW holder, so both `let` and a read-only `def [N]str` // (#8/GAP-B) park their backing here — the section bit is the reloc- // holder constraint, not a mutability grant (def immutability stays // checker-enforced). Returns false when the element type isn't str. fn emitstrarraydata(c: *cgen, directive: str, name: str, module: str, arrt: *syntax.tinfo, rhs: *syntax.node) bool = { let au: *syntax.tinfo = arrt; au = tichase(au); if (au == nil) { return false; }; if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; let eu: *syntax.tinfo = au.sub; eu = tichase(eu); if (eu == nil) { return false; }; if (eu.kind != syntax.tykind.TY_STR) { return false; }; // #8/GAP-B: a str-element array's backing ALWAYS lives in DATAW // (writable section), regardless of the caller's let/def directive — // each element carries an A_DATAR ptr-reloc to its _S_ rodata row, and // w6a requires a DATAR holder be a DATAW slot (asm.c:362). The passed // directive ("DATA" for a def, "DATAW" for a let) is therefore IGNORED // here; the emit below hardcodes DATAW. A `def [N]str` stays immutable // — the checker rejects writes to a def; DATAW is only the reloc-holder // placement, not a mutability grant (rule-8 placement detail). Pre-fix // this gate skipped the def path → no DATA block → w6l undefined // 'main.C' (#270 lineage; int-def is plain DATA, no holder constraint, // so it was unaffected). let esz: i32 = au.sub.size: i32; let alen: i32 = au.alen: i32; let last_ev: *syntax.node = nil; let repeat: bool = false; let cnt: i32 = 0; let e: *syntax.node = rhs.list; for (e != nil && cnt < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { repeat = true; break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; if (ev.kind != syntax.nkind.N_STRLIT) { return false; }; last_ev = ev; cnt += 1; e = e.next; }; emitline("DATAW "); emitfnname(c, name, module); emitline("(SB),\""); let idx: i32 = 0; e = rhs.list; for (e != nil && idx < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; let v: u64 = ev.str.len: u64; i = 0; for (i < 8) { emitdatawbyte((v & 255u64): u8); v = v >> 8u64; i += 1; }; i = 16; for (i < esz) { emitdatawbyte(0u8); i += 1; }; idx += 1; e = e.next; }; for (idx < alen) { let v: u64 = 0u64; if (repeat && last_ev != nil) { v = last_ev.str.len: u64; }; let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; i = 0; for (i < 8) { emitdatawbyte((v & 255u64): u8); v = v >> 8u64; i += 1; }; i = 16; for (i < esz) { emitdatawbyte(0u8); i += 1; }; idx += 1; }; emitline("\"\n"); idx = 0; e = rhs.list; for (e != nil && idx < alen) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { break; }; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev.str.len > 0) { let lab: str = internstrlit(c, ev.str); emitline("DATAR "); emitfnname(c, name, module); emitline("+"); emitint((idx * esz): i64); emitline("(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); }; idx += 1; e = e.next; }; for (idx < alen) { if (repeat && last_ev != nil && last_ev.str.len > 0) { let lab: str = internstrlit(c, last_ev.str); emitline("DATAR "); emitfnname(c, name, module); emitline("+"); emitint((idx * esz): i64); emitline("(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); }; 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, module: str, arrt: *syntax.tinfo, rhs: *syntax.node) bool = { let au: *syntax.tinfo = arrt; au = tichase(au); if (au == nil) { return false; }; if (au.kind != syntax.tykind.TY_ARRAY) { return false; }; if (rhs == nil) { let total: u64 = arrt.size; emitline(directive); emitline(" "); emitfnname(c, name, module); emitline("(SB),\""); let i: u64 = 0u64; for (i < total) { emitdatawbyte(0u8); i = i + 1u64; }; emitline("\"\n"); return true; }; // str-element arrays carry per-element ptr relocations — handled // by the dedicated DATAW+DATAR helper (#18). if (emitstrarraydata(c, directive, name, module, arrt, rhs)) { return true; }; if (!emitarraylitbytes(c, arrt, rhs, 0)) { return false; }; emitline(directive); emitline(" "); emitfnname(c, name, module); emitline("(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); return true; }; // emitslicedata — module-level `let g: []T = [v0,…];` static init (#10 // part a). Mirror of cstage emit_slice_data. A slice literal needs a // writable backing holding the k elements, a 24B header { ptr, len, cap // }, and a DATAR patching the ptr word with the backing's VA. The // backing rides the emitarraylitbytes choke-point via a synthesized // [k]T so int/float/struct/nested-array elements reduce exactly as a // [N]T global's do. Backing symbol = ".d": a second '.' can // never collide with a user global (source identifiers carry no '.'). // Scoped to a writable `let` — A_DATAR's holder must be a DATAW slot // (w6a asm.c:362); read-only `def`, `...` repeat (no target length), // and slice-of-{str,slice,tagged} elements (per-element relocs / #17) // all loud-stop (rule 7, #10 follow-ups). fn emitslicedata(c: *cgen, name: str, module: str, slt: *syntax.tinfo, sltnode: *syntax.node, rhs: *syntax.node) void = { let su: *syntax.tinfo = slt; su = tichase(su); // Defensive, mirrors cstage emit_slice_data's // `if (u == NULL || u->kind != TY_SLICE) return 0` (rule-10): the // letvarisslice gate already guarantees a slice, so this is // unreachable — it guards the su.sub deref below if the contract // is ever violated rather than nil-derefing. if (su == nil || su.kind != syntax.tykind.TY_SLICE) { return; }; let etype: *syntax.tinfo = su.sub; let eu: *syntax.tinfo = etype; eu = tichase(eu); // Count elements; reject `...` (a slice literal has no target N). let k: i32 = 0; let e: *syntax.node = rhs.list; for (e != nil) { if (e.kind == syntax.nkind.N_FIELD) { if (syntax.streq(e.str, "...")) { let m: str = "emitslicedata: '...' repeat has no target length in a slice literal (#10, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; k += 1; e = e.next; }; // #117 aggregate-element arm: a slice of inline (str,*fn)-style // TUPLE rows. The element type-AST node (sltnode.lhs = N_TTUPLE) // drives the per-element slot classification the node-based // emittuplerow helpers expect; cstage drives the same off the tuple // tinfo's params. Bounded to inline N_TTUPLE element types. let tupnode: *syntax.node = nil; if (sltnode != nil) { tupnode = sltnode.lhs; }; let istuprow: bool = false; if (eu != nil && eu.kind == syntax.tykind.TY_TUPLE && tupnode != nil) { if (tupnode.kind == syntax.nkind.N_TTUPLE) { istuprow = true; }; }; if (istuprow) { let stride: i32 = etype.size: i32; // Validate every row before any bytes (two-pass, partial-row // safe). let e2: *syntax.node = rhs.list; for (e2 != nil) { let row: *syntax.node = e2; for (row != nil && row.kind == syntax.nkind.N_CAST) { row = row.lhs; }; let bad: bool = false; if (row == nil) { bad = true; } else if (row.kind != syntax.nkind.N_TUPLE) { bad = true; } else if (!tuplerowfoldable(c, tupnode, row)) { bad = true; }; if (bad) { let m: str = "emitslicedata: tuple-row element not a foldable constant ((str,*fn) rows only; #117, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; e2 = e2.next; }; // Backing: k rows, bytes (one DATAW) then per-row relocs. emitline("DATAW "); emitfnname(c, name, module); emitline(".d(SB),\""); e2 = rhs.list; for (e2 != nil) { let row: *syntax.node = e2; for (row != nil && row.kind == syntax.nkind.N_CAST) { row = row.lhs; }; emittuplerowbytes(c, tupnode, row); e2 = e2.next; }; emitline("\"\n"); let rowoff: i32 = 0; e2 = rhs.list; for (e2 != nil) { let row: *syntax.node = e2; for (row != nil && row.kind == syntax.nkind.N_CAST) { row = row.lhs; }; emittuplerowrelocs(c, name, module, true, rowoff, tupnode, row); rowoff += stride; e2 = e2.next; }; } else { if (eu != nil) { if (eu.kind == syntax.tykind.TY_STR || eu.kind == syntax.tykind.TY_SLICE || eu.kind == syntax.tykind.TY_TAGGED) { let m: str = "emitslicedata: slice-of-{str,slice,tagged} literal static-init unsupported (#10 follow-up, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; let esz: i32 = etype.size: i32; // Synthesize [k]T to ride the emitarraylitbytes choke-point. let arrt: *syntax.tinfo = syntax.newtype(syntax.tykind.TY_ARRAY); arrt.sub = etype; arrt.alen = k: u64; arrt.size = (k * esz): u64; if (!emitarraylitbytes(c, arrt, rhs, 0)) { let m: str = "emitslicedata: slice-literal element not a foldable constant (#10, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; emitline("DATAW "); emitfnname(c, name, module); emitline(".d(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); }; // 24B header: ptr placeholder + LE len + LE cap (both = k). Word // sizes from the type table (rule 13). emitline("DATAW "); emitfnname(c, name, module); emitline("(SB),\""); let i: i32 = 0; let ptrsz: i32 = primtypesize("uintptr"): i32; for (i < ptrsz) { emitdatawbyte(0u8); i += 1; }; let lensz: i32 = primtypesize("size"): i32; i = 0; let kv: u64 = k: u64; for (i < lensz) { emitdatawbyte((kv & 255u64): u8); kv = kv >> 8u64; i += 1; }; i = 0; kv = k: u64; for (i < lensz) { emitdatawbyte((kv & 255u64): u8); kv = kv >> 8u64; i += 1; }; emitline("\"\n"); // Patch the ptr word with the backing VA. emitline("DATAR "); emitfnname(c, name, module); emitline("+0(SB),"); emitfnname(c, name, module); emitline(".d(SB)\n"); }; // emittaggedbytes — raw sz-byte static-init payload for a tagged-union // value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO // directive open/close, NO reloc — emits exactly sz bytes via // emitdatawbyte at the current cursor. Handles zero (nil rhs) + a // foldable int payload only; a wide (str/slice) payload needs a DATAR the // raw core cannot place inside an already-open aggregate directive, and a // struct/non-foldable payload has no scalar form — both return false // WITHOUT emitting, and the caller loud-rejects (task #30 wide/struct // sub-item). `tti` is the union tinfo (NAMED-peeled + TY_TAGGED-gated // internally by flatvariantidxt). emit_phase 0 = validate only; 1 = emit. // rob's EXTRACT ruling (#19 option A): the scalar wrapper emittaggeddata // and the aggregate member branches (emitarraylitbytes/emitstructlitbytes) // share this raw core so a nested tagged member rides the SAME // (tag,payload) SSoT as a scalar tagged global. Mirror of cstage // emit_tagged_bytes. fn emittaggedbytes(c: *cgen, tti: *syntax.tinfo, rhs: *syntax.node, sz: i32, emit_phase: i32) bool = { if (tti == nil) { return false; }; if (rhs == nil) { if (emit_phase == 1) { let zi: i32 = 0; for (zi < sz) { emitdatawbyte(0u8); zi += 1; }; }; return true; }; let r: *syntax.node = rhs; for (r != nil && r.kind == syntax.nkind.N_CAST) { r = r.lhs; }; if (r == nil) { return false; }; let tag: i32 = flatvariantidxt(tti, r.type_: *syntax.tinfo, false); if (tag < 0) { return false; }; if (nodeisstr(c, r) || nodeisslice(c, r)) { return false; }; let v: u64 = 0u64; if (!foldintliteral(r, &v)) { return false; }; if (emit_phase == 1) { let acc: u64 = 0u64; let i: i32 = 0; acc = tag: u64; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; acc = v; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; i = 16; for (i < sz) { emitdatawbyte(0u8); i += 1; }; }; return true; }; // emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static // init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin): // tag word at +0 (the const-selected variant index, taggedvariantindex — // the routine the runtime widen + match dispatch key on), payload at +8, // zero-padded to the union box size `sz`. int and str-literal variants // are wired (the Hare-stdlib shapes, ref/hare/time/chrono/utc.ha:44); any // other variant payload returns false and the caller loud-stops (rule 7) — // never the pre-#87 silent no-DATA + garbage read. Mirror of cstage // emit_tagged_data. fn emittaggeddata(c: *cgen, name: str, module: str, tt: *syntax.node, rhs: *syntax.node, sz: i32) bool = { if (tt == nil) { return false; }; if (rhs == nil) { emitline("DATAW "); emitfnname(c, name, module); emitline("(SB),\""); emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 1); emitline("\"\n"); return true; }; let r: *syntax.node = rhs; for (r != nil && r.kind == syntax.nkind.N_CAST) { r = r.lhs; }; if (r == nil) { return false; }; // E8/#35: select the variant via flatvariantidx — the EXACT twin of // cstage emit_tagged_data's cg_tag_for_variant (cmd/w6c/cgen.c:15472). // taggedvariantindex adds a str/slice SHAPE fallback (cgenutil.ww:3054) // that cstage does NOT run at this site: a same-type/subset CAST init // (`let g: u = true: u;`) stamps the peeled literal's type_ as the // union itself, so flatvariantidx finds no variant and returns -1, // matching cstage's loud (caller emits "unsupported variant init", // cgen.ww:2818). The shape fallback instead picked the first scalar // variant (tag 0) -> SILENT miscompile (ran the int arm on a bool // value, the S1 coincidence trap). A bare-literal init (`= true` / `= // 7`) keeps its concrete/untyped type and still resolves via // flatvariantidx pass 1 (byte-id with cstage); a str-literal CAST // (`"hi": u`) keeps its str type and resolves to the str variant too. // Faithful tag-remap for a cast-init static global = the deferred // #23/#40 nominal widen feature. let tag: i32 = flatvariantidx(c, tt, r); if (tag < 0) { return false; }; let wide: bool = nodeisstr(c, r) || nodeisslice(c, r); let i: i32 = 0; let acc: u64 = 0u64; if (wide) { if (r.kind != syntax.nkind.N_STRLIT) { return false; }; let lv: u64 = r.str.len: u64; emitline("DATAW "); emitfnname(c, name, module); emitline("(SB),\""); // tag@0 acc = tag: u64; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; // ptr placeholder@8 i = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; // len@16 acc = lv; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; // cap@24 (= len for a static str literal, mirroring the box) acc = lv; i = 0; for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; }; // pad to sz i = 32; for (i < sz) { emitdatawbyte(0u8); i += 1; }; emitline("\"\n"); if (r.str.len > 0) { let lab: str = internstrlit(c, r.str); emitline("DATAR "); emitfnname(c, name, module); emitline("+8(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); }; return true; }; // int/zero payload via the shared raw core; validate (phase 0) BEFORE // opening the directive so a non-foldable rhs returns false without // leaving a half-written DATAW. if (!emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 0)) { return false; }; emitline("DATAW "); emitfnname(c, name, module); emitline("(SB),\""); emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 1); emitline("\"\n"); return true; }; // nodefnptr — true if `ev` (casts already peeled by the caller) is the // address-of a top-level fn (`&f`). The detect-half of the FIRST &fn→DATAR // reloc machinery (#117 slice-row + #119 scalar-global); mirrors the // address-of-fn codegen arm (fnretlookup at the N_UN TK_AMP ident, // cgenexpr.ww). The reloc target symbol is emitted via emitfnname at the // call site (cstage node_fnptr_sym returns the mangled string directly). fn nodefnptr(c: *cgen, ev: *syntax.node) bool = { if (ev == nil) { return false; }; if (ev.kind != syntax.nkind.N_UN) { return false; }; if (ev.op != syntax.tkind.TK_AMP) { return false; }; let opnd: *syntax.node = ev.lhs; if (opnd == nil) { return false; }; // #124: a cross-module `&mod.fn` — opnd is an N_DOT whose base is an // SK_USE module qualifier (not a local / let / def), and whose leaf // resolves to a fn in that module. Mangle via the module ident (not // curmod) at the emit sites so the reloc targets the same TEXT symbol // the runtime `&mod.fn` emits (cgenexpr.ww N_DOT addr-of arm). The // N_DOT arm of the #117/#119 reloc helper. if (opnd.kind == syntax.nkind.N_DOT) { if (opnd.lhs == nil) { return false; }; if (opnd.lhs.kind != syntax.nkind.N_IDENT) { return false; }; let basenm: str = opnd.lhs.str; if (localfindnode(c, basenm) != nil) { return false; }; if (isletvar(c, basenm)) { return false; }; if (deflookup(c, basenm)) { return false; }; if (fnretlookupmod(c, opnd.str, basenm) == nil) { return false; }; return true; }; if (opnd.kind != syntax.nkind.N_IDENT) { return false; }; // #14 (F7-c7): type-keyed, mirroring cstage node_fnptr_sym // (type_chase_named(opnd->type)->kind == TY_FN, cmd/w6c/cgen.c:15542- // 15543). The prior name-keyed `fnretlookup(opnd.str)` matched a fn // LEAF NAME even when the operand actually resolved to a same-named // global/local VALUE — so `&g` for an `*i64` global `g` colliding with // a fn `g` (e.g. a `mod.f` fn vs a `f` global) baked the fn's TEXT addr // into the scalar slot (ww runs rc=42; cs fails loud at w6l). Reading // the stamped operand type distinguishes the bare fn rvalue (TY_FN, the // #34 fn-rvalue stamp) from a value ident, closing the leaf-name // collision by construction. (F12 name-keyed overlap noted in the F7 // spec — same predicate-to-stamp shape; fixed once here.) let ou: *syntax.tinfo = tichase(opnd.type_: *syntax.tinfo); if (ou == nil) { return false; }; return ou.kind == syntax.tykind.TY_FN; }; // tuplerowfoldable — validate every cast-peeled element of `rhs` (an // N_TUPLE) reduces to a static row: an int literal (foldintliteral) or a // str literal in a str/slice slot. A tagged element slot has no // static-init shape (tag word + payload widening) — reject so the caller // loud-stops (#22a, rule 7); pre-guard an int init would have emitted one // 8B word into the 16B+ box (silent layout skew). The validate twin of // emittuplerowbytes / emittuplerowrelocs; two-pass keeps a partial row // out of the output (emitarraydata precedent). Factored from emittupledata // so the slice-of-tuple backing (#117) shares it. Mirror of cstage // tuple_row_foldable. fn tuplerowfoldable(c: *cgen, tt: *syntax.node, rhs: *syntax.node) bool = { let tp: *syntax.node = tt.list; let e: *syntax.node = rhs.list; for (e != nil) { let et: *syntax.node = nil; if (tp != nil) { et = tp.lhs; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; { let eti: *syntax.tinfo = nil; if (et != nil) { eti = et.type_: *syntax.tinfo; }; eti = tichase(eti); if (eti != nil && eti.kind == syntax.tykind.TY_TAGGED) { return false; }; }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { if (ev.kind != syntax.nkind.N_STRLIT) { return false; }; } else if (nodefnptr(c, ev)) { // #117: a `&fn` element folds to an 8B reloc slot. } else { let v: u64 = 0u64; if (!foldintliteral(ev, &v)) { return false; }; }; e = e.next; if (tp != nil) { tp = tp.next; }; }; return true; }; // emittuplerowbytes — the row's element bytes, concatenated, into the // currently-open DATAW quoted string (no DATAW wrapper, no sym). Slot // layout (C-t0): a scalar element is one 8B LE word; a str/slice element // its 24B header slot (8 zero ptr placeholder + LE len + 8 zero cap). // Caller has already proven the row foldable. Mirror of cstage // emit_tuple_row_bytes. fn emittuplerowbytes(c: *cgen, tt: *syntax.node, rhs: *syntax.node) void = { let tp: *syntax.node = tt.list; let e: *syntax.node = rhs.list; for (e != nil) { let et: *syntax.node = nil; if (tp != nil) { et = tp.lhs; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; let lv: u64 = ev.str.len: u64; i = 0; for (i < 8) { emitdatawbyte((lv & 255u64): u8); lv = lv >> 8u64; i += 1; }; i = 16; let ssz: i32 = primtypesize("str"): i32; for (i < ssz) { emitdatawbyte(0u8); i += 1; }; } else if (nodefnptr(c, ev)) { // #117: a `&fn` element is an 8B zero ptr placeholder; // the reloc is patched in emittuplerowrelocs. let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; } else { let v: u64 = 0u64; foldintliteral(ev, &v); let i: i32 = 0; let nv: u64 = v; for (i < 8) { emitdatawbyte((nv & 255u64): u8); nv = nv >> 8u64; i += 1; }; }; e = e.next; if (tp != nil) { tp = tp.next; }; }; }; // emittuplerowrelocs — the row's DATAR ptr patches, at backing-relative // ++. A str element patches the ptr word with the // interned strlit's VA; the slot stride steps by tyslicesize/tupeslotn. // `backing` writes the ".d" backing label; rowoff lets a slice // backing place k rows contiguously (#117), emittupledata passes 0 (foff // matches the absolute element offset — byte-neutral). Mirror of cstage // emit_tuple_row_relocs. fn emittuplerowrelocs(c: *cgen, name: str, module: str, backing: bool, rowoff: i32, tt: *syntax.node, rhs: *syntax.node) void = { let foff: i32 = rowoff; let tp: *syntax.node = tt.list; let e: *syntax.node = rhs.list; for (e != nil) { let et: *syntax.node = nil; if (tp != nil) { et = tp.lhs; }; let ev: *syntax.node = e; for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { if (ev.str.len > 0) { let lab: str = internstrlit(c, ev.str); emitline("DATAR "); emitfnname(c, name, module); if (backing) { emitline(".d"); }; emitline("+"); emitint(foff: i64); emitline("(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); }; foff += (tyslicesize(): i32); } else { // #117: the `&fn` element's reloc — the FIRST &fn→DATAR // in the emitter; patches the 8B slot at holder+foff // with the fn's TEXT VA via emitfnname. if (nodefnptr(c, ev)) { emitline("DATAR "); emitfnname(c, name, module); if (backing) { emitline(".d"); }; emitline("+"); emitint(foff: i64); emitline("(SB),"); // #124: a cross-module `&mod.fn` operand mangles // the leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (ev.lhs.kind == syntax.nkind.N_DOT) { emitfnname(c, ev.lhs.str, usehint(c, ev.lhs.lhs.str)); } else { emitfnname(c, ev.lhs.str, c.curmod); }; emitline("(SB)\n"); }; // #22: slot stride via the accessor (tagged is // rejected upstream; non-wide is 8 today — keeps the // stride on the accessor scale). foff += tupeslotn(et); }; e = e.next; if (tp != nil) { tp = tp.next; }; }; }; // emittupledata — module-level `let g: (T0, T1, ...) = (v0, ...);` // static init (C-t3, #48). One slot-laid DATAW row (+ DATAR str-element // ptr patches) via the backing-relative emittuplerow helpers; rhs == nil // zero-inits. Unsupported element inits return false and the caller // loud-stops (rule 7 — pre-C-t3 the whole definition was SILENTLY skipped // and reads saw garbage). Mirror of cstage emit_tuple_data. fn emittupledata(c: *cgen, name: str, module: str, tt: *syntax.node, rhs: *syntax.node) bool = { if (tt == nil) { return false; }; if (rhs == nil) { // #22: slot-sum via the accessor so the zero-fill matches // the checker size (cstage zero-emits u->size). let zsz: i32 = 0; let p0: *syntax.node = tt.list; for (p0 != nil) { zsz += tupeslotn(p0.lhs); p0 = p0.next; }; emitline("DATAW "); emitfnname(c, name, module); emitline("(SB),\""); let zi: i32 = 0; for (zi < zsz) { emitdatawbyte(0u8); zi += 1; }; emitline("\"\n"); return true; }; if (rhs.kind != syntax.nkind.N_TUPLE) { return false; }; if (!tuplerowfoldable(c, tt, rhs)) { return false; }; emitline("DATAW "); emitfnname(c, name, module); emitline("(SB),\""); emittuplerowbytes(c, tt, rhs); emitline("\"\n"); emittuplerowrelocs(c, name, module, false, 0, tt, rhs); return true; }; fn emitinitbackings(n: *syntax.node) void = { for (n != nil) { if (n.kind == syntax.nkind.N_ARRLIT && n.linksym.len > 0) { let u: *syntax.tinfo = tichase(n.type_: *syntax.tinfo); if (u == nil || u.kind != syntax.tykind.TY_ARRAY) { let msg: str = "runtime package slice backing has no array type\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; emitline("DATAW "); emitbytes(n.linksym.ptr, n.linksym.len: u64); emitline("(SB),\""); let count: u64 = u.size; if (count == 0u64) { count = 1u64; }; let i: u64 = 0u64; for (i < count) { emitdatawbyte(0u8); i += 1u64; }; emitline("\"\n"); }; emitinitbackings(n.attr); emitinitbackings(n.lhs); emitinitbackings(n.rhs); emitinitbackings(n.cond); emitinitbackings(n.body); emitinitbackings(n.els); emitinitbackings(n.list); n = n.next; }; }; fn emitletdataw(c: *cgen, file: *syntax.node) void = { let savedmod: str = c.curmod; let savedsource: i32 = c.cursource; let d: *syntax.node = file.list; for (d != nil) { c.curmod = d.nmod; c.cursource = d.sourceid; // #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is // initializer-less; emitting a DATAW for it would DUPLICATE the // definition that lives in the dep's own .o → link collision. // Gate on the explicit imported flag (NOT no-rhs: a package's OWN // init-less let must still zero-init). Symmetric with M2's // producer imported==0 filter — same predicate both ways. if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; if (d.kind == syntax.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); // g-fold #77: ONE chase at the dispatch entry. The // array gates below keyed on the N_TARRAY tnode — // an alias-typed global's N_TNAME matched no arm // and the skip-policy ate the decl: no DATAW, // undefined reference at link. The str/float/ // struct/slice/tuple gates already alias-walk // (letvaris* / the tlt tnode walk) and stay put. let dti: *syntax.tinfo = nil; if (d.lhs != nil) { dti = tichase(d.lhs.type_: *syntax.tinfo); }; // C-t3 (#48): tuple global — slot-laid DATAW // row (+ DATAR ptr patches for str elements) // via emittupledata. Unsupported element // inits die LOUD; pre-C-t3 the definition was // silently skipped (no DATA, no diagnostic) // and reads saw garbage. The istup gate also // keeps a tuple out of the sz==8 / str-size // arms below (a 24B tuple == str size). let tlt: *syntax.node = d.lhs; for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) { tlt = aliaslookup(c, tlt.str); }; let istup: bool = false; if (tlt != nil) { if (tlt.kind == syntax.nkind.N_TTUPLE) { istup = true; }; }; if (istup) { let tr: *syntax.node = d.rhs; for (tr != nil) { if (tr.kind != syntax.nkind.N_CAST) { break; }; tr = tr.lhs; }; if (!emittupledata(c, nm, d.nmod, tlt, tr)) { let mtg: str = "global tuple let: unsupported element init (int/str literals only; rule 7)\n"; os.write(2, mtg.ptr, mtg.len: u64); os.exit(1); }; }; // #87: non-nullable tagged-union global — emit the box // mirroring the runtime local (tag + payload). letemitsize // keeps nullable at 0 so the (*T|void) one-word fold stays // on the 8B scalar arm below. The istagged gate also keeps // a tagged box (size can equal str/slice size) out of those. let istagged: bool = false; if (tlt != nil) { if (tlt.kind == syntax.nkind.N_TTAGGED) { if (!isnullabletype(tlt)) { istagged = true; }; }; }; if (istagged) { if (!emittaggeddata(c, nm, d.nmod, tlt, d.rhs, sz)) { let mtg: str = "global tagged let: unsupported variant init (int/str literal only; rule 7)\n"; os.write(2, mtg.ptr, mtg.len: u64); os.exit(1); }; }; 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, d.nmod, 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: *syntax.node = d.rhs; if (r != nil) { if (r.kind == syntax.nkind.N_STRUCTLIT) { let st: *syntax.tinfo = d.lhs.type_: *syntax.tinfo; emitstructdata(c, "DATAW", nm, d.nmod, 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 (dti != nil) { if (dti.kind == syntax.tykind.TY_ARRAY) { isarr8 = true; }; }; if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup && !istagged) { let v: u64 = 0u64; let ok: bool = true; let fnp: bool = false; let r: *syntax.node = nil; if (d.rhs != nil) { r = d.rhs; for (r != nil) { if (r.kind != syntax.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. // #119: a scalar `&fn` global — the &fn->DATAR // reloc (the #117 helper at its second consumer). if (nodefnptr(c, r)) { fnp = true; } else { ok = foldintliteral(r, &v); }; }; if (fnp) { emitline("DATAW "); emitfnname(c, nm, d.nmod); emitline("(SB),\""); let zi: i32 = 0; for (zi < 8) { emitdatawbyte(0u8); zi += 1; }; emitline("\"\n"); emitline("DATAR "); emitfnname(c, nm, d.nmod); emitline("+0(SB),"); // #124: cross-module `&mod.fn` mangles the // leaf with the MODULE ident; same-module `&fn` // stays on curmod. if (r.lhs.kind == syntax.nkind.N_DOT) { emitfnname(c, r.lhs.str, usehint(c, r.lhs.lhs.str)); } else { emitfnname(c, r.lhs.str, c.curmod); }; emitline("(SB)\n"); } else if (ok) { emitline("DATAW "); emitfnname(c, nm, d.nmod); 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"); }; }; // #12: this arm is SIZE-keyed (sz == 24), not type-keyed, // so a no-init array global whose bytes sum to str width // (e.g. `let g: [3]u64;`) matched here AND the TY_ARRAY arm // below → two identical `DATAW g` rows (cstage is type- // keyed via let_isstr and emits one). Exclude arrays — the // emitarraydata path owns them — mirroring the existing // isarr8 guard on the sz==8 scalar arm. if (sz == primtypesize("str"): i32 && !issg && !istup && !istagged && !isarr8 && !letvarisslice(c, nm)) { let r: *syntax.node = d.rhs; for (r != nil) { if (r.kind != syntax.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 == syntax.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 "); emitfnname(c, nm, d.nmod); 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 "); emitfnname(c, nm, d.nmod); 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 == syntax.nkind.N_NIL) { ok = true; }; if (r.kind == syntax.nkind.N_STRLIT) { if (r.str.len == 0) { ok = true; }; }; }; }; if (ok) { emitline("DATAW "); emitfnname(c, nm, d.nmod); 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 && !istagged && letvarisslice(c, nm)) { let r: *syntax.node = d.rhs; for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; // #10 part a: slice-literal static init // routes through emitslicedata (header + // writable backing + DATAR). Loud-stops on // the deferred element kinds and the read- // only/`...` shapes (rule 7). if (r != nil && r.kind == syntax.nkind.N_ARRLIT) { emitslicedata(c, nm, d.nmod, d.lhs.type_: *syntax.tinfo, d.lhs, r); } else { // zero-init: accept no rhs or nil. // Any other rhs is skipped → // undefined symbol at link. let ok: bool = true; if (d.rhs != nil) { ok = false; if (r != nil) { if (r.kind == syntax.nkind.N_NIL) { ok = true; }; }; }; if (ok) { emitline("DATAW "); emitfnname(c, nm, d.nmod); 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. // #254: the zero-fill byte count comes from the // type table's tinfo.size (cstage cg_let_emit_size // returns u->size, cgen.c:978), NOT letemitsize/ // si.totsize — registerstruct rounds the nested // value-struct field's slot to 8, so a sub-8 outer // struct (ABI 4) over-emitted DATAW 8 bytes vs // cstage's 4. registerstruct / fieldsize / frame // slot-padding stay UNTOUCHED (field offsets). if (issg) { if (d.rhs == nil) { let zsz: i32 = sz; if (dti != nil) { zsz = dti.size: i32; }; emitline("DATAW "); emitfnname(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; for (i < zsz) { 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 (dti != nil) { if (dti.kind == syntax.tykind.TY_ARRAY) { let rh: *syntax.node = d.rhs; let route: bool = false; if (rh == nil) { route = true; }; if (rh != nil) { if (rh.kind == syntax.nkind.N_ARRLIT) { route = true; }; }; // #15: a zero-length array (`[0]T`) // has no bytes — cstage emits no DATA // row; wwstage's unguarded emit produced // a spurious `DATAW name(SB),""`. sz // (letemitsize, cgen.ww:2758) is 0 for // [0]T → skip. (Non-empty [N>0] arrays // keep sz>0.) if (route && sz > 0) { emitarraydata(c, "DATAW", nm, d.nmod, dti, rh); }; }; }; }; }; d = d.next; }; c.curmod = savedmod; c.cursource = savedsource; }; // 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: *syntax.node) void = { let savedmod: str = c.curmod; let savedsource: i32 = c.cursource; let d: *syntax.node = file.list; for (d != nil) { c.curmod = d.nmod; c.cursource = d.sourceid; // #22 M3: a `.wwi` dep def with DATA storage (int-fold / float / // struct / array) must NOT re-emit — the dep's own .o owns the // symbol. Str defs are inline-spliced (never emitted here), so // they need no gate; the def registry stays populated for imported // decls so the target's LOAD paths still resolve the extern. if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; if (d.kind == syntax.nkind.N_DEF) { let r: *syntax.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: *syntax.node = d.lhs; for (dt != nil) { if (dt.kind != syntax.nkind.N_TNAME) { dfsz = 0; break; }; let fsz: i32 = letfloatprim(dt.str); if (fsz > 0) { dfsz = fsz; break; }; let nx: *syntax.node = aliaslookup(c, dt.str); if (nx == nil) { dfsz = 0; break; }; dt = nx; }; if (dfsz > 0) { emitfloatlitdata(c, "DATA", d.str, d.nmod, 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 == syntax.nkind.N_STRUCTLIT) { let st: *syntax.tinfo = d.lhs.type_: *syntax.tinfo; let su: *syntax.tinfo = st; su = tichase(su); if (su != nil) { if (su.kind == syntax.tykind.TY_STRUCT) { emitstructdata(c, "DATA", d.str, d.nmod, 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 == syntax.nkind.N_ARRLIT) { let at: *syntax.tinfo = d.lhs.type_: *syntax.tinfo; let au: *syntax.tinfo = at; au = tichase(au); if (au != nil) { if (au.kind == syntax.tykind.TY_ARRAY) { emitarraydata(c, "DATA", d.str, d.nmod, at, r); }; // #10: a read-only `def g: []T = [...]` // slice literal can't carry the ptr reloc // emitslicedata needs (DATAR holder must be // DATAW, w6a asm.c:362). Loud-stop, never // silent no-emit. if (au.kind == syntax.tykind.TY_SLICE) { let m: str = "emitdefconstants: module-level slice-literal init needs a writable `let` (DATAR holder must be DATAW, w6a asm.c:362); read-only `def` unsupported (#10, rule 7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; }; };}; }; }; 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 "); emitfnname(c, d.str, d.nmod); 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; }; c.curmod = savedmod; c.cursource = savedsource; }; // 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; }; }; // fnret decides 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: *syntax.node, params: *syntax.node, frnext: *fnret, }; fn collectfnrets(c: *cgen, file: *syntax.node) void = { c.fnrets = nil; let d: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.nkind.N_FNDECL) { if (d.initfn != 0 || d.initsynthetic != 0) { d = d.next; continue; }; 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) *syntax.node = { let f: *fnret = c.fnrets; for (f != nil) { if (syntax.streq(f.fname, name)) { if (syntax.streq(f.fmod, c.curmod)) { return f.rtype; }; }; f = f.frnext; }; f = c.fnrets; for (f != nil) { if (syntax.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) *syntax.node = { // M1 #22 (#199b): the qualifier may be the import ALIAS the user // wrote (`utf8`); fn decls register f.fmod under the dotted import // PATH (`encoding.utf8`). Map alias->path so a nested-package callee // matches its own module instead of falling back to the name-only // pass — which a same-leaf caller-module fn (e.g. strings.next vs // utf8.next) otherwise wins, resolving a match scrutinee to the // caller's union and collapsing arms 2+. usehint is idempotent on a // path / c.curmod (returns the input when no `use` matches), so the // already-mapped callers (cgenexpr.ww:4309/5229) and the bare-ident // c.curmod callers are unaffected. The choke-point twin of the // struct/alias/enum usehint splitters (cgen.ww:128/319, // cgenutil.ww:2173) — closes the whole fnret class by construction. let mk: str = usehint(c, mod); if (mk.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (syntax.streq(f.fname, name)) { if (syntax.streq(f.fmod, mk)) { 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) *syntax.node = { let f: *fnret = c.fnrets; for (f != nil) { if (syntax.streq(f.fname, name)) { if (syntax.streq(f.fmod, c.curmod)) { return f.params; }; }; f = f.frnext; }; f = c.fnrets; for (f != nil) { if (syntax.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 (syntax.streq(f.fname, name)) { if (syntax.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) *syntax.node = { // M1 #22 (#199b): map import alias -> dotted path, identical to // fnretlookupmod (the param-side twin). usehint is idempotent on a // path / c.curmod so existing callers are unaffected. let mk: str = usehint(c, mod); if (mk.len > 0) { let f: *fnret = c.fnrets; for (f != nil) { if (syntax.streq(f.fname, name)) { if (syntax.streq(f.fmod, mk)) { return f.params; }; }; f = f.frnext; }; }; return fnparamslookup(c, name); }; // `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an // ident reference loads it via `MOVQ NAME(SB), AX`. type defent = struct { dname: str, dmod: str, // originating module (`// MODULE: foo`), or empty drhs: *syntax.node, dtnode: *syntax.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: *syntax.node) void = { c.defs = nil; let d: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.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 (syntax.streq(e.dname, name)) { if (syntax.streq(e.dmod, c.curmod)) { return true; }; }; e = e.dnext; }; e = c.defs; for (e != nil) { if (syntax.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) *syntax.node = { let e: *defent = c.defs; for (e != nil) { if (syntax.streq(e.dname, name)) { if (syntax.streq(e.dmod, c.curmod)) { return e.drhs; }; }; e = e.dnext; }; e = c.defs; for (e != nil) { if (syntax.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) *syntax.node = { if (mod.len > 0) { let e: *defent = c.defs; for (e != nil) { if (syntax.streq(e.dname, name)) { if (syntax.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: *syntax.node) bool = { let r: *syntax.node = rhs; for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; if (r != nil) { if (r.kind == syntax.nkind.N_UN) { if (r.op == syntax.tkind.TK_MINUS) { r = r.lhs; for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; } else { if (r.op == syntax.tkind.TK_PLUS) { r = r.lhs; for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; }; }; }; }; }; if (r == nil) { return false; }; return r.kind == syntax.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: *syntax.node) bool = { let nm: str = opnd.str; if (defvarstructinfo(c, nm) != nil) { return true; }; // #88: the emission side (emitdefconstants' array arm) peels // TY_NAMED off d.lhs.type_ transitively, so an alias-typed def // array HAS a DATA symbol — keying this gate on the unchased // dtnode kind (N_TARRAY) lied it back to the loud error. Chase // the same stamped tinfo so gate == emission set stays exact. let dtn: *syntax.node = defvartnode(c, nm); if (dtn != nil) { let du88: *syntax.tinfo = tichase(dtn.type_: *syntax.tinfo); if (du88 != nil) { if (du88.kind == syntax.tykind.TY_ARRAY) { return true; }; }; }; let drhs: *syntax.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; }; // 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`) omod: str, // owning module of a `use` decl (#40); unused for mods sourceid: i32, // owning lexical source-file scope for N_USE entries mnext: *modent, }; fn collectmods(c: *cgen, file: *syntax.node) void = { c.mods = nil; c.uses = nil; if (file == nil) { return; }; let d: *syntax.node = file.list; for (d != nil) { // M1 #22: record alias→path for the qualified-ref hint. if (d.kind == syntax.nkind.N_USE && d.useblank == 0) { if (d.usepath.len > 0) { let um: *modent = alloc(modent{mname=d.str, nmod=d.usepath, omod=d.nmod, sourceid=d.sourceid, mnext=c.uses})!; c.uses = um; }; }; if (d.initfn != 0 || d.initsynthetic != 0) { d = d.next; continue; }; // 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 == syntax.nkind.N_FNDECL) { // Fns mangle regardless of export status — covers // lib/os.read vs lib/io.read collision. let isffi: bool = false; let a: *syntax.node = d.attr; for (a != nil) { if (a.kind == syntax.nkind.N_ATTR) { let an: str = a.str; if (syntax.streq(an, "symbol")) { isffi = true; }; }; a = a.next; }; if (!isffi) { // #84: register bare-module (package-less `//ww:module- // reset`) fns TOO (nmod.len==0) — previously the // `nmod.len > 0` gate skipped ALL bare fns, so a bare fn // never entered c.mods and a bare-ident ref to it first- // matched an imported same-leaf fn (modlookupforfn) → a // #40 residual / #263-class silent dead-dup. With the bare // entry present, modlookupforfn prefers it on an empty // hint. main is the linker entry convention: a `package // main` primary's decls are MODULED "main" so main is NOT // bare here — skip it (cgfn force-emits the bare `main` // label). M1 #32: only ROOT main (imported==0) stays bare; // an IMPORTED `fn main` mangles on its path. Mirrors // cstage cgen.c mod_collect. // #99: under sep a dep unit's main is imported==0 too (its // body is composed with a path-carrying `//ww:module-reset`, // #57), so imported==0 no longer means "root unit" per-unit. // Explicit entry mode clears sepisdep independently of export // production; every other package's main remains mangled. if (!syntax.streq(d.str, "main") || d.imported != 0 || c.sepisdep != 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, sourceid=d.sourceid, mnext=c.mods})!; c.mods = m; }; }; }; // §7-A / #53: exported non-fn decls (def/type/let) path-qualify // like fns — `export def MAX` becomes `.MAX`, not a bare // `MAX` two packages could clash on under sep-compile. No export // guard: every decl with a module mangles identically. // #85 (deferred): the `nmod.len > 0` gate below retains the // bare-NON-fn skip — the sibling of #84 (a package-less let/def/ // type leaf colliding with an imported same-kind export) needs a // different fix (modlookup is hint-LESS) and has no corpus repro; // retained until then (rule-11). if (d.kind == syntax.nkind.N_DEF) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, sourceid=d.sourceid, mnext=c.mods})!; c.mods = m; }; }; if (d.kind == syntax.nkind.N_TYPEDECL) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, sourceid=d.sourceid, mnext=c.mods})!; c.mods = m; }; }; if (d.kind == syntax.nkind.N_LET) { if (d.nmod.len > 0) { let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, sourceid=d.sourceid, 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 (syntax.streq(m.mname, name)) { return m.nmod; }; m = m.mnext; }; let empty: str; empty.ptr = nil; empty.len = 0; return empty; }; // usehint — map a declared default qualifier to its canonical import path so // codegen mangles on package identity. It is source-file local: separate files // may bind the same declared name to different paths. Raw non-package // compilation retains its historical single-occurrence fallback. fn usehint(c: *cgen, alias: str) str = { let m: *modent = c.uses; let any: str; any.ptr = nil; any.len = 0; for (m != nil) { if (syntax.streq(m.mname, alias)) { if (m.sourceid == c.cursource && syntax.streq(m.omod, c.curmod)) { return m.nmod; }; if (c.sepmode == 0 && any.ptr == nil && any.len == 0) { any = m.nmod; }; }; m = m.mnext; }; if (any.ptr != nil || any.len != 0) { return any; }; return alias; }; // 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 (syntax.streq(m.mname, name)) { if (m.nmod.len == 0) { // #84: a bare-module entry (now registered). A bare-ident // ref (hint.len==0: the caller is itself in the bare/root // module) resolves to it — bare wins over an imported // same-leaf fn, by construction, order-independent. A // non-empty hint named a module, so a bare decl is // irrelevant: skip it, leaving the legacy first-imported- // match untouched (byte-id-neutral for moduled callers). if (hint.len == 0) { let empty: str; empty.ptr = nil; empty.len = 0; return empty; }; } else { if (hint.len > 0 && syntax.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); }; fn fficollect(c: *cgen, file: *syntax.node) void = { c.ffis = nil; if (file == nil) { return; }; let d: *syntax.node = file.list; for (d != nil) { if (d.kind == syntax.nkind.N_FNDECL) { if (d.initfn != 0 || d.initsynthetic != 0) { d = d.next; continue; }; let a: *syntax.node = d.attr; for (a != nil) { if (a.kind == syntax.nkind.N_ATTR) { let aname: str = a.str; if (syntax.streq(aname, "symbol")) { let symnode: *syntax.node = a.list; if (symnode != nil) { if (symnode.kind == syntax.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 (syntax.streq(id, ident)) { return f.symbol; }; f = f.fnext; }; return ident; }; // The compiler intrinsic `alloc` has a package-mode runtime ABI independent // of whichever transitive interfaces happen to be present. Raw w6c remains // declaration-driven so existing @symbol/FFI behavior is unchanged. fn allocresolve(c: *cgen) str = { if (c.sepmode != 0) { return "rt_malloc"; }; return ffiresolve(c, "malloc"); }; 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. 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 "?"; };