Files
ww/selfhost/cmd/wcc/cgen.ww
Hojun-Cho 75a03a7d69 wcc: qualify all references to the syntax package (#75)
After the frontend consolidated into one syntax package (#74), wcc still referenced syntax symbols unqualified — residue of the old flat combined namespace, where bare refs resolved by accident. Under separate compilation Hare and Go both require the package qualifier, so those bare refs would not sep-resolve.

Qualify every wcc reference to a syntax type, function, or enum member as syntax.X across the seven syntax-importing files. Resolution-only: the resolved symbol and emitted code are unchanged, so the two combined.ww regenerate textually but all five _ww binaries hold byte-for-byte. The struct-literal sites resolve via #76. This makes w6c fully separate-compilable.
2026-06-16 22:45:17 +09:00

4072 lines
142 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 syntax;
import strconv;
import strings;
import io;
import memio;
// Split files. Bundler pulls these in transitively so consumers only
// need `use cgen;`. Order matters for the flat-bundle concat — utils
// first so cgenexpr/stmt/decl can reference helpers defined here.
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: *syntax.node, // the rhs type expr
aanext: *aliasent,
};
fn collectaliases(c: *cgen, file: *syntax.node) void = {
c.aliases = nil;
// #29: seed `type nomem = !void;` here AS WELL AS in check.ww's
// seedprimitives. The two seeds aren't redundant: wwstage's check
// owns c.top (used by name resolution); cgen owns its own
// c.aliases chain (used by resolvetype / slotsize / TBANG checks).
// Without this seed, resolvetype("nomem") returns the raw N_TNAME
// — slotsize falls through to 8B without zero-init, diverging from
// cstage's `let e: nomem;` MOVQ $0 emit on the slot (rule 10).
// Inserted at the head so the user-decl loop below prepends; the
// same-module / any-match passes in aliaslookup then let a local
// `type nomem = !void;` shadow this fallback within its module.
let empty: str;
let tnvoid: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, empty, 0, 0);
tnvoid.str = "void";
let bang: *syntax.node = syntax.newnode(syntax.nkind.N_TBANG, empty, 0, 0);
bang.lhs = tnvoid;
let nomemal: *aliasent = alloc(aliasent{aname="nomem", amod=empty, target=bang, aanext=nil})!;
c.aliases = nomemal;
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_TYPEDECL) {
let body: *syntax.node = d.lhs;
if (body != nil) {
if (body.kind != syntax.nkind.N_TSTRUCT) {
let a: *aliasent = alloc(aliasent{aname=d.str, amod=d.nmod, target=body, aanext=c.aliases})!;
c.aliases = a;
};
};
};
d = d.next;
};
};
fn aliaslookup(c: *cgen, name: str) *syntax.node = {
// Same-module first, then any. Mirrors cstage's scope_lookup_prefer
// (cmd/wcc/check.c:65); without the prefer pass a bare `invalid`
// in module M with `type invalid = !void;` can collapse onto a
// strconv-style `type invalid = !i32;` registered earlier in
// c.aliases (head-first walk). The leaf-collision then drives a
// narrow MOVSXD load of a slot the let-decl zero-inits 8B-wide
// (task #27 silent-correct-by-zero-init).
let a: *aliasent = c.aliases;
for (a != nil) {
if (syntax.streq(a.aname, name)) {
if (syntax.streq(a.amod, c.curmod)) { return a.target; };
};
a = a.aanext;
};
a = c.aliases;
for (a != nil) {
if (syntax.streq(a.aname, name)) { return a.target; };
a = a.aanext;
};
// Module-qualified form: `pkg.alias` → match the leaf name
// scoped to its originating module. Mirrors check.c's module-
// qualified type resolution; requiring `amod == pkg` is what
// prevents two modules with same-leaf-name aliases from
// collapsing into whichever entry appears first in the chain.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == '.') {
let pkg: str;
pkg.ptr = name.ptr;
pkg.len = i;
let leaf: str;
leaf.ptr = name.ptr + ((i + 1): u64);
leaf.len = name.len - (i + 1);
// M1 #22: map the embedded use ALIAS (`utf8`) to the
// dotted import PATH the decl's module now carries.
let pkgmod: str = usehint(c, pkg);
let b: *aliasent = c.aliases;
for (b != nil) {
if (syntax.streq(b.aname, leaf)) {
if (syntax.streq(b.amod, pkgmod)) {
return b.target;
};
};
b = b.aanext;
};
i = -1;
} else {
i -= 1;
};
};
return nil;
};
// #223: same-module-ONLY alias resolution. aliaslookup's any-module
// fallback can return a foreign same-leaf alias; the alias-peel in
// cgdot needs to know whether THIS module defines the name as an alias
// (so the peel continues) without that cross-module fallback. Returns
// the alias target only when an alias of `name` lives in c.curmod.
fn aliassamemod(c: *cgen, name: str) *syntax.node = {
let a: *aliasent = c.aliases;
for (a != nil) {
if (syntax.streq(a.aname, name)) {
if (syntax.streq(a.amod, c.curmod)) { return a.target; };
};
a = a.aanext;
};
return nil;
};
// ---- enum 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: *syntax.node, out: *u64) bool = {
if (e == nil) { return false; };
let k: syntax.nkind = e.kind;
if (k == syntax.nkind.N_INTLIT) { *out = e.uval; return true; };
if (k == syntax.nkind.N_RUNELIT) { *out = e.uval; return true; };
if (k == syntax.nkind.N_TRUE) { *out = 1u64; return true; };
if (k == syntax.nkind.N_FALSE) { *out = 0u64; return true; };
if (k == syntax.nkind.N_NIL) { *out = 0u64; return true; };
if (k == syntax.nkind.N_UN) {
let v: u64;
if (!foldintliteral(e.lhs, &v)) { return false; };
let op: syntax.tkind = e.op;
if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; };
if (op == syntax.tkind.TK_PLUS) { *out = v; return true; };
return false;
};
return false;
};
fn enumevalmember(prev: *enummember, e: *syntax.node, out: *u64) bool = {
if (e == nil) { return false; };
if (foldintliteral(e, out)) { return true; };
let k: syntax.nkind = e.kind;
if (k == syntax.nkind.N_IDENT) {
let m: *enummember = prev;
for (m != nil) {
if (syntax.streq(m.mname, e.str)) {
*out = m.mval;
return true;
};
m = m.emnext;
};
return false;
};
if (k == syntax.nkind.N_BIN) {
let a: u64;
let b: u64;
if (!enumevalmember(prev, e.lhs, &a)) { return false; };
if (!enumevalmember(prev, e.rhs, &b)) { return false; };
let op: syntax.tkind = e.op;
if (op == syntax.tkind.TK_PLUS) { *out = a + b; return true; };
if (op == syntax.tkind.TK_MINUS) { *out = a - b; return true; };
if (op == syntax.tkind.TK_STAR) { *out = a * b; return true; };
if (op == syntax.tkind.TK_SLASH) {
if (b == 0u64) { return false; };
*out = a / b; return true;
};
if (op == syntax.tkind.TK_PERCENT) {
if (b == 0u64) { return false; };
*out = a % b; return true;
};
if (op == syntax.tkind.TK_AMP) { *out = a & b; return true; };
if (op == syntax.tkind.TK_PIPE) { *out = a | b; return true; };
if (op == syntax.tkind.TK_CARET) { *out = a ^ b; return true; };
if (op == syntax.tkind.TK_LSHIFT) { *out = a << b; return true; };
if (op == syntax.tkind.TK_RSHIFT) { *out = a >> b; return true; };
return false;
};
if (k == syntax.nkind.N_UN) {
let v: u64;
if (!enumevalmember(prev, e.lhs, &v)) { return false; };
let op: syntax.tkind = e.op;
if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; };
if (op == syntax.tkind.TK_PLUS) { *out = v; return true; };
return false;
};
return false;
};
fn collectenums(c: *cgen, file: *syntax.node) void = {
c.enums = nil;
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_TYPEDECL) {
let body: *syntax.node = d.lhs;
if (body != nil) {
if (body.kind == syntax.nkind.N_TENUM) {
let et: *enumtype = alloc(enumtype{ename=d.str, emod=d.nmod, storage=body.lhs, members=nil, etnext=nil})!;
let prev: u64 = (-1i64): u64;
let mhead: *enummember = nil;
let mtail: *enummember = nil;
let m: *syntax.node = body.list;
for (m != nil) {
let val: u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumevalmember(mhead, m.lhs, &val)) {
val = prev + 1u64;
};
};
prev = val;
let em: *enummember = alloc(enummember{mname=m.str, mval=val, emnext=nil})!;
if (mhead == nil) { mhead = em; mtail = em; }
else { mtail.emnext = em; mtail = em; };
m = m.next;
};
et.members = mhead;
et.etnext = c.enums;
c.enums = et;
};
};
};
d = d.next;
};
};
fn enumlookup(c: *cgen, name: str) *enumtype = {
// Same-module first, then any. Trio-leaf graduation mirroring
// aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without
// the prefer pass a bare-leaf enum ident in module M can collapse
// onto another module's same-leaf enum prepended earlier in
// c.enums, silently folding `Foo.MEMBER` to the wrong constant.
let e: *enumtype = c.enums;
for (e != nil) {
if (syntax.streq(e.ename, name)) {
if (syntax.streq(e.emod, c.curmod)) { return e; };
};
e = e.etnext;
};
e = c.enums;
for (e != nil) {
if (syntax.streq(e.ename, name)) { return e; };
e = e.etnext;
};
// Module-qualified form embedded in name (`pkg.enum`): scope the
// leaf to its originating module. The `emod == pkg` guard prevents
// same-leaf enums in two modules from collapsing.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == '.') {
let pkg: str;
pkg.ptr = name.ptr;
pkg.len = i;
let leaf: str;
leaf.ptr = name.ptr + ((i + 1): u64);
leaf.len = name.len - (i + 1);
// M1 #22: map the embedded use ALIAS (`utf8`) to the
// dotted import PATH the decl's module now carries.
let pkgmod: str = usehint(c, pkg);
let b: *enumtype = c.enums;
for (b != nil) {
if (syntax.streq(b.ename, leaf)) {
if (syntax.streq(b.emod, pkgmod)) {
return b;
};
};
b = b.etnext;
};
return nil;
};
i -= 1;
};
return nil;
};
// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER`
// where the qualifier is an explicit N_IDENT module name. Mirrors
// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the
// bare enumlookup so a missing or empty mod still finds the leaf.
fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = {
if (mod.len > 0) {
let e: *enumtype = c.enums;
for (e != nil) {
if (syntax.streq(e.ename, name)) {
if (syntax.streq(e.emod, mod)) { return e; };
};
e = e.etnext;
};
};
return enumlookup(c, name);
};
fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = {
let m: *enummember = en.members;
for (m != nil) {
if (syntax.streq(m.mname, mname)) {
*out = m.mval;
return true;
};
m = m.emnext;
};
return false;
};
// resolvetype — follow typedef alias chains to a "canonical" type
// expr (str/slice/array/struct/...). Stops on cycles via depth limit.
fn resolvetype(c: *cgen, t: *syntax.node) *syntax.node = {
let cur: *syntax.node = t;
let depth: i32 = 0;
for (depth < 16) {
if (cur == nil) { return nil; };
if (cur.kind != syntax.nkind.N_TNAME) { return cur; };
let nm: str = cur.str;
let next: *syntax.node = aliaslookup(c, nm);
if (next == nil) { return cur; };
cur = next;
depth += 1;
};
return cur;
};
// ---- 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: *syntax.node, // the field type expr, for nested struct lookups
finext: *fieldinfo,
};
type structinfo = struct {
sname: str,
smod: str, // originating module (`// MODULE: foo`), or empty
fields: *fieldinfo,
totsize: i32,
sinext: *structinfo,
};
// ---- 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: *syntax.node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil
lnext: *local,
};
// strlit — interned string literal record. Emitted as a DATA directive
// after all functions; cgexpr nkind.N_STRLIT loads (LEAQ ptr, MOVQ len).
type strlit = struct {
label: str, // "_S_<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: *syntax.node, // AST type expr for the storage type (i32 by default)
members: *enummember,
etnext: *enumtype,
};
def LOOP_MAX: i32 = 16;
def DEFER_MAX: i32 = 32; // #40: match cstage cgen.c DEFER_MAX (shared cap)
// The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret
// classifier (sretretsize over-cap-tuple arm) AND every emit/receive site
// (cgreturn tuple SEND, cgmlet/cgmassign destructure, cgcall arg guard).
// Classify and emit MUST agree on these, else a tuple gets classified
// sret by one and in-reg by the other -> corruption. Mirrors cstage
// cgen.c TUPLE_GPCAP/TUPLE_SSECAP (#10).
def TUPLE_GPCAP: i32 = 4; // AX,DX,CX,R8
def TUPLE_SSECAP: i32 = 2; // X0,X1
type cgen = struct {
locals: *local,
// atlocals — persistent registry of `@`-prefix scratch slots
// for the current fn. cgblock save/restores c.locals to scope
// inner shadows (post-#27); a return/cgindex/cgwidentaggedstore
// inside one block must not reallocate @retscr/@tagscr when a
// sibling block uses them again. cgblock leaves atlocals alone
// so the slot offsets survive. localadd checks here first for
// @-prefix names; localfind falls back here when c.locals misses
// an @-name. Pre-#15 this was a handful of named offsets on the
// cgen (c.retscroff / c.sretargoff / c.sretscroff); post-#15
// every @-name flows through the same registry.
atlocals: *local,
frame: i32,
lastwasreturn: i32,
labelseq: i32,
strlitseq: i32,
strlits: *strlit,
ffis: *ffi,
defs: *defent,
fnrets: *fnret,
aliases: *aliasent,
structs: *structinfo,
enums: *enumtype,
mods: *modent, // fn (any export status) + non-exported
// let/def/type decls → originating module
uses: *modent, // M1 #22: N_USE alias → dotted import path,
// for the qualified-ref codegen hint
// (mname=alias, nmod=path)
lets: *letvar, // top-level mutable scalar `let` bindings
fnname: str,
curmod: str, // current fn's `// MODULE: foo` directive (len=0
// when the fn is in the primary file). Drives
// bare-IDENT call mangling — `frob()` from
// inside lib/foo binds to `foo.frob` even when
// other modules also export `frob`. Set in cgfn
// before walking the body.
fnret: *syntax.node, // declared return type of current fn (or nil)
looptop: i32,
loopendbuf: []str, // stack of end labels for break
loopcontbuf: []str, // stack of cont labels for continue
yieldtop: i32,
yieldbuf: []str, // stack of match end labels for yield
defertop: i32,
deferbuf: []*syntax.node, // stack of deferred exprs (LIFO at return)
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
// with size > 24B are passed via a hidden first-arg pointer
// (RDI) to a caller-prealloc dest; the callee writes through
// that pointer and returns it in RAX.
//
// sretdestoff — caller-side dest BP offset, propagated from a
// receive site (cglet / cgassign ident) to the
// nested cgexpr → cgcall so the call emits
// `LEAQ off(BP), DI` instead of allocating a
// scratch. 0 means no receiver wired.
// sretforward — set by cgreturn `return f();` from an sret callee to
// signal cgcall: source RDI for inner from outer's
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
// dest. Inner writes into outer's caller-prealloc;
// inner's RAX (the dest pointer) is already outer's
// return value. Cleared after cgcall consumes it.
//
// The single-slot caches for @sretarg / @sretscr / @retscr that
// used to live here are gone: localadd's `@`-prefix dedup against
// c.locals (fail-loud on size grow) is the SSoT now. cgenstmt /
// cgenexpr resolve `@sretarg` via localfind when they need the
// saved RDI.
sretdestoff: i32,
// #220: sret receive into a GLOBAL lvalue. A BP-relative i32
// (sretdestoff) can't name a top-level let, so the lhs IDENT node
// is carried and emitted as `LEAQ name(SB), DI`. nil means no
// global receiver wired; mutually exclusive with sretdestoff.
sretdestnode: *syntax.node,
sretforward: i32,
// #22 M3 `-c`: separate-compile / primary-only codegen. Emit code+
// DATA ONLY for this package's own (imported==0) decls; treat every
// `.wwi`-sourced (imported==1) dep decl as an external. Off on the
// combined path (every existing invocation) so M3 is a pure addition.
// NOT reset by cgeninit (which runs per-fn) — set once in main and
// must survive to the post-loop emitletdataw/emitdefconstants pass,
// like strlits/ffis. Symmetric with cstage Cg.sep_mode.
sepmode: i32,
};
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
// Populated alongside modents; consulted by cgassign, cgdot, cgident
// and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot
// instead of being silently dropped. tnode is the declared type AST
// node — needed to distinguish scalar (8B) from str (16B) globals
// when picking the load/store sequence.
type letvar = struct {
name: str,
tnode: *syntax.node,
lvnext: *letvar,
};
fn cgeninit(c: *cgen) void = {
c.locals = nil;
c.atlocals = nil;
c.frame = 0;
c.lastwasreturn = 0;
c.labelseq = 0;
c.sretdestoff = 0;
c.sretdestnode = nil;
c.sretforward = 0;
// Note: strlit_seq, strlits, ffis are *not* reset here; they
// persist across cgfn calls within one file. cgfile resets them
// at the start of each compilation unit.
c.looptop = 0;
let loopendbuf: []str = alloc([], LOOP_MAX: u64)!;
c.loopendbuf = loopendbuf;
let loopcontbuf: []str = alloc([], LOOP_MAX: u64)!;
c.loopcontbuf = loopcontbuf;
c.yieldtop = 0;
let yieldbuf: []str = alloc([], LOOP_MAX: u64)!;
c.yieldbuf = yieldbuf;
c.defertop = 0;
let deferbuf: []*syntax.node = alloc([], DEFER_MAX: u64)!;
c.deferbuf = deferbuf;
};
// localalloc — append a slot for `name` without dedup. Used for
// match-arm bindings, which cstage allocates via cgexpr's by-value
// `locals` list — so two separate matches each get fresh slots even
// when their bind names collide.
fn localalloc(c: *cgen, name: str, sz: i32, tnode: *syntax.node) i32 = {
let asz: i32 = sz;
if (asz < 8) { asz = 8; };
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
c.frame += asz;
let off: i32 = 0 - c.frame;
let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=c.locals})!;
c.locals = l;
return off;
};
// localreserve — localalloc minus the chain-link. #152: cglet reserves
// the slot (frame bump + offset) before its initializer emits, then links
// the binding into c.locals only AFTER, so a self-shadowing init
// (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in
// the outer scope: harec check.c clet runs cexpr before scope_define).
fn localreserve(c: *cgen, name: str, sz: i32, tnode: *syntax.node) *local = {
// #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) —
// `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to
// the old `max(8, round8(sz))` accumulation for every sz>0 (frame
// stays 8-aligned, so a 1..8B slot still costs 8); the only change
// is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0
// frame instead of over-reserving 8. local.sz is read only by the
// @-prefix grow-check in localadd, never for user lets, so storing
// the raw sz here is inert.
c.frame = (c.frame + sz + 7) & ~7;
let off: i32 = 0 - c.frame;
let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!;
return l;
};
// localaddstack — register a param at a positive BP offset. Used for
// args that overflow the 6 SysV int / 8 float reg windows; the caller
// pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP),
// etc. (after the saved RIP+BP). No spill instruction is emitted; the
// slot IS the caller's stack slot.
fn localaddstack(c: *cgen, name: str, tnode: *syntax.node, off: i32) void = {
let l: *local = alloc(local{name=name, off=off, sz=0, tnode=tnode, lnext=c.locals})!;
c.locals = l;
};
fn localadd(c: *cgen, name: str, sz: i32, tnode: *syntax.node) i32 = {
// User-let path (post-#27): always allocate a fresh slot per
// binding. Pre-fix this deduped by name to share one slot
// across same-name lets in disjoint scopes — inherited from
// cstage's localoff. Both stages had the same silent-stack-
// corruption bug: an inner 8B `let a: i64` allocated first
// would force a later outer `let a: [128]u8` onto the 8B slot,
// and `a[127]` would write at +119(BP), past the saved RIP.
//
// `@`-prefix scratch slots (`@tagscr<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 (syntax.streq(cn, name)) {
if (asz > cur.sz) {
// rule-7 surface, post-#15: pinned slot
// offset can't grow in place.
let msg: str = "localadd: @-prefix slot grew within fn\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
cur.tnode = tnode;
return cur.off;
};
cur = cur.lnext;
};
// First use: allocate via localalloc (bumps c.frame +
// pushes to c.locals so localfind sees it within this
// block) and pin a parallel entry in c.atlocals so the
// allocation survives cgblock save/restore.
let off: i32 = localalloc(c, name, sz, tnode);
let at: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=c.atlocals})!;
c.atlocals = at;
return off;
};
};
return localalloc(c, name, sz, tnode);
};
// tagscradd — the ONLY alloc path for the per-fn tagged scratch (#44).
// "@tagscr<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 (syntax.streq(ln, name)) { return l; };
l = l.lnext;
};
// @-prefix scratch slots survive cgblock save/restore via
// c.atlocals; a localfindnode from a sibling/outer block must
// still resolve them.
if (name.len > 0) {
if (name[0] == 64u8) {
let a: *local = c.atlocals;
for (a != nil) {
if (syntax.streq(a.name, name)) { return a; };
a = a.lnext;
};
};
};
return nil;
};
fn localfind(c: *cgen, name: str) i32 = {
let l: *local = c.locals;
for (l != nil) {
let ln: str = l.name;
if (strings.compare(ln, name) == 0) { return l.off; };
l = l.lnext;
};
if (name.len > 0) {
if (name[0] == 64u8) {
let a: *local = c.atlocals;
for (a != nil) {
if (syntax.streq(a.name, name)) { return a.off; };
a = a.lnext;
};
};
};
return 0;
};
// ---- 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 (syntax.streq(bs, bytes)) {
return s.label;
};
s = s.slnext;
};
// New label "<module>._S_<seq>" (bare "_S_<seq>" when curmod empty).
// #49: per-unit prefix so two str-bearing packages don't both emit
// `_S_0`.. and collide at w6l link. Pure function of the module path
// (matching mklabel's spelling), so the self-host fixed-point holds.
let 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; };
buf[i] = 95u8; i += 1; buf[i] = 83u8; i += 1; buf[i] = 95u8; i += 1; // "_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[i + dk] = ns.ptr[dk]; dk += 1; };
c.strlitseq += 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 lab: str;
lab.ptr = p.ptr;
lab.len = total;
let nw: *strlit = alloc(strlit{label=lab, bytes=bytes, slnext=c.strlits})!;
c.strlits = nw;
return lab;
};
// letscalarprim — recognise the bare type-name keywords whose values
// fit in an 8-byte .data slot and load back with a plain MOVQ. Float
// types are handled separately by letfloatprim — they need MOVSS/MOVSD
// and use 4-byte (f32) or 8-byte (f64) slots.
fn letscalarprim(nm: str) bool = {
if (syntax.streq(nm, "bool")) { return true; };
if (syntax.streq(nm, "rune")) { return true; };
if (syntax.streq(nm, "i8")) { return true; };
if (syntax.streq(nm, "i16")) { return true; };
if (syntax.streq(nm, "i32")) { return true; };
if (syntax.streq(nm, "i64")) { return true; };
if (syntax.streq(nm, "u8")) { return true; };
if (syntax.streq(nm, "u16")) { return true; };
if (syntax.streq(nm, "u32")) { return true; };
if (syntax.streq(nm, "u64")) { return true; };
if (syntax.streq(nm, "int")) { return true; };
if (syntax.streq(nm, "uint")) { return true; };
if (syntax.streq(nm, "uintptr")) { return true; };
if (syntax.streq(nm, "size")) { return true; };
return false;
};
// 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 (syntax.streq(nm, "f32")) { return 4; };
if (syntax.streq(nm, "f64")) { return 8; };
return 0;
};
// letemitsize — slot size in bytes for a top-level `let`, or 0 if
// the type isn't yet supported as a writable global. Walks type
// aliases so byte output matches C cgen, which resolves Type kinds.
// 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: *syntax.node) i32 = {
if (d == nil) { return 0; };
let t: *syntax.node = d.lhs;
for (t != nil) {
if (t.kind == syntax.nkind.N_TPTR) { return 8; };
if (t.kind == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; };
if (t.kind == syntax.nkind.N_TARRAY) {
let lenn: *syntax.node = t.rhs;
let elemn: *syntax.node = t.lhs;
let alen: i32 = 1;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
alen = lenn.uval: i32;
} else {
// #56: def/const dim — resolve from the stamped array
// tinfo (rule-13), the letemitsize twin of the cgdot
// .len fix. Pre-fix a non-N_INTLIT dim defaulted alen=1
// → array global mis-sized (one element's worth).
let abt: *syntax.tinfo = tichase(t.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
alen = abt.alen: i32;
};
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == syntax.nkind.N_TNAME) {
let ps: i32 = aliasprimsize(c, elemn.str);
if (ps > 0) { esz = ps; };
};
};
return alen * esz;
};
// C-t3 (#48): tuple global — per-element slot sum (C-t0
// layout: a str/slice its header, everything else one 8B
// eightbyte). Mirrors cstage let_emit_size TY_TUPLE (u->size,
// the checker slot sum). Pre-C-t3 the 0 here kept tuple
// globals out of collectlets entirely — no DATA emitted, and
// the module-leaf fallback mis-emitted the field index as a
// symbol (`MOVQ 0(SB), AX`).
if (t.kind == syntax.nkind.N_TTUPLE) {
let tsum: i32 = 0;
let p: *syntax.node = t.list;
for (p != nil) {
let et: *syntax.node = p.lhs;
if (isstrtype(c, et) || isslicetype(c, et)) {
tsum += (tyslicesize(): i32);
} else {
tsum += 8;
};
p = p.next;
};
return tsum;
};
// #87: non-nullable tagged-union global — box size (tag word +
// max payload, mirror of the runtime local). Mirrors cstage
// let_emit_size TY_TAGGED.
if (t.kind == syntax.nkind.N_TTAGGED) {
// #45 (silent→loud bridge, task #15): a nullable (*T|void)
// GLOBAL has no storage path. Returning 0 here made
// letcollect + emitletdataw silently skip the decl (no DATA,
// no let-registration), so a later match/is/as resolved
// 0(BP) or an undefined symbol — a silent miscompile in the
// CSP handle-singleton substrate. Die loud at the size/
// storage layer so all three read paths hit one diagnostic;
// the full storage + read-class arc is task #15 (CSP-prereq).
if (isnullabletype(t)) {
let mng: str = "nullable-global storage unimplemented (task #15)\n";
os.write(2, mng.ptr, mng.len: u64);
os.exit(1);
};
return slotsize(c, t);
};
if (t.kind != syntax.nkind.N_TNAME) { return 0; };
let nm: str = t.str;
if (letscalarprim(nm)) { return 8; };
let fsz: i32 = letfloatprim(nm);
if (fsz > 0) { return fsz; };
if (syntax.streq(nm, "str")) { return primtypesize("str"): i32; };
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
let next: *syntax.node = aliaslookup(c, nm);
if (next == nil) { return 0; };
t = next;
};
return 0;
};
// defaultinferredlets — #66(b-i)/#134-neg: an inferred module-global whose rhs
// is an int literal (`let s = 42;`) or a single unary +/-/~ over one
// (`let s = -42;`/`~42;`) is stamped by the checker with an
// N_TNAME("untyped_int") annotation (d.lhs). letemitsize / emitletdataw / the
// cgident global-read arm key on that annotation's name, which letscalarprim
// doesn't recognise → the global is dropped from collectlets (no DATAW) and the
// read falls to the silent module-leaf (no MOVQ, MOVSXD on stale AX → wrong).
// cstage instead type_default's the untyped int to the 8B machine word BEFORE
// emit (and folds the unary). Mirror that here at the single global-decl pass:
// peel one unary +/-/~ over an N_INTLIT (operand `.lhs`, operator `.op`, as
// foldintliteral) and rewrite the annotation to the concrete machine word `int`.
// The inferred decl is then structurally the typed control (`let s: int = -42`),
// so all three consumers fire on the existing typed-path code — byte-identical
// to cstage. int (not i32) per [[project_int_machine_word_derived_limits]] —
// i32 is the #108 truncation trap, opposite polarity.
// Scope — INT literal operand ONLY: one unary level (covers -42/+42/~42); a
// nested unary (`- -42`) is a #134-residual (cstage folds it via
// foldintliteral's recursion, ww peels one level and leaves it silent) — not
// widened here. A const-EXPR rhs (`let s = 7*6`, N_BIN) is #133 — a
// SEPARATE loud both-stage gap (no DATA → link-fail) — and a unary over a
// NON-literal (`let s = -x`) is not constant; both stay on their current route.
// An inferred FLOAT global (`let s = 3.0;`) is the #134-float leg carved to
// #135: cstage integer-types the inferred float at the USE site (MOVQ, not
// MOVSD), so defaulting it ww-only here would emit MOVSD vs cstage's MOVQ = a
// cs≠ww divergence (rule-10) — it ships only WITH the cstage float-use-site fix.
// Runs before collectlets in cgfile so the mutated d.lhs is visible to
// letpreintern + emitletdataw too.
fn defaultinferredlets(c: *cgen, file: *syntax.node) void = {
if (file == nil) { return; };
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_LET) {
if (d.lhs != nil && d.rhs != nil
&& d.lhs.kind == syntax.nkind.N_TNAME
&& syntax.streq(d.lhs.str, "untyped_int")) {
let opnd: *syntax.node = d.rhs;
if (opnd.kind == syntax.nkind.N_UN
&& (opnd.op == syntax.tkind.TK_PLUS
|| opnd.op == syntax.tkind.TK_MINUS
|| opnd.op == syntax.tkind.TK_TILDE)) {
opnd = opnd.lhs;
};
if (opnd != nil
&& opnd.kind == syntax.nkind.N_INTLIT) {
d.lhs.str = "int";
};
};
// #135: the inferred-FLOAT twin, now unblocked. The carve-
// out above (deferred to #135) feared a cs≠ww divergence
// because cstage USED to integer-type an inferred float
// (MOVQ); #150-B fixed cstage to type_default untyped_float
// → f64 and load MOVSD, so defaulting here now CONVERGES.
// Without it, letemitsize sees "untyped_float" (not in
// letfloatprim) → 0 → the global is dropped from collectlets
// (no DATAW) and the read falls to cgident's silent bare
// return (X0 untouched). Mirror cstage check.c clet
// type_default.
if (d.lhs != nil && d.rhs != nil
&& d.lhs.kind == syntax.nkind.N_TNAME
&& syntax.streq(d.lhs.str, "untyped_float")) {
let opnd: *syntax.node = d.rhs;
if (opnd.kind == syntax.nkind.N_UN
&& (opnd.op == syntax.tkind.TK_PLUS
|| opnd.op == syntax.tkind.TK_MINUS)) {
opnd = opnd.lhs;
};
if (opnd != nil
&& opnd.kind == syntax.nkind.N_FLOATLIT) {
d.lhs.str = "f64";
};
};
};
d = d.next;
};
};
fn collectlets(c: *cgen, file: *syntax.node) void = {
c.lets = nil;
if (file == nil) { return; };
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_LET) {
let nm: str = d.str;
if (nm.len > 0) {
if (letemitsize(c, d) > 0) {
let lv: *letvar = alloc(letvar{name=nm, tnode=d.lhs, lvnext=c.lets})!;
c.lets = lv;
};
};
};
d = d.next;
};
};
fn isletvar(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (syntax.streq(lv.name, name)) { return true; };
lv = lv.lvnext;
};
return false;
};
// letvarisstr — is the named top-level let a str global? Resolves
// aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/
// cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare
// MOVQ scalar load.
// letvartnode — direct lookup of a top-level let's tnode. Used by
// cgindex / cgassign to detect global `[N]T` arrays and `*T`
// pointers, where the addressing path needs LEAQ name(SB) (array)
// or MOVQ name(SB) (pointer) and the element size from T.
fn letvartnode(c: *cgen, name: str) *syntax.node = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (syntax.streq(lv.name, name)) { return lv.tnode; };
lv = lv.lvnext;
};
return nil;
};
fn letvarisstr(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (syntax.streq(lv.name, name)) {
let t: *syntax.node = lv.tnode;
for (t != nil) {
if (t.kind != syntax.nkind.N_TNAME) { return false; };
let nm: str = t.str;
if (syntax.streq(nm, "str")) { return true; };
let nx: *syntax.node = aliaslookup(c, nm);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// letvarisslice — is the named top-level let a slice global?
// Slice headers are 24 bytes; the ABI flows as (AX, BX, CX) so the
// load sequence ends with `MOVQ 16(CX), CX` (overwrites the
// address holder with the cap). Mirrors C cgen's `let_isslice`,
// which resolves the declared type via type_unwrap — so an alias of
// a slice IS a slice. Walks the N_TNAME alias chain exactly as the
// sibling letvarisstr does (the structural N_TSLICE node is the
// terminator, in place of letvarisstr's "str" name): without this,
// a `type S = []T; let g: S = [...]` global misroutes to the str arm
// and never reaches emitslicedata, diverging from cstage (#10).
fn letvarisslice(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (syntax.streq(lv.name, name)) {
let t: *syntax.node = lv.tnode;
for (t != nil) {
if (t.kind == syntax.nkind.N_TSLICE) { return true; };
if (t.kind != syntax.nkind.N_TNAME) { return false; };
let nx: *syntax.node = aliaslookup(c, t.str);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// letvarisfloat — slot size for a named float global, or 0 if not
// a float-typed let. Walks aliases so the byte-identity contract
// matches C cgen's `let_isfloat` (which resolves Type kinds).
fn letvarisfloat(c: *cgen, name: str) i32 = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (syntax.streq(lv.name, name)) {
let t: *syntax.node = lv.tnode;
for (t != nil) {
if (t.kind != syntax.nkind.N_TNAME) { return 0; };
let fsz: i32 = letfloatprim(t.str);
if (fsz > 0) { return fsz; };
let nx: *syntax.node = aliaslookup(c, t.str);
if (nx == nil) { return 0; };
t = nx;
};
return 0;
};
lv = lv.lvnext;
};
return 0;
};
// letvarisstruct — is the named top-level let a struct global?
// Struct globals use LEAQ name(SB), CX as the field-access base; the
// cgdot read and cgassign write paths branch on this to skip the
// frame-relative addressing they use for locals.
fn letvarisstruct(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (syntax.streq(lv.name, name)) {
let t: *syntax.node = lv.tnode;
for (t != nil) {
if (t.kind != syntax.nkind.N_TNAME) { return false; };
let nm: str = t.str;
if (structlookup(c, nm) != nil) { return true; };
let nx: *syntax.node = aliaslookup(c, nm);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// letvarstructinfo — for a struct global, return its structinfo
// so the cgdot/cgassign paths can look up fields. nil if the let
// isn't a struct (or wasn't found).
fn letvarstructinfo(c: *cgen, name: str) *structinfo = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (syntax.streq(lv.name, name)) {
let t: *syntax.node = lv.tnode;
for (t != nil) {
if (t.kind != syntax.nkind.N_TNAME) { return nil; };
let nm: str = t.str;
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si; };
let nx: *syntax.node = aliaslookup(c, nm);
if (nx == nil) { return nil; };
t = nx;
};
return nil;
};
lv = lv.lvnext;
};
return nil;
};
// defvarstructinfo — sister of letvarstructinfo for top-level struct
// `def`s. #129 A.2 adds DATA storage for struct-typed defs; the
// LOAD-side cgdot direct-struct-global branch needs to resolve the
// def's structinfo the same way it resolves a let's, so the field-
// offset arithmetic + LEAQ name(SB) routing fires. Walks c.defs and
// the type-spec node (defent.dtnode), aliaslookup-chasing TY_NAMED
// through to the underlying struct name. Returns nil for non-struct
// defs (int/float/str — those use the existing emitsymname-based
// paths).
fn defvarstructinfo(c: *cgen, name: str) *structinfo = {
let e: *defent = c.defs;
for (e != nil) {
if (syntax.streq(e.dname, name)) {
let t: *syntax.node = e.dtnode;
for (t != nil) {
if (t.kind != syntax.nkind.N_TNAME) { return nil; };
let nm: str = t.str;
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si; };
let nx: *syntax.node = aliaslookup(c, nm);
if (nx == nil) { return nil; };
t = nx;
};
return nil;
};
e = e.dnext;
};
return nil;
};
// defvartnode — sister of letvartnode for top-level `def`s. Returns
// the type-spec node (defent.dtnode) for the named def, or nil. #129
// A.3 uses it in cgindex's array-base resolution so a `def: [N]T`
// resolves through the same N_TARRAY-detect → LEAQ name(SB) shape as
// a let array. Parallel to defvarstructinfo (#129 A.2) at the LOAD
// side widening.
fn defvartnode(c: *cgen, name: str) *syntax.node = {
let e: *defent = c.defs;
for (e != nil) {
if (syntax.streq(e.dname, name)) { return e.dtnode; };
e = e.dnext;
};
return nil;
};
// emitdatawbyte — write one byte of an asm string literal using
// the same escape rules as emitdefconstants / emitdatasection.
fn emitdatawbyte(b: u8) void = {
if (b == 34u8) { emitline("\\\""); return; };
if (b == 92u8) { emitline("\\\\"); return; };
if (b < 32u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
emitbytes( bb.ptr, 2u64);
return;
};
if (b >= 127u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
emitbytes( bb.ptr, 2u64);
return;
};
let bb: [1]u8;
bb[0] = b;
emitbytes( bb.ptr, 1u64);
};
// preinternstrarray — SSoT for the #18 [N]str element-strlit intern
// ORDER (element order, then `...` repeat-fill). Shared by letpreintern's
// let arm and the #8/GAP-B def arm so both intern labels in the SAME
// order emitstrarraydata references them by — a divergent order would
// mis-pair the DATAR rows with their _S_ rodata. au is the chased
// TY_ARRAY tinfo, r the N_ARRLIT rhs; caller verified the element is str.
fn preinternstrarray(c: *cgen, au: *syntax.tinfo, r: *syntax.node) void = {
let alen: i32 = au.alen: i32;
let cnt: i32 = 0;
let last_ev: *syntax.node = nil;
let repeat: bool = false;
let e: *syntax.node = r.list;
for (e != nil && cnt < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) {
repeat = true;
break;
};
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) {
ev = ev.lhs;
};
if (ev == nil) { break; };
if (ev.kind != syntax.nkind.N_STRLIT) { break; };
if (ev.str.len > 0) {
internstrlit(c, ev.str);
};
last_ev = ev;
cnt += 1;
e = e.next;
};
if (repeat && last_ev != nil) {
if (last_ev.str.len > 0) {
for (cnt < alen) {
internstrlit(c, last_ev.str);
cnt += 1;
};
};
};
};
// letpreintern — intern strlits referenced from top-level str-let
// initialisers BEFORE emitdatasection runs. Mirrors cmd/w6c/cgen.c
// let_pre_intern: emitletdataw later looks up the same label, and
// emitdatasection emits the DATA row in the same .s file. Running
// emitletdataw after emitdatasection would flip the (DATA strlits,
// DATAW lets) section order and break byte-identity.
export fn letpreintern(c: *cgen, file: *syntax.node) void = {
if (file == nil) { return; };
// #49: strlit labels allocated here (static-data initialisers) take
// the OWNING decl's module prefix, not the stale last-fn curmod.
// Save/restore so the later emit passes — which read curmod for
// fn-ptr relocs — see the same value they did before; letpreintern
// itself only interns, so driving curmod here has no other effect.
let savedmod: str = c.curmod;
let d: *syntax.node = file.list;
for (d != nil) {
c.curmod = d.nmod;
// #22 M3: skip imported deps so the strlit table (and its _S_
// sequence) is a pure function of THIS package's own decls. A
// dep's body initializer would intern here, but its `.wwi` (init
// stripped) would not — gating on imported keeps the
// bodies-vs-.wwi `.s` byte-identical for P's own symbols. Cross-
// module str-def splicing rides the def registry (interned at the
// use site, not here), so it is unaffected.
if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; };
// #8/GAP-B: a `def [N]str` needs the SAME element-strlit
// pre-interning as the let [N]str arm (the #18 ordering
// contract) so emitstrarraydata's DATAR rows find their _S_
// rodata. letpreintern walked only N_LET; a def's labels were
// allocated too late (emitdefconstants pass) → dangling _S_.
// Str-array ONLY — def tuple/slice/tagged/scalar-str stay out
// of scope (#10/#270 / inline-Sdef).
if (d.kind == syntax.nkind.N_DEF) {
let dr: *syntax.node = d.rhs;
for (dr != nil && dr.kind == syntax.nkind.N_CAST) {
dr = dr.lhs;
};
if (d.lhs != nil && dr != nil
&& dr.kind == syntax.nkind.N_ARRLIT) {
let dau: *syntax.tinfo = tichase(d.lhs.type_: *syntax.tinfo);
if (dau != nil && dau.kind == syntax.tykind.TY_ARRAY) {
let deu: *syntax.tinfo = tichase(dau.sub);
if (deu != nil && deu.kind == syntax.tykind.TY_STR) {
preinternstrarray(c, dau, dr);
};
};
};
};
if (d.kind == syntax.nkind.N_LET) {
let r: *syntax.node = d.rhs;
for (r != nil) {
if (r.kind != syntax.nkind.N_CAST) { break; };
r = r.lhs;
};
// #18: `let xs: [N]str = […];` — pre-intern each
// element's strlit in element order (then repeat-fill)
// so emitstrarraydata's DATAR rows find an _S_ rodata
// row. Must match that helper's interning order exactly
// to keep labels stable.
// g-fold #77: gate on the CHASED tinfo kind — the
// N_TARRAY tnode test missed alias-typed [N]str
// globals, desyncing label order vs cstage.
let handled: bool = false;
if (d.lhs != nil && r != nil) {
let au: *syntax.tinfo = tichase(d.lhs.type_: *syntax.tinfo);
if (au != nil && au.kind == syntax.tykind.TY_ARRAY
&& r.kind == syntax.nkind.N_ARRLIT) {
let eu: *syntax.tinfo = tichase(au.sub);
if (eu != nil && eu.kind == syntax.tykind.TY_STR) {
handled = true;
preinternstrarray(c, au, r);
};
};
};
// C-t3 (#48): tuple global — pre-intern str-element
// literals in element order so emitletdataw's tuple
// arm's DATAR rows find their _S_ rodata rows (the
// #18 array-arm pattern; cstage let_pre_intern twin).
if (!handled && r != nil && d.lhs != nil) {
if (r.kind == syntax.nkind.N_TUPLE) {
let tlt: *syntax.node = d.lhs;
for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) {
tlt = aliaslookup(c, tlt.str);
};
if (tlt != nil) {
if (tlt.kind == syntax.nkind.N_TTUPLE) {
handled = true;
let tp: *syntax.node = tlt.list;
let e: *syntax.node = r.list;
for (e != nil && tp != nil) {
let et: *syntax.node = tp.lhs;
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) {
ev = ev.lhs;
};
if (ev != nil) {
if (ev.kind == syntax.nkind.N_STRLIT
&& (isstrtype(c, et) || isslicetype(c, et))) {
if (ev.str.len > 0) {
internstrlit(c, ev.str);
};
};
};
e = e.next;
tp = tp.next;
};
};
};
};
};
// #87: tagged global with a str/slice-variant literal init —
// pre-intern so emittaggeddata's DATAR (ptr@+8) finds its _S_
// rodata row (the #48 tuple-arm pattern; cstage letpreintern twin).
// #117: slice-of-tuple global — pre-intern each row's
// str-element literals in row-then-element order so
// emitslicedata's per-row DATAR patches find their _S_
// rodata rows (cstage letpreintern twin). Bounded to
// inline N_TTUPLE element types.
if (!handled && r != nil && d.lhs != nil) {
if (r.kind == syntax.nkind.N_ARRLIT
&& d.lhs.kind == syntax.nkind.N_TSLICE) {
let tupnode: *syntax.node = d.lhs.lhs;
if (tupnode != nil && tupnode.kind == syntax.nkind.N_TTUPLE) {
handled = true;
let row: *syntax.node = r.list;
for (row != nil) {
let rw: *syntax.node = row;
for (rw != nil && rw.kind == syntax.nkind.N_CAST) {
rw = rw.lhs;
};
if (rw != nil && rw.kind == syntax.nkind.N_TUPLE) {
let tp: *syntax.node = tupnode.list;
let e: *syntax.node = rw.list;
for (e != nil && tp != nil) {
let et: *syntax.node = tp.lhs;
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) {
ev = ev.lhs;
};
if (ev != nil) {
if (ev.kind == syntax.nkind.N_STRLIT
&& (isstrtype(c, et) || isslicetype(c, et))) {
if (ev.str.len > 0) {
internstrlit(c, ev.str);
};
};
};
e = e.next;
tp = tp.next;
};
};
row = row.next;
};
};
};
};
if (!handled && r != nil && d.lhs != nil) {
let tlt: *syntax.node = d.lhs;
for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) {
tlt = aliaslookup(c, tlt.str);
};
if (tlt != nil) {
if (tlt.kind == syntax.nkind.N_TTAGGED && !isnullabletype(tlt)) {
if (r.kind == syntax.nkind.N_STRLIT && r.str.len > 0
&& (nodeisstr(c, r) || nodeisslice(c, r))) {
handled = true;
internstrlit(c, r.str);
};
};
};
};
if (!handled) {
let sz: i32 = letemitsize(c, d);
// #43: route the str-let gate through primtypesize so
// #1 doesn't desync this with emitletdataw's matching
// `sz == primtypesize("str"): i32` strlit-init branch.
if (sz == primtypesize("str"): i32) {
if (r != nil) {
if (r.kind == syntax.nkind.N_STRLIT) {
if (r.str.len > 0) {
internstrlit(c, r.str);
};
};
};
};
};
};
d = d.next;
};
c.curmod = savedmod;
};
// emitletdataw — DATAW directive per top-level `let` global.
// 8B scalar with int/rune/bool/nil literal init (or no init).
// 16B str — no init / `nil` / `""` → 16 zero bytes; or non-empty
// strlit init → 8 zero placeholder + 8 LE len bytes plus a
// DATAR slot+0,strlit reloc that the linker patches at load.
// sz struct — zero only.
// Non-literal scalar inits and unsupported shapes are skipped so the
// link surfaces an undefined-symbol error if the binding is used.
// Emit a (DATA|DATAW) row for a float-typed top-level let/def with a
// FLOATLIT rhs (optionally wrapped in N_CAST or N_UN(±,...)). Shared
// SSoT for emitletdataw float arm + emitdefconstants float arm (#129
// Phase A.1, rule-12 sea-of-stars). The N_UN(MINUS/PLUS) peel mirrors
// foldintliteral's MINUS/TILDE/PLUS peel (#24); the float arm had
// never been given the same treatment so `let g: f64 = -1.5;`
// silently fell through to no-emit + undef-ref at link. Negation is
// an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling
// f64/f32 bitcast helpers into cgen. Returns true on emit, false if
// rhs doesn't reduce to a foldable float literal.
fn emitfloatlitdata(c: *cgen, directive: str, name: str, module: str,
sz: i32, rhs: *syntax.node) bool = {
let isf32: bool = (sz == 4);
let bits: u64 = 0u64;
let neg: bool = false;
if (rhs != nil) {
let r: *syntax.node = rhs;
for (r != nil) {
if (r.kind != syntax.nkind.N_CAST) { break; };
r = r.lhs;
};
if (r != nil) {
if (r.kind == syntax.nkind.N_UN) {
if (r.op == syntax.tkind.TK_MINUS) {
neg = true;
r = r.lhs;
for (r != nil) {
if (r.kind != syntax.nkind.N_CAST) { break; };
r = r.lhs;
};
} else { if (r.op == syntax.tkind.TK_PLUS) {
r = r.lhs;
for (r != nil) {
if (r.kind != syntax.nkind.N_CAST) { break; };
r = r.lhs;
};
};};
};
};
if (r == nil) { return false; };
if (r.kind != syntax.nkind.N_FLOATLIT) { return false; };
// r.uval holds f64 bits regardless of literal suffix (lexer
// stores the pre-narrow bits). f32 needs an explicit
// (double→float) narrowing at emit time — mirrors cstage's
// `union { float f; u32 u; } x; x.f = (float)r->fval`
// (cgen.c:8436). Pre-#129 wwstage truncated the low 4 bytes
// of the f64 bits, which silently emitted 0 for f32 lits;
// the bug never bit because no current consumer has a f32
// let-init (surfaced by the consolidation gate).
bits = r.uval;
if (isf32) {
let dv: f64 = *((&bits): *f64);
let fv: f32 = (dv: f32);
let uv: u32 = *((&fv): *u32);
bits = uv: u64;
};
};
emitline(directive);
emitline(" ");
emitfnname(c, name, module);
emitline("(SB),\"");
// IEEE-754 sign-bit XOR for negation happens INSIDE the emit
// loop on the top byte only — equivalent to a whole-u64 XOR with
// 2^63 but never materialises that constant. Avoids strconv's
// i64tos-on-i64-MIN bug (#144) and any future cstage const-fold
// of `1 << 63` back to the i64-MIN immediate, either of which
// would break cs==ww byte-id on the cgen.ww self-rebuild (995).
let i: i32 = 0;
let nb: u64 = bits;
for (i < sz) {
let b: u8 = (nb & 255u64): u8;
if (neg) {
if (i == sz - 1) { b = b ^ 128u8; };
};
emitdatawbyte(b);
nb = nb >> 8u64;
i += 1;
};
emitline("\"\n");
return true;
};
// emitstructlitbytes — payload of a struct-typed top-level let/def
// with N_STRUCTLIT rhs. Walks structt.fields, zero-fills padding via
// the per-field offset (rule 13), dispatches per field type:
// foldintliteral for int/bool/nil, inline bitcast+sign-XOR for float,
// recursive call for nested struct. Other field kinds (str / slice /
// ptr-with-address / array) are out of #129 A.2 scope — rule-7 aborts
// loud rather than silently emitting wrong bytes. Mirror of cstage
// emit_struct_lit_bytes. `base` offsets the field-start computation
// so the recursive call walks an inner struct's fields within its
// outer parent's byte stream.
fn emitstructlitbytes(c: *cgen, structt: *syntax.tinfo, rhs: *syntax.node,
base: u64) bool = {
let su: *syntax.tinfo = structt;
su = tichase(su);
if (su == nil) { return false; };
if (su.kind != syntax.tykind.TY_STRUCT) { return false; };
let pos: u64 = base;
let f: *syntax.tfield = su.fields;
for (f != nil) {
let fstart: u64 = base + f.offset;
for (pos < fstart) {
emitdatawbyte(0u8);
pos = pos + 1u64;
};
let v: *syntax.node = nil;
if (rhs != nil) {
let fnod: *syntax.node = rhs.list;
for (fnod != nil) {
if (syntax.streq(fnod.str, f.name)) {
v = fnod.lhs;
break;
};
fnod = fnod.next;
};
};
let fsz: i32 = f.type_.size: i32;
if (v == nil) {
let i: i32 = 0;
for (i < fsz) {
emitdatawbyte(0u8);
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
let vr: *syntax.node = v;
for (vr != nil && vr.kind == syntax.nkind.N_CAST) { vr = vr.lhs; };
let fu: *syntax.tinfo = f.type_;
fu = tichase(fu);
// #19 option A: a non-nullable tagged-union field rides the shared
// (tag,payload) core at the field slot size, mirroring the scalar
// tagged global — NOT the int emitter. Wide/struct/non-foldable
// payload loud-rejects (task #30 sub-item, rule 7). v is non-nil
// here (the absent-field zero-fill is handled above).
if (fu != nil && fu.kind == syntax.tykind.TY_TAGGED && !syntax.typeisnullable(fu)) {
if (!emittaggedbytes(c, f.type_, v, fsz, 1)) {
let m: str = "emitstructlitbytes: tagged-union struct-field static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) {
if (vr == nil) {
let m: str = "emitstructlitbytes: nested struct field rhs nil (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (vr.kind != syntax.nkind.N_STRUCTLIT) {
let m: str = "emitstructlitbytes: nested struct rhs not N_STRUCTLIT (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
emitstructlitbytes(c, f.type_, vr, fstart);
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
// #129 A.3: array-typed field with N_ARRLIT rhs (the shape
// parked in A.2). Recurses through emitarraylitbytes for
// element-kind dispatch. Rule-7 stops loudly if rhs shape
// doesn't match.
if (fu != nil && fu.kind == syntax.tykind.TY_ARRAY) {
if (vr == nil) {
let m: str = "emitstructlitbytes: array field rhs nil (#129 A.3)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (vr.kind != syntax.nkind.N_ARRLIT) {
let m: str = "emitstructlitbytes: array field rhs not N_ARRLIT (#129 A.3)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (!emitarraylitbytes(c, f.type_, vr, 1)) {
let m: str = "emitstructlitbytes: array field rhs has non-reducible elements (#129 A.3)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
if (syntax.typeisfloat(f.type_)) {
let isf32: bool = (fsz == 4);
let neg: bool = false;
let fr: *syntax.node = vr;
if (fr != nil) { if (fr.kind == syntax.nkind.N_UN) {
if (fr.op == syntax.tkind.TK_MINUS) {
neg = true;
fr = fr.lhs;
for (fr != nil && fr.kind == syntax.nkind.N_CAST) { fr = fr.lhs; };
} else { if (fr.op == syntax.tkind.TK_PLUS) {
fr = fr.lhs;
for (fr != nil && fr.kind == syntax.nkind.N_CAST) { fr = fr.lhs; };
};};
};};
if (fr == nil) {
let m: str = "emitstructlitbytes: float field rhs nil (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (fr.kind != syntax.nkind.N_FLOATLIT) {
let m: str = "emitstructlitbytes: float field rhs not FLOATLIT (#129 A.2)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let bits: u64 = fr.uval;
if (isf32) {
let dv: f64 = *((&bits): *f64);
let fv: f32 = (dv: f32);
let uv: u32 = *((&fv): *u32);
bits = uv: u64;
};
let i: i32 = 0;
let nb: u64 = bits;
for (i < fsz) {
let b: u8 = (nb & 255u64): u8;
if (neg) {
if (i == fsz - 1) { b = b ^ 128u8; };
};
emitdatawbyte(b);
nb = nb >> 8u64;
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
continue;
};
let iv: u64 = 0u64;
if (!foldintliteral(vr, &iv)) {
let m: str = "emitstructlitbytes: field rhs not foldable (str/slice/ptr/array out of #129 A.2 scope)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let i: i32 = 0;
let nb: u64 = iv;
for (i < fsz) {
emitdatawbyte((nb & 255u64): u8);
nb = nb >> 8u64;
i = i + 1;
};
pos = fstart + fsz: u64;
f = f.tnext;
};
let endpos: u64 = base + structt.size;
for (pos < endpos) {
emitdatawbyte(0u8);
pos = pos + 1u64;
};
return true;
};
// emitstructdata — top-level wrapper. Opens the DATA/DATAW directive
// then delegates to emitstructlitbytes. Shared between emitletdataw
// struct arm and emitdefconstants struct arm (#129 A.2).
fn emitstructdata(c: *cgen, directive: str, name: str, module: str,
structt: *syntax.tinfo, rhs: *syntax.node) bool = {
let su: *syntax.tinfo = structt;
su = tichase(su);
if (su == nil) { return false; };
if (su.kind != syntax.tykind.TY_STRUCT) { return false; };
emitline(directive);
emitline(" ");
emitfnname(c, name, module);
emitline("(SB),\"");
emitstructlitbytes(c, structt, rhs, 0u64);
emitline("\"\n");
return true;
};
// emitarraylitbytes — emit alen * esz bytes for an [N]T top-level let/
// def with N_ARRLIT rhs. Mirrors cstage emit_array_lit_bytes. Per-
// element dispatch:
// - int (covers bool/rune/typed-int/N_UN-int): foldintliteral per
// element. Existing pre-#129-A.3 emitletdataw array arm logic
// preserved byte-for-byte so bootstrap consumers (lib/os, lib/
// bufio, lib/strings, lib/encoding/utf8, lib/strconv/stof_data)
// don't shift.
// - float (f32/f64): peel N_CAST/N_UN(±), bitcast magnitude via
// pointer-cast round-trip (mirror emitfloatlitdata), sign-XOR
// top byte of each element inline. No 2^63 immediate.
// - struct: per element call emitstructlitbytes (#129 A.2 helper).
// - other element kinds (ptr/nested-array): returns false — caller
// falls through to zero-init.
//
// Two-pass validate-then-emit (`emit_phase=0` validate-only, `=1`
// actually emit) keeps emit-on-failure from emitting partial bytes
// into an open DATA literal.
fn emitarraylitbytes(c: *cgen, arrt: *syntax.tinfo, rhs: *syntax.node,
emit_phase: i32) bool = {
let au: *syntax.tinfo = arrt;
au = tichase(au);
if (au == nil) { return false; };
if (au.kind != syntax.tykind.TY_ARRAY) { return false; };
let esz: i32 = au.sub.size: i32;
let alen: i32 = au.alen: i32;
let eu: *syntax.tinfo = au.sub;
eu = tichase(eu);
// #19 option A: a non-nullable tagged-union element rides the shared
// (tag,payload) core (emittaggedbytes) at the full slot stride esz,
// mirroring the scalar tagged global — NOT the int emitter, which
// mis-folds the payload into the tag word. A wide/struct/non-foldable
// payload element loud-rejects (task #30 sub-item, rule 7). A nullable
// `(*T|void)` element is a 1-word fold, not a tag box — left to the
// existing int path (task #15).
if (eu != nil && eu.kind == syntax.tykind.TY_TAGGED && !syntax.typeisnullable(eu)) {
let idx: i32 = 0;
let last_ev: *syntax.node = nil;
let e: *syntax.node = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { break; };
};
if (!emittaggedbytes(c, au.sub, e, esz, 0)) {
let m: str = "emitarraylitbytes: tagged-union array element static-init needs a zero/int payload; wide (str/slice) or struct payload is deferred (task #30, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
last_ev = e;
idx += 1;
e = e.next;
};
if (emit_phase == 0) { return true; };
idx = 0;
let repeat: bool = false;
e = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { repeat = true; break; };
};
emittaggedbytes(c, au.sub, e, esz, 1);
idx += 1;
e = e.next;
};
for (idx < alen) {
if (repeat && last_ev != nil) {
emittaggedbytes(c, au.sub, last_ev, esz, 1);
} else {
let bb: i32 = 0;
for (bb < esz) { emitdatawbyte(0u8); bb += 1; };
};
idx += 1;
};
return true;
};
if (eu != nil && eu.kind == syntax.tykind.TY_STRUCT) {
// Validate: every element must be N_STRUCTLIT (after N_CAST).
let idx: i32 = 0;
let last_ev: *syntax.node = nil;
let e: *syntax.node = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { break; };
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
if (ev == nil) { return false; };
if (ev.kind != syntax.nkind.N_STRUCTLIT) { return false; };
last_ev = ev;
idx += 1;
e = e.next;
};
if (emit_phase == 0) { return true; };
idx = 0;
let repeat: bool = false;
e = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { repeat = true; break; };
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
emitstructlitbytes(c, au.sub, ev, 0u64);
idx += 1;
e = e.next;
};
for (idx < alen) {
if (repeat && last_ev != nil) {
emitstructlitbytes(c, au.sub, last_ev, 0u64);
} else {
let bb: i32 = 0;
for (bb < esz) { emitdatawbyte(0u8); bb += 1; };
};
idx += 1;
};
return true;
};
// #129 A.3 capstone (PREREQ-1, #156): nested-array element [M]T
// inside [N][M]T. Mirror of the TY_STRUCT-element arm above and of
// the TY_ARRAY-field-in-struct arm in emitstructlitbytes — recurse
// into emitarraylitbytes per element; recursion bottoms out at
// scalar (int/float) elements. esz = au.sub.size gives the per-
// element stride (rule 13). The `...` repeat marker with nested-
// array elements is rejected loud (rule 7): no consumer needs it
// (powers_of_ten is fully enumerated).
if (eu != nil && eu.kind == syntax.tykind.TY_ARRAY) {
let idx: i32 = 0;
let e: *syntax.node = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) {
let m: str = "emitarraylitbytes: '...' repeat with nested-array elements unsupported (#129 A.3, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
if (ev == nil) { return false; };
if (ev.kind != syntax.nkind.N_ARRLIT) { return false; };
if (!emitarraylitbytes(c, au.sub, ev, 0)) { return false; };
idx += 1;
e = e.next;
};
if (emit_phase == 0) { return true; };
idx = 0;
e = rhs.list;
for (e != nil && idx < alen) {
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
emitarraylitbytes(c, au.sub, ev, 1);
idx += 1;
e = e.next;
};
for (idx < alen) {
let bb: i32 = 0;
for (bb < esz) { emitdatawbyte(0u8); bb += 1; };
idx += 1;
};
return true;
};
if (syntax.typeisfloat(au.sub)) {
let isf32: bool = syntax.typeisf32(au.sub);
// Validate.
let idx: i32 = 0;
let e: *syntax.node = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { break; };
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
if (ev != nil) { if (ev.kind == syntax.nkind.N_UN) {
if (ev.op == syntax.tkind.TK_MINUS) {
ev = ev.lhs;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
} else { if (ev.op == syntax.tkind.TK_PLUS) {
ev = ev.lhs;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
};};
};};
if (ev == nil) { return false; };
if (ev.kind != syntax.nkind.N_FLOATLIT) { return false; };
idx += 1;
e = e.next;
};
if (emit_phase == 0) { return true; };
idx = 0;
let last_bits: u64 = 0u64;
let last_neg: bool = false;
let repeat: bool = false;
e = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { repeat = true; break; };
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
let neg: bool = false;
if (ev != nil) { if (ev.kind == syntax.nkind.N_UN) {
if (ev.op == syntax.tkind.TK_MINUS) {
neg = true;
ev = ev.lhs;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
} else { if (ev.op == syntax.tkind.TK_PLUS) {
ev = ev.lhs;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
};};
};};
let bits: u64 = ev.uval;
if (isf32) {
let dv: f64 = *((&bits): *f64);
let fv: f32 = (dv: f32);
let uv: u32 = *((&fv): *u32);
bits = uv: u64;
};
let bb: i32 = 0;
let nb: u64 = bits;
for (bb < esz) {
let byt: u8 = (nb & 255u64): u8;
if (neg) {
if (bb == esz - 1) { byt = byt ^ 128u8; };
};
emitdatawbyte(byt);
nb = nb >> 8u64;
bb += 1;
};
last_bits = bits;
last_neg = neg;
idx += 1;
e = e.next;
};
for (idx < alen) {
if (repeat) {
let bb: i32 = 0;
let nb: u64 = last_bits;
for (bb < esz) {
let byt: u8 = (nb & 255u64): u8;
if (last_neg) {
if (bb == esz - 1) { byt = byt ^ 128u8; };
};
emitdatawbyte(byt);
nb = nb >> 8u64;
bb += 1;
};
} else {
let bb: i32 = 0;
for (bb < esz) { emitdatawbyte(0u8); bb += 1; };
};
idx += 1;
};
return true;
};
// Int-element path — preserved byte-for-byte from the pre-A.3
// emitletdataw in-place arm so bootstrap consumers (u8/i8/u16
// arrays) don't shift.
let idx: i32 = 0;
let e: *syntax.node = rhs.list;
let last: u64 = 0u64;
let repeat: bool = false;
// Validate first.
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { repeat = true; break; };
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
if (ev == nil) { return false; };
if (!foldintliteral(ev, &last)) { return false; };
idx += 1;
e = e.next;
};
if (emit_phase == 0) { return true; };
idx = 0;
last = 0u64;
repeat = false;
e = rhs.list;
let inrepeat: bool = false;
for (idx < alen) {
// #13: explicit elements fold normally; a `...` repeat replays the
// LAST value; the tail PAST the explicit elements (no `...`) is
// ZERO-filled. Pre-fix the default was `last`, so an under-length
// literal (`[4]u64 = [1, 2]`) repeated the last value into the tail
// instead of zero — cstage already zeroes (Hare: unspecified array
// elements are zeroed; the #16-task zero-value ruling); this aligns
// wwstage, a gate-blind cs!=ww divergence at the array-global path.
let v: u64 = 0u64;
if (inrepeat) {
v = last;
} else { if (e != nil) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) {
inrepeat = true;
v = last;
} else {
e = e.next;
v = last;
};
} else {
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
if (!foldintliteral(ev, &v)) { v = 0u64; };
last = v;
e = e.next;
};
};};
let nb: u64 = v;
let bb: i32 = 0;
for (bb < esz) {
emitdatawbyte((nb & 255u64): u8);
nb = nb >> 8u64;
bb += 1;
};
idx += 1;
};
return true;
};
// emitstrarraydata — module-level `let xs: [N]str = […];` static init
// (#18). Mirror of cstage emit_strarray_data. A str element carries a
// ptr→rodata relocation, not just bytes, so it can't ride
// emitarraylitbytes (bytes-only); instead apply the scalar-str-global
// pattern (DATAW header with a zero ptr placeholder + inline LE len,
// then a per-element DATAR) at offset idx*esz. Each strlit was pre-
// interned by letpreintern so its _S_ rodata row exists before this
// row's DATAR references it. Always emits into DATAW (writable): A_DATAR
// requires a DATAW holder, so both `let` and a read-only `def [N]str`
// (#8/GAP-B) park their backing here — the section bit is the reloc-
// holder constraint, not a mutability grant (def immutability stays
// checker-enforced). Returns false when the element type isn't str.
fn emitstrarraydata(c: *cgen, directive: str, name: str, module: str,
arrt: *syntax.tinfo, rhs: *syntax.node) bool = {
let au: *syntax.tinfo = arrt;
au = tichase(au);
if (au == nil) { return false; };
if (au.kind != syntax.tykind.TY_ARRAY) { return false; };
let eu: *syntax.tinfo = au.sub;
eu = tichase(eu);
if (eu == nil) { return false; };
if (eu.kind != syntax.tykind.TY_STR) { return false; };
// #8/GAP-B: a str-element array's backing ALWAYS lives in DATAW
// (writable section), regardless of the caller's let/def directive —
// each element carries an A_DATAR ptr-reloc to its _S_ rodata row, and
// w6a requires a DATAR holder be a DATAW slot (asm.c:362). The passed
// directive ("DATA" for a def, "DATAW" for a let) is therefore IGNORED
// here; the emit below hardcodes DATAW. A `def [N]str` stays immutable
// — the checker rejects writes to a def; DATAW is only the reloc-holder
// placement, not a mutability grant (rule-8 placement detail). Pre-fix
// this gate skipped the def path → no DATA block → w6l undefined
// 'main.C' (#270 lineage; int-def is plain DATA, no holder constraint,
// so it was unaffected).
let esz: i32 = au.sub.size: i32;
let alen: i32 = au.alen: i32;
let last_ev: *syntax.node = nil;
let repeat: bool = false;
let cnt: i32 = 0;
let e: *syntax.node = rhs.list;
for (e != nil && cnt < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { repeat = true; break; };
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
if (ev == nil) { return false; };
if (ev.kind != syntax.nkind.N_STRLIT) { return false; };
last_ev = ev;
cnt += 1;
e = e.next;
};
emitline("DATAW ");
emitfnname(c, name, module);
emitline("(SB),\"");
let idx: i32 = 0;
e = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { break; };
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
let i: i32 = 0;
for (i < 8) { emitdatawbyte(0u8); i += 1; };
let v: u64 = ev.str.len: u64;
i = 0;
for (i < 8) {
emitdatawbyte((v & 255u64): u8);
v = v >> 8u64;
i += 1;
};
i = 16;
for (i < esz) { emitdatawbyte(0u8); i += 1; };
idx += 1;
e = e.next;
};
for (idx < alen) {
let v: u64 = 0u64;
if (repeat && last_ev != nil) { v = last_ev.str.len: u64; };
let i: i32 = 0;
for (i < 8) { emitdatawbyte(0u8); i += 1; };
i = 0;
for (i < 8) {
emitdatawbyte((v & 255u64): u8);
v = v >> 8u64;
i += 1;
};
i = 16;
for (i < esz) { emitdatawbyte(0u8); i += 1; };
idx += 1;
};
emitline("\"\n");
idx = 0;
e = rhs.list;
for (e != nil && idx < alen) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { break; };
};
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
if (ev.str.len > 0) {
let lab: str = internstrlit(c, ev.str);
emitline("DATAR ");
emitfnname(c, name, module);
emitline("+");
emitint((idx * esz): i64);
emitline("(SB),");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB)\n");
};
idx += 1;
e = e.next;
};
for (idx < alen) {
if (repeat && last_ev != nil && last_ev.str.len > 0) {
let lab: str = internstrlit(c, last_ev.str);
emitline("DATAR ");
emitfnname(c, name, module);
emitline("+");
emitint((idx * esz): i64);
emitline("(SB),");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB)\n");
};
idx += 1;
};
return true;
};
// emitarraydata — top-level wrapper. Two-pass validate-then-emit
// avoids partial-byte corruption if the rhs shape can't reduce.
// nil rhs is the "no-rhs zero-init" shape (e.g. `let buf: [N]u8;`
// in lib/strconv/strconv.ww:287, lib/os/os.ww:92, etc.) — emit
// alen*esz zero bytes. This was the implicit pre-A.3 emitletdataw
// behavior (the old loop emitted zeros when `elems` was nil); the
// refactor would have skipped emit entirely without this branch,
// causing `undefined reference to strconv.f64tos_buf` at link.
fn emitarraydata(c: *cgen, directive: str, name: str, module: str,
arrt: *syntax.tinfo, rhs: *syntax.node) bool = {
let au: *syntax.tinfo = arrt;
au = tichase(au);
if (au == nil) { return false; };
if (au.kind != syntax.tykind.TY_ARRAY) { return false; };
if (rhs == nil) {
let total: u64 = arrt.size;
emitline(directive);
emitline(" ");
emitfnname(c, name, module);
emitline("(SB),\"");
let i: u64 = 0u64;
for (i < total) { emitdatawbyte(0u8); i = i + 1u64; };
emitline("\"\n");
return true;
};
// str-element arrays carry per-element ptr relocations — handled
// by the dedicated DATAW+DATAR helper (#18).
if (emitstrarraydata(c, directive, name, module, arrt, rhs)) {
return true;
};
if (!emitarraylitbytes(c, arrt, rhs, 0)) { return false; };
emitline(directive);
emitline(" ");
emitfnname(c, name, module);
emitline("(SB),\"");
emitarraylitbytes(c, arrt, rhs, 1);
emitline("\"\n");
return true;
};
// emitslicedata — module-level `let g: []T = [v0,…];` static init (#10
// part a). Mirror of cstage emit_slice_data. A slice literal needs a
// writable backing holding the k elements, a 24B header { ptr, len, cap
// }, and a DATAR patching the ptr word with the backing's VA. The
// backing rides the emitarraylitbytes choke-point via a synthesized
// [k]T so int/float/struct/nested-array elements reduce exactly as a
// [N]T global's do. Backing symbol = "<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: *syntax.tinfo,
sltnode: *syntax.node, rhs: *syntax.node) void = {
let su: *syntax.tinfo = slt;
su = tichase(su);
// Defensive, mirrors cstage emit_slice_data's
// `if (u == NULL || u->kind != TY_SLICE) return 0` (rule-10): the
// letvarisslice gate already guarantees a slice, so this is
// unreachable — it guards the su.sub deref below if the contract
// is ever violated rather than nil-derefing.
if (su == nil || su.kind != syntax.tykind.TY_SLICE) { return; };
let etype: *syntax.tinfo = su.sub;
let eu: *syntax.tinfo = etype;
eu = tichase(eu);
// Count elements; reject `...` (a slice literal has no target N).
let k: i32 = 0;
let e: *syntax.node = rhs.list;
for (e != nil) {
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) {
let m: str = "emitslicedata: '...' repeat has no target length in a slice literal (#10, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
};
k += 1;
e = e.next;
};
// #117 aggregate-element arm: a slice of inline (str,*fn)-style
// TUPLE rows. The element type-AST node (sltnode.lhs = N_TTUPLE)
// drives the per-element slot classification the node-based
// emittuplerow helpers expect; cstage drives the same off the tuple
// tinfo's params. Bounded to inline N_TTUPLE element types.
let tupnode: *syntax.node = nil;
if (sltnode != nil) { tupnode = sltnode.lhs; };
let istuprow: bool = false;
if (eu != nil && eu.kind == syntax.tykind.TY_TUPLE && tupnode != nil) {
if (tupnode.kind == syntax.nkind.N_TTUPLE) { istuprow = true; };
};
if (istuprow) {
let stride: i32 = etype.size: i32;
// Validate every row before any bytes (two-pass, partial-row
// safe).
let e2: *syntax.node = rhs.list;
for (e2 != nil) {
let row: *syntax.node = e2;
for (row != nil && row.kind == syntax.nkind.N_CAST) { row = row.lhs; };
let bad: bool = false;
if (row == nil) { bad = true; }
else if (row.kind != syntax.nkind.N_TUPLE) { bad = true; }
else if (!tuplerowfoldable(c, tupnode, row)) { bad = true; };
if (bad) {
let m: str = "emitslicedata: tuple-row element not a foldable constant ((str,*fn) rows only; #117, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
e2 = e2.next;
};
// Backing: k rows, bytes (one DATAW) then per-row relocs.
emitline("DATAW ");
emitfnname(c, name, module);
emitline(".d(SB),\"");
e2 = rhs.list;
for (e2 != nil) {
let row: *syntax.node = e2;
for (row != nil && row.kind == syntax.nkind.N_CAST) { row = row.lhs; };
emittuplerowbytes(c, tupnode, row);
e2 = e2.next;
};
emitline("\"\n");
let rowoff: i32 = 0;
e2 = rhs.list;
for (e2 != nil) {
let row: *syntax.node = e2;
for (row != nil && row.kind == syntax.nkind.N_CAST) { row = row.lhs; };
emittuplerowrelocs(c, name, module, true, rowoff, tupnode, row);
rowoff += stride;
e2 = e2.next;
};
} else {
if (eu != nil) {
if (eu.kind == syntax.tykind.TY_STR || eu.kind == syntax.tykind.TY_SLICE
|| eu.kind == syntax.tykind.TY_TAGGED) {
let m: str = "emitslicedata: slice-of-{str,slice,tagged} literal static-init unsupported (#10 follow-up, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
};
let esz: i32 = etype.size: i32;
// Synthesize [k]T to ride the emitarraylitbytes choke-point.
let arrt: *syntax.tinfo = syntax.newtype(syntax.tykind.TY_ARRAY);
arrt.sub = etype;
arrt.alen = k: u64;
arrt.size = (k * esz): u64;
if (!emitarraylitbytes(c, arrt, rhs, 0)) {
let m: str = "emitslicedata: slice-literal element not a foldable constant (#10, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
// Writable backing data.
emitline("DATAW ");
emitfnname(c, name, module);
emitline(".d(SB),\"");
emitarraylitbytes(c, arrt, rhs, 1);
emitline("\"\n");
};
// 24B header: ptr placeholder + LE len + LE cap (both = k). Word
// sizes from the type table (rule 13).
emitline("DATAW ");
emitfnname(c, name, module);
emitline("(SB),\"");
let i: i32 = 0;
let ptrsz: i32 = primtypesize("uintptr"): i32;
for (i < ptrsz) { emitdatawbyte(0u8); i += 1; };
let lensz: i32 = primtypesize("size"): i32;
i = 0;
let kv: u64 = k: u64;
for (i < lensz) {
emitdatawbyte((kv & 255u64): u8);
kv = kv >> 8u64;
i += 1;
};
i = 0;
kv = k: u64;
for (i < lensz) {
emitdatawbyte((kv & 255u64): u8);
kv = kv >> 8u64;
i += 1;
};
emitline("\"\n");
// Patch the ptr word with the backing VA.
emitline("DATAR ");
emitfnname(c, name, module);
emitline("+0(SB),");
emitfnname(c, name, module);
emitline(".d(SB)\n");
};
// emittaggedbytes — raw sz-byte static-init payload for a tagged-union
// value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO
// directive open/close, NO reloc — emits exactly sz bytes via
// emitdatawbyte at the current cursor. Handles zero (nil rhs) + a
// foldable int payload only; a wide (str/slice) payload needs a DATAR the
// raw core cannot place inside an already-open aggregate directive, and a
// struct/non-foldable payload has no scalar form — both return false
// WITHOUT emitting, and the caller loud-rejects (task #30 wide/struct
// sub-item). `tti` is the union tinfo (NAMED-peeled + TY_TAGGED-gated
// internally by flatvariantidxt). emit_phase 0 = validate only; 1 = emit.
// rob's EXTRACT ruling (#19 option A): the scalar wrapper emittaggeddata
// and the aggregate member branches (emitarraylitbytes/emitstructlitbytes)
// share this raw core so a nested tagged member rides the SAME
// (tag,payload) SSoT as a scalar tagged global. Mirror of cstage
// emit_tagged_bytes.
fn emittaggedbytes(c: *cgen, tti: *syntax.tinfo, rhs: *syntax.node, sz: i32, emit_phase: i32) bool = {
if (tti == nil) { return false; };
if (rhs == nil) {
if (emit_phase == 1) {
let zi: i32 = 0;
for (zi < sz) { emitdatawbyte(0u8); zi += 1; };
};
return true;
};
let r: *syntax.node = rhs;
for (r != nil && r.kind == syntax.nkind.N_CAST) { r = r.lhs; };
if (r == nil) { return false; };
let tag: i32 = flatvariantidxt(tti, r.type_: *syntax.tinfo, false);
if (tag < 0) { return false; };
if (nodeisstr(c, r) || nodeisslice(c, r)) { return false; };
let v: u64 = 0u64;
if (!foldintliteral(r, &v)) { return false; };
if (emit_phase == 1) {
let acc: u64 = 0u64;
let i: i32 = 0;
acc = tag: u64;
for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; };
acc = v;
i = 0;
for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; };
i = 16;
for (i < sz) { emitdatawbyte(0u8); i += 1; };
};
return true;
};
// emittaggeddata — module-level `let g: (T0 | T1 | ...) = v;` static
// init (#87). Byte-MIRRORS a runtime LOCAL tagged box (rob §3 SSoT pin):
// tag word at +0 (the const-selected variant index, taggedvariantindex —
// the routine the runtime widen + match dispatch key on), payload at +8,
// zero-padded to the union box size `sz`. int and str-literal variants
// are wired (the Hare-stdlib shapes, ref/hare/time/chrono/utc.ha:44); any
// other variant payload returns false and the caller loud-stops (rule 7) —
// never the pre-#87 silent no-DATA + garbage read. Mirror of cstage
// emit_tagged_data.
fn emittaggeddata(c: *cgen, name: str, module: str, tt: *syntax.node, rhs: *syntax.node, sz: i32) bool = {
if (tt == nil) { return false; };
if (rhs == nil) {
emitline("DATAW ");
emitfnname(c, name, module);
emitline("(SB),\"");
emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 1);
emitline("\"\n");
return true;
};
let r: *syntax.node = rhs;
for (r != nil && r.kind == syntax.nkind.N_CAST) { r = r.lhs; };
if (r == nil) { return false; };
// E8/#35: select the variant via flatvariantidx — the EXACT twin of
// cstage emit_tagged_data's cg_tag_for_variant (cmd/w6c/cgen.c:15472).
// taggedvariantindex adds a str/slice SHAPE fallback (cgenutil.ww:3054)
// that cstage does NOT run at this site: a same-type/subset CAST init
// (`let g: u = true: u;`) stamps the peeled literal's type_ as the
// union itself, so flatvariantidx finds no variant and returns -1,
// matching cstage's loud (caller emits "unsupported variant init",
// cgen.ww:2818). The shape fallback instead picked the first scalar
// variant (tag 0) -> SILENT miscompile (ran the int arm on a bool
// value, the S1 coincidence trap). A bare-literal init (`= true` / `=
// 7`) keeps its concrete/untyped type and still resolves via
// flatvariantidx pass 1 (byte-id with cstage); a str-literal CAST
// (`"hi": u`) keeps its str type and resolves to the str variant too.
// Faithful tag-remap for a cast-init static global = the deferred
// #23/#40 nominal widen feature.
let tag: i32 = flatvariantidx(c, tt, r);
if (tag < 0) { return false; };
let wide: bool = nodeisstr(c, r) || nodeisslice(c, r);
let i: i32 = 0;
let acc: u64 = 0u64;
if (wide) {
if (r.kind != syntax.nkind.N_STRLIT) { return false; };
let lv: u64 = r.str.len: u64;
emitline("DATAW ");
emitfnname(c, name, module);
emitline("(SB),\"");
// tag@0
acc = tag: u64;
i = 0;
for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; };
// ptr placeholder@8
i = 0;
for (i < 8) { emitdatawbyte(0u8); i += 1; };
// len@16
acc = lv;
i = 0;
for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; };
// cap@24 (= len for a static str literal, mirroring the box)
acc = lv;
i = 0;
for (i < 8) { emitdatawbyte((acc & 255u64): u8); acc = acc >> 8u64; i += 1; };
// pad to sz
i = 32;
for (i < sz) { emitdatawbyte(0u8); i += 1; };
emitline("\"\n");
if (r.str.len > 0) {
let lab: str = internstrlit(c, r.str);
emitline("DATAR ");
emitfnname(c, name, module);
emitline("+8(SB),");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB)\n");
};
return true;
};
// int/zero payload via the shared raw core; validate (phase 0) BEFORE
// opening the directive so a non-foldable rhs returns false without
// leaving a half-written DATAW.
if (!emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 0)) { return false; };
emitline("DATAW ");
emitfnname(c, name, module);
emitline("(SB),\"");
emittaggedbytes(c, tt.type_: *syntax.tinfo, rhs, sz, 1);
emitline("\"\n");
return true;
};
// nodefnptr — true if `ev` (casts already peeled by the caller) is the
// address-of a top-level fn (`&f`). The detect-half of the FIRST &fn→DATAR
// reloc machinery (#117 slice-row + #119 scalar-global); mirrors the
// address-of-fn codegen arm (fnretlookup at the N_UN TK_AMP ident,
// cgenexpr.ww). The reloc target symbol is emitted via emitfnname at the
// call site (cstage node_fnptr_sym returns the mangled string directly).
fn nodefnptr(c: *cgen, ev: *syntax.node) bool = {
if (ev == nil) { return false; };
if (ev.kind != syntax.nkind.N_UN) { return false; };
if (ev.op != syntax.tkind.TK_AMP) { return false; };
let opnd: *syntax.node = ev.lhs;
if (opnd == nil) { return false; };
// #124: a cross-module `&mod.fn` — opnd is an N_DOT whose base is an
// SK_USE module qualifier (not a local / let / def), and whose leaf
// resolves to a fn in that module. Mangle via the module ident (not
// curmod) at the emit sites so the reloc targets the same TEXT symbol
// the runtime `&mod.fn` emits (cgenexpr.ww N_DOT addr-of arm). The
// N_DOT arm of the #117/#119 reloc helper.
if (opnd.kind == syntax.nkind.N_DOT) {
if (opnd.lhs == nil) { return false; };
if (opnd.lhs.kind != syntax.nkind.N_IDENT) { return false; };
let basenm: str = opnd.lhs.str;
if (localfindnode(c, basenm) != nil) { return false; };
if (isletvar(c, basenm)) { return false; };
if (deflookup(c, basenm)) { return false; };
if (fnretlookupmod(c, opnd.str, basenm) == nil) { return false; };
return true;
};
if (opnd.kind != syntax.nkind.N_IDENT) { return false; };
// #14 (F7-c7): type-keyed, mirroring cstage node_fnptr_sym
// (type_chase_named(opnd->type)->kind == TY_FN, cmd/w6c/cgen.c:15542-
// 15543). The prior name-keyed `fnretlookup(opnd.str)` matched a fn
// LEAF NAME even when the operand actually resolved to a same-named
// global/local VALUE — so `&g` for an `*i64` global `g` colliding with
// a fn `g` (e.g. a `mod.f` fn vs a `f` global) baked the fn's TEXT addr
// into the scalar slot (ww runs rc=42; cs fails loud at w6l). Reading
// the stamped operand type distinguishes the bare fn rvalue (TY_FN, the
// #34 fn-rvalue stamp) from a value ident, closing the leaf-name
// collision by construction. (F12 name-keyed overlap noted in the F7
// spec — same predicate-to-stamp shape; fixed once here.)
let ou: *syntax.tinfo = tichase(opnd.type_: *syntax.tinfo);
if (ou == nil) { return false; };
return ou.kind == syntax.tykind.TY_FN;
};
// tuplerowfoldable — validate every cast-peeled element of `rhs` (an
// N_TUPLE) reduces to a static row: an int literal (foldintliteral) or a
// str literal in a str/slice slot. A tagged element slot has no
// static-init shape (tag word + payload widening) — reject so the caller
// loud-stops (#22a, rule 7); pre-guard an int init would have emitted one
// 8B word into the 16B+ box (silent layout skew). The validate twin of
// emittuplerowbytes / emittuplerowrelocs; two-pass keeps a partial row
// out of the output (emitarraydata precedent). Factored from emittupledata
// so the slice-of-tuple backing (#117) shares it. Mirror of cstage
// tuple_row_foldable.
fn tuplerowfoldable(c: *cgen, tt: *syntax.node, rhs: *syntax.node) bool = {
let tp: *syntax.node = tt.list;
let e: *syntax.node = rhs.list;
for (e != nil) {
let et: *syntax.node = nil;
if (tp != nil) { et = tp.lhs; };
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
if (ev == nil) { return false; };
{
let eti: *syntax.tinfo = nil;
if (et != nil) { eti = et.type_: *syntax.tinfo; };
eti = tichase(eti);
if (eti != nil && eti.kind == syntax.tykind.TY_TAGGED) {
return false;
};
};
let wide: bool = isstrtype(c, et) || isslicetype(c, et);
if (wide) {
if (ev.kind != syntax.nkind.N_STRLIT) { return false; };
} else if (nodefnptr(c, ev)) {
// #117: a `&fn` element folds to an 8B reloc slot.
} else {
let v: u64 = 0u64;
if (!foldintliteral(ev, &v)) { return false; };
};
e = e.next;
if (tp != nil) { tp = tp.next; };
};
return true;
};
// emittuplerowbytes — the row's element bytes, concatenated, into the
// currently-open DATAW quoted string (no DATAW wrapper, no sym). Slot
// layout (C-t0): a scalar element is one 8B LE word; a str/slice element
// its 24B header slot (8 zero ptr placeholder + LE len + 8 zero cap).
// Caller has already proven the row foldable. Mirror of cstage
// emit_tuple_row_bytes.
fn emittuplerowbytes(c: *cgen, tt: *syntax.node, rhs: *syntax.node) void = {
let tp: *syntax.node = tt.list;
let e: *syntax.node = rhs.list;
for (e != nil) {
let et: *syntax.node = nil;
if (tp != nil) { et = tp.lhs; };
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
let wide: bool = isstrtype(c, et) || isslicetype(c, et);
if (wide) {
let i: i32 = 0;
for (i < 8) { emitdatawbyte(0u8); i += 1; };
let lv: u64 = ev.str.len: u64;
i = 0;
for (i < 8) {
emitdatawbyte((lv & 255u64): u8);
lv = lv >> 8u64;
i += 1;
};
i = 16;
let ssz: i32 = primtypesize("str"): i32;
for (i < ssz) { emitdatawbyte(0u8); i += 1; };
} else if (nodefnptr(c, ev)) {
// #117: a `&fn` element is an 8B zero ptr placeholder;
// the reloc is patched in emittuplerowrelocs.
let i: i32 = 0;
for (i < 8) { emitdatawbyte(0u8); i += 1; };
} else {
let v: u64 = 0u64;
foldintliteral(ev, &v);
let i: i32 = 0;
let nv: u64 = v;
for (i < 8) {
emitdatawbyte((nv & 255u64): u8);
nv = nv >> 8u64;
i += 1;
};
};
e = e.next;
if (tp != nil) { tp = tp.next; };
};
};
// emittuplerowrelocs — the row's DATAR ptr patches, at backing-relative
// <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: *syntax.node, rhs: *syntax.node) void = {
let foff: i32 = rowoff;
let tp: *syntax.node = tt.list;
let e: *syntax.node = rhs.list;
for (e != nil) {
let et: *syntax.node = nil;
if (tp != nil) { et = tp.lhs; };
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.nkind.N_CAST) { ev = ev.lhs; };
let wide: bool = isstrtype(c, et) || isslicetype(c, et);
if (wide) {
if (ev.str.len > 0) {
let lab: str = internstrlit(c, ev.str);
emitline("DATAR ");
emitfnname(c, name, module);
if (backing) { emitline(".d"); };
emitline("+");
emitint(foff: i64);
emitline("(SB),");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB)\n");
};
foff += (tyslicesize(): i32);
} else {
// #117: the `&fn` element's reloc — the FIRST &fn→DATAR
// in the emitter; patches the 8B slot at holder+foff
// with the fn's TEXT VA via emitfnname.
if (nodefnptr(c, ev)) {
emitline("DATAR ");
emitfnname(c, name, module);
if (backing) { emitline(".d"); };
emitline("+");
emitint(foff: i64);
emitline("(SB),");
// #124: a cross-module `&mod.fn` operand mangles
// the leaf with the MODULE ident; same-module `&fn`
// stays on curmod.
if (ev.lhs.kind == syntax.nkind.N_DOT) {
emitfnname(c, ev.lhs.str, usehint(c, ev.lhs.lhs.str));
} else {
emitfnname(c, ev.lhs.str, c.curmod);
};
emitline("(SB)\n");
};
// #22: slot stride via the accessor (tagged is
// rejected upstream; non-wide is 8 today — keeps the
// stride on the accessor scale).
foff += tupeslotn(et);
};
e = e.next;
if (tp != nil) { tp = tp.next; };
};
};
// emittupledata — module-level `let g: (T0, T1, ...) = (v0, ...);`
// static init (C-t3, #48). One slot-laid DATAW row (+ DATAR str-element
// ptr patches) via the backing-relative emittuplerow helpers; rhs == nil
// zero-inits. Unsupported element inits return false and the caller
// loud-stops (rule 7 — pre-C-t3 the whole definition was SILENTLY skipped
// and reads saw garbage). Mirror of cstage emit_tuple_data.
fn emittupledata(c: *cgen, name: str, module: str, tt: *syntax.node, rhs: *syntax.node) bool = {
if (tt == nil) { return false; };
if (rhs == nil) {
// #22: slot-sum via the accessor so the zero-fill matches
// the checker size (cstage zero-emits u->size).
let zsz: i32 = 0;
let p0: *syntax.node = tt.list;
for (p0 != nil) {
zsz += tupeslotn(p0.lhs);
p0 = p0.next;
};
emitline("DATAW ");
emitfnname(c, name, module);
emitline("(SB),\"");
let zi: i32 = 0;
for (zi < zsz) { emitdatawbyte(0u8); zi += 1; };
emitline("\"\n");
return true;
};
if (rhs.kind != syntax.nkind.N_TUPLE) { return false; };
if (!tuplerowfoldable(c, tt, rhs)) { return false; };
emitline("DATAW ");
emitfnname(c, name, module);
emitline("(SB),\"");
emittuplerowbytes(c, tt, rhs);
emitline("\"\n");
emittuplerowrelocs(c, name, module, false, 0, tt, rhs);
return true;
};
fn emitletdataw(c: *cgen, file: *syntax.node) void = {
let d: *syntax.node = file.list;
for (d != nil) {
// #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is
// initializer-less; emitting a DATAW for it would DUPLICATE the
// definition that lives in the dep's own .o → link collision.
// Gate on the explicit imported flag (NOT no-rhs: a package's OWN
// init-less let must still zero-init). Symmetric with M2's
// producer imported==0 filter — same predicate both ways.
if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; };
if (d.kind == syntax.nkind.N_LET) {
let nm: str = d.str;
if (nm.len > 0) {
let sz: i32 = letemitsize(c, d);
let issg: bool = letvarisstruct(c, nm);
let fsz: i32 = letvarisfloat(c, nm);
// g-fold #77: ONE chase at the dispatch entry. The
// array gates below keyed on the N_TARRAY tnode —
// an alias-typed global's N_TNAME matched no arm
// and the skip-policy ate the decl: no DATAW,
// undefined reference at link. The str/float/
// struct/slice/tuple gates already alias-walk
// (letvaris* / the tlt tnode walk) and stay put.
let dti: *syntax.tinfo = nil;
if (d.lhs != nil) { dti = tichase(d.lhs.type_: *syntax.tinfo); };
// C-t3 (#48): tuple global — slot-laid DATAW
// row (+ DATAR ptr patches for str elements)
// via emittupledata. Unsupported element
// inits die LOUD; pre-C-t3 the definition was
// silently skipped (no DATA, no diagnostic)
// and reads saw garbage. The istup gate also
// keeps a tuple out of the sz==8 / str-size
// arms below (a 24B tuple == str size).
let tlt: *syntax.node = d.lhs;
for (tlt != nil && tlt.kind == syntax.nkind.N_TNAME) {
tlt = aliaslookup(c, tlt.str);
};
let istup: bool = false;
if (tlt != nil) {
if (tlt.kind == syntax.nkind.N_TTUPLE) {
istup = true;
};
};
if (istup) {
let tr: *syntax.node = d.rhs;
for (tr != nil) {
if (tr.kind != syntax.nkind.N_CAST) { break; };
tr = tr.lhs;
};
if (!emittupledata(c, nm, d.nmod, tlt, tr)) {
let mtg: str = "global tuple let: unsupported element init (int/str literals only; rule 7)\n";
os.write(2, mtg.ptr, mtg.len: u64);
os.exit(1);
};
};
// #87: non-nullable tagged-union global — emit the box
// mirroring the runtime local (tag + payload). letemitsize
// keeps nullable at 0 so the (*T|void) one-word fold stays
// on the 8B scalar arm below. The istagged gate also keeps
// a tagged box (size can equal str/slice size) out of those.
let istagged: bool = false;
if (tlt != nil) {
if (tlt.kind == syntax.nkind.N_TTAGGED) {
if (!isnullabletype(tlt)) { istagged = true; };
};
};
if (istagged) {
if (!emittaggeddata(c, nm, d.nmod, tlt, d.rhs, sz)) {
let mtg: str = "global tagged let: unsupported variant init (int/str literal only; rule 7)\n";
os.write(2, mtg.ptr, mtg.len: u64);
os.exit(1);
};
};
if (fsz > 0) {
// Float global: routes through the
// emitfloatlitdata SSoT helper, shared
// with emitdefconstants's float arm
// (#129 Phase A.1, rule-12). Bare-call
// discards the bool return (mirrors
// cgen.ww:723 fmt.fprintln pattern).
emitfloatlitdata(c, "DATAW", nm, d.nmod,
fsz, d.rhs);
};
// #129 A.2: struct-typed let with N_STRUCTLIT rhs
// routes through the emitstructdata SSoT helper.
// Pre-A.2 emitletdataw had no struct arm, so the
// declaration fell out of the .data section and
// the link surfaced an undefined-symbol error.
if (issg) {
let r: *syntax.node = d.rhs;
if (r != nil) {
if (r.kind == syntax.nkind.N_STRUCTLIT) {
let st: *syntax.tinfo = d.lhs.type_: *syntax.tinfo;
emitstructdata(c, "DATAW", nm,
d.nmod, st, r);
};
};
};
// Skip the scalar 8B path when the global is a
// fixed-size array that just happens to sum to 8
// bytes (e.g. [4]u16, [8]u8) — the array path
// below handles it and the duplicate DATAW would
// otherwise differ across stages on user code.
let isarr8: bool = false;
if (dti != nil) {
if (dti.kind == syntax.tykind.TY_ARRAY) { isarr8 = true; };
};
if (sz == 8 && !issg && fsz == 0 && !isarr8 && !istup && !istagged) {
let v: u64 = 0u64;
let ok: bool = true;
let fnp: bool = false;
let r: *syntax.node = nil;
if (d.rhs != nil) {
r = d.rhs;
for (r != nil) {
if (r.kind != syntax.nkind.N_CAST) { break; };
r = r.lhs;
};
// Same helper as emitdefconstants (#24)
// — widens the gate so N_UN over an
// int leaf folds. `let x: i8 = -1i8;`
// arrives as N_UN(TK_MINUS, N_INTLIT)
// after the typed-AST cast peel.
// #119: a scalar `&fn` global — the &fn->DATAR
// reloc (the #117 helper at its second consumer).
if (nodefnptr(c, r)) { fnp = true; }
else { ok = foldintliteral(r, &v); };
};
if (fnp) {
emitline("DATAW ");
emitfnname(c, nm, d.nmod);
emitline("(SB),\"");
let zi: i32 = 0;
for (zi < 8) { emitdatawbyte(0u8); zi += 1; };
emitline("\"\n");
emitline("DATAR ");
emitfnname(c, nm, d.nmod);
emitline("+0(SB),");
// #124: cross-module `&mod.fn` mangles the
// leaf with the MODULE ident; same-module `&fn`
// stays on curmod.
if (r.lhs.kind == syntax.nkind.N_DOT) {
emitfnname(c, r.lhs.str, usehint(c, r.lhs.lhs.str));
} else {
emitfnname(c, r.lhs.str, c.curmod);
};
emitline("(SB)\n");
} else if (ok) {
emitline("DATAW ");
emitfnname(c, nm, d.nmod);
emitline("(SB),\"");
let i: i32 = 0;
let n: u64 = v;
for (i < 8) {
let b: u8 = (n & 255u64): u8;
n = n >> 8u64;
emitdatawbyte(b);
i += 1;
};
emitline("\"\n");
};
};
// #12: this arm is SIZE-keyed (sz == 24), not type-keyed,
// so a no-init array global whose bytes sum to str width
// (e.g. `let g: [3]u64;`) matched here AND the TY_ARRAY arm
// below → two identical `DATAW g` rows (cstage is type-
// keyed via let_isstr and emits one). Exclude arrays — the
// emitarraydata path owns them — mirroring the existing
// isarr8 guard on the sz==8 scalar arm.
if (sz == primtypesize("str"): i32 && !issg && !istup && !istagged && !isarr8 && !letvarisslice(c, nm)) {
let r: *syntax.node = d.rhs;
for (r != nil) {
if (r.kind != syntax.nkind.N_CAST) { break; };
r = r.lhs;
};
// str-literal init (non-empty): emit
// the 16B payload as 8 placeholder zero
// bytes + 8 LE bytes of length, then a
// DATAR reloc to patch the ptr half with
// the strlit's runtime VA.
let strlitinit: bool = false;
if (r != nil) {
if (r.kind == syntax.nkind.N_STRLIT) {
if (r.str.len > 0) { strlitinit = true; };
};
};
if (strlitinit) {
let lab: str = internstrlit(c, r.str);
let v: u64 = r.str.len: u64;
emitline("DATAW ");
emitfnname(c, nm, d.nmod);
emitline("(SB),\"");
let i: i32 = 0;
for (i < 8) { emitdatawbyte(0u8); i += 1; };
i = 0;
let nv: u64 = v;
for (i < 8) {
emitdatawbyte((nv & 255u64): u8);
nv = nv >> 8u64;
i += 1;
};
emitline("\"\n");
emitline("DATAR ");
emitfnname(c, nm, d.nmod);
emitline("+0(SB),");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB)\n");
} else {
// zero-init: accept no rhs, nil,
// or empty strlit.
let ok: bool = true;
if (d.rhs != nil) {
ok = false;
if (r != nil) {
if (r.kind == syntax.nkind.N_NIL) { ok = true; };
if (r.kind == syntax.nkind.N_STRLIT) {
if (r.str.len == 0) { ok = true; };
};
};
};
if (ok) {
emitline("DATAW ");
emitfnname(c, nm, d.nmod);
emitline("(SB),\"");
let i: i32 = 0;
let szstr: i32 = primtypesize("str"): i32;
for (i < szstr) {
emitdatawbyte(0u8);
i += 1;
};
emitline("\"\n");
};
};
};
if (sz == tyslicesize(): i32 && !issg && !istagged && letvarisslice(c, nm)) {
let r: *syntax.node = d.rhs;
for (r != nil) {
if (r.kind != syntax.nkind.N_CAST) { break; };
r = r.lhs;
};
// #10 part a: slice-literal static init
// routes through emitslicedata (header +
// writable backing + DATAR). Loud-stops on
// the deferred element kinds and the read-
// only/`...` shapes (rule 7).
if (r != nil && r.kind == syntax.nkind.N_ARRLIT) {
emitslicedata(c, nm, d.nmod,
d.lhs.type_: *syntax.tinfo, d.lhs, r);
} else {
// zero-init: accept no rhs or nil.
// Any other rhs is skipped →
// undefined symbol at link.
let ok: bool = true;
if (d.rhs != nil) {
ok = false;
if (r != nil) {
if (r.kind == syntax.nkind.N_NIL) { ok = true; };
};
};
if (ok) {
emitline("DATAW ");
emitfnname(c, nm, d.nmod);
emitline("(SB),\"");
let i: i32 = 0;
let szsl: i32 = tyslicesize(): i32;
for (i < szsl) {
emitdatawbyte(0u8);
i += 1;
};
emitline("\"\n");
};
};
};
// Struct globals — any size, zero-init only.
// A struct literal init isn't compile-time
// evaluated yet; skip and the link will surface
// an undefined-symbol error if referenced.
// #254: the zero-fill byte count comes from the
// type table's tinfo.size (cstage cg_let_emit_size
// returns u->size, cgen.c:978), NOT letemitsize/
// si.totsize — registerstruct rounds the nested
// value-struct field's slot to 8, so a sub-8 outer
// struct (ABI 4) over-emitted DATAW 8 bytes vs
// cstage's 4. registerstruct / fieldsize / frame
// slot-padding stay UNTOUCHED (field offsets).
if (issg) {
if (d.rhs == nil) {
let zsz: i32 = sz;
if (dti != nil) { zsz = dti.size: i32; };
emitline("DATAW ");
emitfnname(c, nm, d.nmod);
emitline("(SB),\"");
let i: i32 = 0;
for (i < zsz) {
emitdatawbyte(0u8);
i += 1;
};
emitline("\"\n");
};
};
// #129 A.3: array global routes through the
// emitarraydata SSoT helper. Int-elem path is
// byte-for-byte preserved (bootstrap consumers in
// lib/os, lib/bufio, lib/strings, lib/encoding/
// utf8, lib/strconv/stof_data don't shift). Float/
// struct elements gain emit via element-kind
// dispatch. Helper validates pre-emit so partial
// fold-failures don't corrupt the DATA literal.
// No-rhs arrays (e.g. `let buf: [N]u8;`) go through
// the same helper with rhs=nil → zero-fill branch.
if (dti != nil) {
if (dti.kind == syntax.tykind.TY_ARRAY) {
let rh: *syntax.node = d.rhs;
let route: bool = false;
if (rh == nil) { route = true; };
if (rh != nil) {
if (rh.kind == syntax.nkind.N_ARRLIT) {
route = true;
};
};
// #15: a zero-length array (`[0]T`)
// has no bytes — cstage emits no DATA
// row; wwstage's unguarded emit produced
// a spurious `DATAW name(SB),""`. sz
// (letemitsize, cgen.ww:2758) is 0 for
// [0]T → skip. (Non-empty [N>0] arrays
// keep sz>0.)
if (route && sz > 0) {
emitarraydata(c, "DATAW", nm,
d.nmod, dti, rh);
};
};
};
};
};
d = d.next;
};
};
// emitdefconstants — DATA directive per top-level fold-to-literal
// `def`. 8 bytes little-endian to match what the C cgen emits.
// foldintliteral gates: int/rune literal, true/false/nil, and a
// unary +/-/~ over the same. `def NEG: i32 = -100;` arrives as
// N_UN(TK_MINUS, N_INTLIT) — the unary peel is exactly what the
// gate is for.
fn emitdefconstants(c: *cgen, file: *syntax.node) void = {
let d: *syntax.node = file.list;
for (d != nil) {
// #22 M3: a `.wwi` dep def with DATA storage (int-fold / float /
// struct / array) must NOT re-emit — the dep's own .o owns the
// symbol. Str defs are inline-spliced (never emitted here), so
// they need no gate; the def registry stays populated for imported
// decls so the target's LOAD paths still resolve the extern.
if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; };
if (d.kind == syntax.nkind.N_DEF) {
let r: *syntax.node = d.rhs;
let v: u64 = 0u64;
let ok: bool = false;
if (r != nil) {
ok = foldintliteral(r, &v);
};
if (!ok) {
// Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT))
// rhs: route through the same SSoT helper as
// emitletdataw's float arm. Pre-#129 this fell
// through to no-emit + undef-ref at link. Type-size
// walk mirrors letvarisfloat (#129 Phase A.1).
let dfsz: i32 = 0;
let dt: *syntax.node = d.lhs;
for (dt != nil) {
if (dt.kind != syntax.nkind.N_TNAME) { dfsz = 0; break; };
let fsz: i32 = letfloatprim(dt.str);
if (fsz > 0) { dfsz = fsz; break; };
let nx: *syntax.node = aliaslookup(c, dt.str);
if (nx == nil) { dfsz = 0; break; };
dt = nx;
};
if (dfsz > 0) {
emitfloatlitdata(c, "DATA", d.str,
d.nmod, dfsz, d.rhs);
} else {
// #129 A.2: struct-typed def with N_STRUCTLIT
// rhs. The checker stamps d.lhs.type_ with the
// struct's tinfo; helper peels TY_NAMED. Parallel
// to emitletdataw struct arm; uses DATA (read-
// only) directive.
if (r != nil) { if (r.kind == syntax.nkind.N_STRUCTLIT) {
let st: *syntax.tinfo = d.lhs.type_: *syntax.tinfo;
let su: *syntax.tinfo = st;
su = tichase(su);
if (su != nil) {
if (su.kind == syntax.tykind.TY_STRUCT) {
emitstructdata(c, "DATA",
d.str, d.nmod, st, r);
};
};
};};
// #129 A.3: array-typed def with N_ARRLIT rhs.
// Parallel to emitletdataw array arm; uses DATA.
if (r != nil) { if (r.kind == syntax.nkind.N_ARRLIT) {
let at: *syntax.tinfo = d.lhs.type_: *syntax.tinfo;
let au: *syntax.tinfo = at;
au = tichase(au);
if (au != nil) {
if (au.kind == syntax.tykind.TY_ARRAY) {
emitarraydata(c, "DATA",
d.str, d.nmod, at, r);
};
// #10: a read-only `def g: []T = [...]`
// slice literal can't carry the ptr reloc
// emitslicedata needs (DATAR holder must be
// DATAW, w6a asm.c:362). Loud-stop, never
// silent no-emit.
if (au.kind == syntax.tykind.TY_SLICE) {
let m: str = "emitdefconstants: module-level slice-literal init needs a writable `let` (DATAR holder must be DATAW, w6a asm.c:362); read-only `def` unsupported (#10, rule 7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
};
};};
};
};
if (ok) {
// #127: route DATA-emit through the SAME emitsymname
// SSoT that LOAD/CALL sites use. Replaces the prior
// 8-line d.exported/d.nmod prefix logic with a single
// modlookup-based mangle, removing duplicate logic
// (rule-12 sea-of-stars). Mirrors cstage emit_defs at
// cmd/w6c/cgen.c:8494 (mod_mangle). Bootstrap-neutral
// post-90d31c5 (the PATH_MAX duplicate-def consumer
// that motivated the divergence is gone), so the asm
// surface is unchanged on the corpus.
emitline("DATA ");
emitfnname(c, d.str, d.nmod);
emitline("(SB),\"");
let i: i32 = 0;
let n: u64 = v;
for (i < 8) {
let b: u8 = (n & 255u64): u8;
n = n >> 8u64;
// C emit_defs only special-cases " and \;
// every other non-printable goes as \xHH.
if (b == 34u8) { emitline("\\\""); }
else { if (b == 92u8) { emitline("\\\\"); }
else {
if (b < 32u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
emitbytes( bb.ptr, 2u64);
} else {
if (b >= 127u8) {
emitline("\\x");
let hi: u8 = b >> 4u8;
let lo: u8 = b & 15u8;
let bb: [2]u8;
if (hi < 10u8) { bb[0] = hi + 48u8; }
else { bb[0] = (hi - 10u8) + 97u8; };
if (lo < 10u8) { bb[1] = lo + 48u8; }
else { bb[1] = (lo - 10u8) + 97u8; };
emitbytes( bb.ptr, 2u64);
} else {
let bb: [1]u8;
bb[0] = b;
emitbytes( bb.ptr, 1u64);
};
};
};};
i += 1;
};
emitline("\"\n");
};
};
d = d.next;
};
};
// 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: *syntax.node,
params: *syntax.node,
frnext: *fnret,
};
fn collectfnrets(c: *cgen, file: *syntax.node) void = {
c.fnrets = nil;
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_FNDECL) {
let f: *fnret = alloc(fnret{fname=d.str, fmod=d.nmod, rtype=d.lhs, params=d.list, frnext=c.fnrets})!;
c.fnrets = f;
};
d = d.next;
};
};
// fnretlookup — declared return-type node for a fn by leaf name, or nil
// if the name isn't a registered fn. Same-module-first walk before the
// head-walk fallback. Eighth and final leaf of the trio graduation (#4e)
// mirroring aliaslookup (#27), fnret/fnparamslookupmod (#28/#31),
// enum/struct/deflookup (#4a/#4b/#4c), fnparamslookup (#4d): without
// the prefer pass a bare-leaf `foo()` call site in module M (N_IDENT
// callee) silently picks another module's same-leaf `foo` from the
// head of c.fnrets, then every downstream consumer keying on the
// return type (str-pair shuffle, tagged-union ABI, tuple destructure,
// float ABI, sret slot sizing, fn-rvalue LEAQ, slice flow) fires
// against the wrong-module shape.
// fnretlookup — the called fn's declared return type, keyed by NAME
// (same-module-first, then first leaf match). The receive sites that
// re-derive a call's result SHAPE from this (cglet tagged-store,
// cgwidentaggedstore scalar-vs-tagged classify, tuple/sret/unsigned
// arms) are correct only when the leaf name uniquely picks the callee.
//
// #211 (gate-blind cgen divergence, sibling of the #208 checker fix): a
// VALUE-receiver fn-pointer FIELD call `s.f(...)` reaches the receive
// sites keyed on the field leaf `f` with the receiver VARIABLE name as
// the "module" (not a real module), so this lookup mis-binds a same-named
// GLOBAL fn. When that global's register shape differs from the field's
// (scalar global vs tagged field), the slot is stored with the wrong ABI
// shape → cstage≠wwstage asm, silent miscompile. The sound fix derives
// the result from the FIELD's fn type / the checker-stamped n.type_ (as
// cstage does, cmd/wcc/check.c:1378-1433), not by leaf name. NO guard is
// added here: same-shape leaf collisions resolve by name legitimately
// today, and a discriminating guard would need the shape-compare that IS
// the fix. Masked until #208 landed (the checker rejected the shape
// before cgen ran). test/wcc/782 pins the cstage-correct runtime
// (cstage-only) and graduates to STAGE_WW on #211 close.
fn fnretlookup(c: *cgen, name: str) *syntax.node = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (syntax.streq(f.fname, name)) {
if (syntax.streq(f.fmod, c.curmod)) { return f.rtype; };
};
f = f.frnext;
};
f = c.fnrets;
for (f != nil) {
if (syntax.streq(f.fname, name)) { return f.rtype; };
f = f.frnext;
};
return nil;
};
// fnretlookupmod — same-module-first walk. Module-qualified `mod.fn(...)`
// callees route here so a leaf collision (same fn name exported from
// multiple modules) resolves to the explicit module. Falls back to the
// first leaf match if no matching module is registered. Mirror of
// fnparamslookupmod (#28); without this, matchscrutt's N_DOT branch
// picks the last-declared `next` regardless of qualifier, so a 4-arm
// `match (utf8.next(d))` inside a `fn next() (rune | done)` resolves
// the scrutinee tagged type to `(rune | done)` — flatvariantidx then
// can't see arms 2/3 and collapses them onto tag 0 (task #31).
fn fnretlookupmod(c: *cgen, name: str, mod: str) *syntax.node = {
// M1 #22 (#199b): the qualifier may be the import ALIAS the user
// wrote (`utf8`); fn decls register f.fmod under the dotted import
// PATH (`encoding.utf8`). Map alias->path so a nested-package callee
// matches its own module instead of falling back to the name-only
// pass — which a same-leaf caller-module fn (e.g. strings.next vs
// utf8.next) otherwise wins, resolving a match scrutinee to the
// caller's union and collapsing arms 2+. usehint is idempotent on a
// path / c.curmod (returns the input when no `use` matches), so the
// already-mapped callers (cgenexpr.ww:4309/5229) and the bare-ident
// c.curmod callers are unaffected. The choke-point twin of the
// struct/alias/enum usehint splitters (cgen.ww:128/319,
// cgenutil.ww:2173) — closes the whole fnret class by construction.
let mk: str = usehint(c, mod);
if (mk.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (syntax.streq(f.fname, name)) {
if (syntax.streq(f.fmod, mk)) { return f.rtype; };
};
f = f.frnext;
};
};
return fnretlookup(c, name);
};
// fnparamslookup — head of the declared param-list for a fn, or nil
// if the name isn't a registered fn. Same-module-first walk before the
// head-walk fallback. Trio-leaf graduation (#4d) mirroring aliaslookup
// (#27), fnret/fnparamslookupmod (#28/#31), enum/struct/deflookup
// (#4a/#4b/#4c): without the prefer pass a bare-leaf `foo(x)` call in
// module M (callee N_IDENT) silently picks another module's same-leaf
// `foo` from the head of c.fnrets, then pushargsrev's widening
// detection fires (or doesn't) against the wrong param-type — `foo(7)`
// against a same-leaf `(i32 | void)` param re-layouts 7 into a 2-word
// tagged slot vs the same-module `i32` param's single push.
fn fnparamslookup(c: *cgen, name: str) *syntax.node = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (syntax.streq(f.fname, name)) {
if (syntax.streq(f.fmod, c.curmod)) { return f.params; };
};
f = f.frnext;
};
f = c.fnrets;
for (f != nil) {
if (syntax.streq(f.fname, name)) { return f.params; };
f = f.frnext;
};
return nil;
};
// samemodfn — true iff `name` is registered as a fn in c.curmod. Used
// by cgcall to suppress the bare-name Hare-style builtins (`alloc(x)`,
// future free/append/len audits) when the current module declares its
// own decl by that name. Mirrors cstage's same-module check at
// cmd/wcc/check.c (alloc gate, task #23) — `scope_lookup_prefer` over
// the flat scope would also match `use os;`-imported decls in a primary,
// suppressing the builtin spuriously; the same-module-tag filter here
// (and `c.curmod && ...` on the cstage side) keeps the gate strict.
fn samemodfn(c: *cgen, name: str) bool = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (syntax.streq(f.fname, name)) {
if (syntax.streq(f.fmod, c.curmod)) { return true; };
};
f = f.frnext;
};
return false;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves
// to the explicit module. Falls back to the first leaf match if no
// matching module is registered — mirrors aliaslookup's two-pass shape
// (cgen.ww:75, fixed in #27).
fn fnparamslookupmod(c: *cgen, name: str, mod: str) *syntax.node = {
// M1 #22 (#199b): map import alias -> dotted path, identical to
// fnretlookupmod (the param-side twin). usehint is idempotent on a
// path / c.curmod so existing callers are unaffected.
let mk: str = usehint(c, mod);
if (mk.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (syntax.streq(f.fname, name)) {
if (syntax.streq(f.fmod, mk)) { return f.params; };
};
f = f.frnext;
};
};
return fnparamslookup(c, name);
};
// ---- def-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: *syntax.node,
dtnode: *syntax.node, // #129 A.2: type-spec node (d.lhs); needed for
// struct-def structinfo lookup at the cgdot
// LOAD-side widening site.
dnext: *defent,
};
fn collectdefs(c: *cgen, file: *syntax.node) void = {
c.defs = nil;
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_DEF) {
let e: *defent = alloc(defent{dname=d.str, dmod=d.nmod, drhs=d.rhs, dtnode=d.lhs, dnext=c.defs})!;
c.defs = e;
};
d = d.next;
};
};
// Same-module-first walk, then any. Trio-leaf graduation mirroring
// aliaslookup (#27) and enum/structlookup (#4a/#4b): bool answer is
// invariant either way, but the structural shape mirrors deflookuprhs
// where the entry's drhs IS module-sensitive.
fn deflookup(c: *cgen, name: str) bool = {
let e: *defent = c.defs;
for (e != nil) {
if (syntax.streq(e.dname, name)) {
if (syntax.streq(e.dmod, c.curmod)) { return true; };
};
e = e.dnext;
};
e = c.defs;
for (e != nil) {
if (syntax.streq(e.dname, name)) { return true; };
e = e.dnext;
};
return false;
};
// Returns the rhs init node for a top-level `def`, or nil if `name`
// doesn't name a def. Same-module-first walk: without the prefer pass
// `MSG.ptr`/`MSG.len` in module M can collapse onto another module's
// same-leaf `def MSG: str = ...` sitting at the head of c.defs and
// inline the wrong strlit. Used by cgdot to inline `.ptr`/`.len` on
// `def NAME: str = "..."` — those aren't laid out in memory.
fn deflookuprhs(c: *cgen, name: str) *syntax.node = {
let e: *defent = c.defs;
for (e != nil) {
if (syntax.streq(e.dname, name)) {
if (syntax.streq(e.dmod, c.curmod)) { return e.drhs; };
};
e = e.dnext;
};
e = c.defs;
for (e != nil) {
if (syntax.streq(e.dname, name)) { return e.drhs; };
e = e.dnext;
};
return nil;
};
// deflookuprhsmod — same-module-first walk for `mod.NAME` references.
// Trio-leaf *mod variant mirroring fnretlookupmod (#31) / fnparamslookupmod
// (#28) / enumlookupmod (#4a). Module-qualified `alpha.MSG` from a third
// module needs the explicit alpha hint; deflookuprhs prefers c.curmod
// (which doesn't match either source module on a 3rd-module qualifier)
// and falls back to head-pick, possibly inlining beta.MSG's strlit when
// both alpha and beta declare same-leaf str defs. cgdot's mod-qualified
// str-def value-load routes here so a cross-module N_DOT collision
// resolves to the explicit module. Falls back to deflookuprhs's bare-
// leaf two-pass when no module matches.
fn deflookuprhsmod(c: *cgen, name: str, mod: str) *syntax.node = {
if (mod.len > 0) {
let e: *defent = c.defs;
for (e != nil) {
if (syntax.streq(e.dname, name)) {
if (syntax.streq(e.dmod, mod)) { return e.drhs; };
};
e = e.dnext;
};
};
return deflookuprhs(c, name);
};
// #149: rhs peels (N_CAST / unary ±) to a float literal — the exact
// shape emitfloatlitdata (cgen.ww) emits a DATA symbol for. The scalar-
// float address-of gate must equal that emission set, or `&def` LEAQs a
// symbol the data pass never wrote. Keep in sync with emitfloatlitdata's
// peel.
fn floatlitleaf(rhs: *syntax.node) bool = {
let r: *syntax.node = rhs;
for (r != nil) {
if (r.kind != syntax.nkind.N_CAST) { break; };
r = r.lhs;
};
if (r != nil) {
if (r.kind == syntax.nkind.N_UN) {
if (r.op == syntax.tkind.TK_MINUS) {
r = r.lhs;
for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; };
} else { if (r.op == syntax.tkind.TK_PLUS) {
r = r.lhs;
for (r != nil) { if (r.kind != syntax.nkind.N_CAST) { break; }; r = r.lhs; };
}; };
};
};
if (r == nil) { return false; };
return r.kind == syntax.nkind.N_FLOATLIT;
};
// #149/#147: a top-level def is addressable for `&def` iff emitdefs emits
// a DATA symbol for it — struct, array, scalar int (foldintliteral), or
// scalar float whose rhs peels to a FLOATLIT. Gate held identical to
// cstage def_is{struct,array,scalar}def so the addressable set matches
// byte-for-byte (rule 10). str defs and computed-rhs floats (#147
// `def NAN = 0.0/0.0`) have no symbol and are excluded → routed to the
// loud error, never a LEAQ of a missing symbol. `opnd` is the `&`-operand
// N_IDENT; its checker-stamped type_ carries the def's type (same as the
// cgident float-def read at cgenexpr.ww).
fn defisaddressable(c: *cgen, opnd: *syntax.node) bool = {
let nm: str = opnd.str;
if (defvarstructinfo(c, nm) != nil) { return true; };
// #88: the emission side (emitdefconstants' array arm) peels
// TY_NAMED off d.lhs.type_ transitively, so an alias-typed def
// array HAS a DATA symbol — keying this gate on the unchased
// dtnode kind (N_TARRAY) lied it back to the loud error. Chase
// the same stamped tinfo so gate == emission set stays exact.
let dtn: *syntax.node = defvartnode(c, nm);
if (dtn != nil) {
let du88: *syntax.tinfo = tichase(dtn.type_: *syntax.tinfo);
if (du88 != nil) { if (du88.kind == syntax.tykind.TY_ARRAY) { return true; }; };
};
let drhs: *syntax.node = deflookuprhs(c, nm);
if (drhs == nil) { return false; };
let v: u64 = 0u64;
if (foldintliteral(drhs, &v)) { return true; };
if (isfloattype(c, opnd)) { if (floatlitleaf(drhs)) { return true; }; };
return false;
};
// ---- 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: *syntax.node) void = {
c.mods = nil;
c.uses = nil;
if (file == nil) { return; };
let d: *syntax.node = file.list;
for (d != nil) {
// M1 #22: record alias→path for the qualified-ref hint.
if (d.kind == syntax.nkind.N_USE) {
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 == syntax.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: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR) {
let an: str = a.str;
if (syntax.streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
if (!isffi) {
// M1 #32: the ROOT main (imported==0) stays bare;
// an IMPORTED `fn main` mangles on its path.
if (!syntax.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;
};
};
};
};
// §7-A / #53: exported non-fn decls (def/type/let) path-qualify
// like fns — `export def MAX` becomes `<mod>.MAX`, not a bare
// `MAX` two packages could clash on under sep-compile. No export
// guard: every decl with a module mangles identically.
if (d.kind == syntax.nkind.N_DEF) {
if (d.nmod.len > 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
};
if (d.kind == syntax.nkind.N_TYPEDECL) {
if (d.nmod.len > 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
};
if (d.kind == syntax.nkind.N_LET) {
if (d.nmod.len > 0) {
let m: *modent = alloc(modent{mname=d.str, nmod=d.nmod, omod=d.nmod, mnext=c.mods})!;
c.mods = m;
};
};
d = d.next;
};
};
fn modlookup(c: *cgen, name: str) str = {
let m: *modent = c.mods;
for (m != nil) {
if (syntax.streq(m.mname, name)) { return m.nmod; };
m = m.mnext;
};
let empty: str;
empty.ptr = nil;
empty.len = 0;
return empty;
};
// usehint — 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 (syntax.streq(m.mname, alias)) {
if (syntax.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 (syntax.streq(m.mname, name)) {
if (hint.len > 0 && m.nmod.len > 0
&& syntax.streq(m.nmod, hint)) {
return m.nmod;
};
if (first.len == 0 && first.ptr == nil) {
first = m.nmod;
};
};
m = m.mnext;
};
return first;
};
// emitsymname — write the asm symbol name for `ident`. Honours, in
// order: FFI mapping (@symbol), module mangling (private decls), bare
// name. Use everywhere a top-level non-fn name is emitted before `(SB)`
// — DATA labels for top-level lets/defs, address-of-let, etc. Fn names
// (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates
// cross-module same-leaf fn exports.
fn emitsymname(c: *cgen, ident: str) void = {
let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) {
// FFI hit — emit the mapped linker symbol verbatim.
emitbytes( resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookup(c, ident);
if (mod.len > 0) {
emitbytes( mod.ptr, mod.len: u64);
emitbytes( ".".ptr, 1u64);
};
emitbytes( ident.ptr, ident.len: u64);
};
// emitfnname — write the asm symbol name for a fn `ident`, threading
// `hint` (the explicit module from a `mod.fn` use site, or c.curmod
// for bare-IDENT calls) through modlookupforfn. Same FFI override
// semantics as emitsymname; same dot-separator format. Use at every
// CALL / LEAQ-of-fn / TEXT-def site.
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
let resolved: str = ffiresolve(c, ident);
if (resolved.ptr != ident.ptr) {
emitbytes( resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookupforfn(c, ident, hint);
if (mod.len > 0) {
emitbytes( mod.ptr, mod.len: u64);
emitbytes( ".".ptr, 1u64);
};
emitbytes( ident.ptr, ident.len: u64);
};
// ---- FFI map ---------------------------------------------------------
fn fficollect(c: *cgen, file: *syntax.node) void = {
c.ffis = nil;
if (file == nil) { return; };
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_FNDECL) {
let a: *syntax.node = d.attr;
for (a != nil) {
if (a.kind == syntax.nkind.N_ATTR) {
let aname: str = a.str;
if (syntax.streq(aname, "symbol")) {
let symnode: *syntax.node = a.list;
if (symnode != nil) {
if (symnode.kind == syntax.nkind.N_STRLIT) {
let f: *ffi = alloc(ffi{ident=d.str, symbol=symnode.str, fnext=c.ffis})!;
c.ffis = f;
};
};
};
};
a = a.next;
};
};
d = d.next;
};
};
fn ffiresolve(c: *cgen, ident: str) str = {
let f: *ffi = c.ffis;
for (f != nil) {
let id: str = f.ident;
if (syntax.streq(id, ident)) { return f.symbol; };
f = f.fnext;
};
return ident;
};
// ---- 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 "?";
};