Files
ww/selfhost/cmd/wcc/cgen.ww
Hojun-Cho e9c11cb5ae wcc/ww: module-scope the cgen mangle-hint (#40)
use_hint/usehint were unit-global first-leaf-match: two directory-
packages exporting the same fn leaf, each imported by a different module
aliasing the same bareword, mis-routed every qualified call to whichever
use was collected first. Identically wrong on both stages (byte-id-green
#263-class). Key the hint on (owner-module, alias) and prefer cur_mod,
mirroring the checker's use_path curmod-preference (55f54fb).

989_m1usehint_run: two same-leaf pick() across a.math/b.math, each
module's call routes to its own import (111/222) + cs.s==ww.s.
2026-06-15 19:12:15 +09:00

4091 lines
138 KiB
Plaintext

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