Files
ww/selfhost/cmd/wcc/cgen.ww
Hojun-Cho b787641ef9 selfhost+test: route N_DOT match scrutinee through fnretlookupmod (#31)
Wwstage matchscrutt now mirrors cstage's typed-AST scrutinee-type
lookup for module-qualified mod.fn(...) callees, restoring per-arm
tag dispatch on cross-module shadowed-name 4-arm matches. Class A
runtime miscompile, silent across collectfnrets shadowing — was
the 8th unmask of session 5.

Pre-fix: wwstage's matchscrutt N_DOT branch (cgenutil.ww:2061)
called `fnretlookup(c, callee.str)` — name-only resolution.
collectfnrets prepends to c.fnrets, so when a caller fn (e.g.
lib/strings's `next`) shadows a callee fn-name (utf8's `next`),
the prepend chain has the caller's narrower tagged return at the
head. matchscrutt then resolved the scrutinee type to the WRONG
tagged shape, and variantindex lookups for arms past the
shadowing caller's variant count returned -1 → want=0 →
match-arm `CMPQ $0, AX` for arms 2 and 3 on a (rune | done |
more | invalid) probe. Effect: arms 2/3 silently unreachable
even when the runtime tag matched, falling through to default.

Cstage gets the scrutinee type via the checker-set callee type
on the N_DOT node, so picks the correct utf8.next return shape.

Polarity catalog: wwstage UNDER — fnretlookup missing module-
preferring discipline. **Third leaf in the same trio**: #27
(aliaslookupmod), #28 (fnparamslookupmod), #31 (fnretlookupmod).
Pattern is recurring; full graduation of all leaf-name lookups
to same-module-first is a candidate for STATUS-3 task #1
variant-widen consolidation refactor (deferred to next session
opener per rob).

Fix: new fnretlookupmod helper in cgen.ww (same-module-first
walk, fallback to existing first-match — cell-for-cell mirror
of fnparamslookupmod from #28). matchscrutt N_DOT branch
extracts `cmod` from callee.lhs.str and routes through the
helper. Other 13 fnretlookup callsites untouched per #28's
"fix only what has a real consumer" discipline. fnret.fmod
field + collectfnrets f.fmod assignment already landed in #28.

Surfaced by lib/strings commit-2 pre-flight: probe iter+next
shape calls utf8.next; the probe's own `fn next` shadows
utf8.next at the c.fnrets head. Bootstrap-stable because no
selfhost-corpus path shadows a fn name across modules with a
wider tagged return on the shadowed side; lib/strings.iter
pulling utf8.next under wwstage was the first exerciser.

Filed follow-up (NOT in scope here): #32 wwstage runtime stomp
on utf8.next via *iterator caller — separate Class A surfaced
by 929 direct utf8.next regression row design. #31's fix is
correct in isolation; #32 blocks lib/strings commit 2 (#30).

Tests:
  - 728_match_4arm_cross_module pins distinct CMPQ $K, AX tags
    in TEXT b.next via bitmap covering [0..arms), robust to
    arm ordering. Three cross-module shadowed-name shapes × cmp
    -s byte-id. Sentinel-flip-verified: revert fnretlookupmod
    route → 3/6 wwstage fixtures fail "arm K repeats tag $0
    (collapse)".
  - 929_match_4arm_cross_module_run runtime-pins 6 rows × 2
    stages per-arm exit-code shape: 3/4/5/6-arm boundary,
    mixed (i32|str|rune|u8), reverse arm-order in match source.
    Confirms bug follows fnretlookup-resolved type, not match
    source order.

102/102 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
2026-05-18 11:12:14 +09:00

1891 lines
56 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.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
// 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.
use cgenutil;
use cgenexpr;
use cgenstmt;
use cgendecl;
// ---- typedef alias registry -----------------------------------------
//
// `type error = str;` makes `error` a struct-shape alias. We track
// alias→target so isstrtype / isslicetype / structlookup can
// resolve through the chain. Only direct nkind.N_TNAME aliases are mapped;
// `type p = struct {...}` is handled by collectstructs.
type aliasent = struct {
aname: str,
amod: str, // originating module (`// MODULE: foo`), or empty
target: *node, // the rhs type expr
aanext: *aliasent,
};
fn collectaliases(c: *cgen, file: *node) void = {
c.aliases = nil;
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_TYPEDECL) {
let body: *node = d.lhs;
if (body != nil) {
if (body.kind != nkind.N_TSTRUCT) {
let a: *aliasent = amalloc(c.a, 64u64): *aliasent;
a.aname = d.str;
a.amod = d.module;
a.target = body;
a.aanext = c.aliases;
c.aliases = a;
};
};
};
d = d.next;
};
};
fn aliaslookup(c: *cgen, name: str) *node = {
// Same-module first, then any. Mirrors cstage's scope_lookup_prefer
// (cmd/wcc/check.c:65); without the prefer pass a bare `invalid`
// in module M with `type invalid = !void;` can collapse onto a
// strconv-style `type invalid = !i32;` registered earlier in
// c.aliases (head-first walk). The leaf-collision then drives a
// narrow MOVSXD load of a slot the let-decl zero-inits 8B-wide
// (task #27 silent-correct-by-zero-init).
let a: *aliasent = c.aliases;
for (a != nil) {
if (streq(a.aname, name)) {
if (streq(a.amod, c.curmod)) { return a.target; };
};
a = a.aanext;
};
a = c.aliases;
for (a != nil) {
if (streq(a.aname, name)) { return a.target; };
a = a.aanext;
};
// Module-qualified form: `pkg.alias` → match the leaf name
// scoped to its originating module. Mirrors check.c's module-
// qualified type resolution; requiring `amod == pkg` is what
// prevents two modules with same-leaf-name aliases from
// collapsing into whichever entry appears first in the chain.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == 46u8) { // '.'
let pkg: str;
pkg.ptr = name.ptr;
pkg.len = i;
let leaf: str;
leaf.ptr = name.ptr + ((i + 1): u64);
leaf.len = name.len - (i + 1);
let b: *aliasent = c.aliases;
for (b != nil) {
if (streq(b.aname, leaf)) {
if (streq(b.amod, pkg)) {
return b.target;
};
};
b = b.aanext;
};
i = -1;
} else {
i -= 1;
};
};
return nil;
};
// ---- enum registry --------------------------------------------------
//
// Mirrors cmd/wcc/check.c's enum resolution at collect time: walk
// every `type Foo = enum [storage] { ... }`, pre-compute each
// member's u64 value (supporting auto-increment and sibling refs),
// and stash them so cgdot can fold `Foo.MEMBER` → MOVQ $value, AX.
// foldintliteral — fold the literal subset usable for top-level
// constant slots: int/rune literal, true/false/nil, and a unary
// +/-/~ over the same (any depth). No sibling-ident, no binary op.
// Shared between enumevalmember (literal leaves) and
// emitdefconstants (top-level def rhs).
//
// Whitelist kept tight on purpose: anything richer (sibling refs,
// arithmetic) belongs in enumevalmember, which calls this for its
// literal leaves and handles the rest itself.
fn foldintliteral(e: *node, out: *u64) bool = {
if (e == nil) { return false; };
let k: nkind = e.kind;
if (k == nkind.N_INTLIT) { *out = e.uval; return true; };
if (k == nkind.N_RUNELIT) { *out = e.uval; return true; };
if (k == nkind.N_TRUE) { *out = 1u64; return true; };
if (k == nkind.N_FALSE) { *out = 0u64; return true; };
if (k == nkind.N_NIL) { *out = 0u64; return true; };
if (k == nkind.N_UN) {
let v: u64;
if (!foldintliteral(e.lhs, &v)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == tkind.TK_TILDE) { *out = ~v; return true; };
if (op == tkind.TK_PLUS) { *out = v; return true; };
return false;
};
return false;
};
fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = {
if (e == nil) { return false; };
if (foldintliteral(e, out)) { return true; };
let k: nkind = e.kind;
if (k == nkind.N_IDENT) {
let m: *enummember = prev;
for (m != nil) {
if (streq(m.mname, e.str)) {
*out = m.mval;
return true;
};
m = m.emnext;
};
return false;
};
if (k == nkind.N_BIN) {
let a: u64;
let b: u64;
if (!enumevalmember(prev, e.lhs, &a)) { return false; };
if (!enumevalmember(prev, e.rhs, &b)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_PLUS) { *out = a + b; return true; };
if (op == tkind.TK_MINUS) { *out = a - b; return true; };
if (op == tkind.TK_STAR) { *out = a * b; return true; };
if (op == tkind.TK_SLASH) {
if (b == 0u64) { return false; };
*out = a / b; return true;
};
if (op == tkind.TK_PERCENT) {
if (b == 0u64) { return false; };
*out = a % b; return true;
};
if (op == tkind.TK_AMP) { *out = a & b; return true; };
if (op == tkind.TK_PIPE) { *out = a | b; return true; };
if (op == tkind.TK_CARET) { *out = a ^ b; return true; };
if (op == tkind.TK_LSHIFT) { *out = a << b; return true; };
if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; };
return false;
};
if (k == nkind.N_UN) {
let v: u64;
if (!enumevalmember(prev, e.lhs, &v)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == tkind.TK_TILDE) { *out = ~v; return true; };
if (op == tkind.TK_PLUS) { *out = v; return true; };
return false;
};
return false;
};
fn collectenums(c: *cgen, file: *node) void = {
c.enums = nil;
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_TYPEDECL) {
let body: *node = d.lhs;
if (body != nil) {
if (body.kind == nkind.N_TENUM) {
let et: *enumtype = amalloc(c.a, 64u64): *enumtype;
et.ename = d.str;
et.emod = d.module;
et.storage = body.lhs;
et.members = nil;
let prev: u64 = (-1i64): u64;
let mhead: *enummember = nil;
let mtail: *enummember = nil;
let m: *node = body.list;
for (m != nil) {
let val: u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumevalmember(mhead, m.lhs, &val)) {
val = prev + 1u64;
};
};
prev = val;
let em: *enummember = amalloc(c.a, 32u64): *enummember;
em.mname = m.str;
em.mval = val;
em.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 = {
// Exact match first: bare-from-source idents and already-leafed
// names hit here directly.
let e: *enumtype = c.enums;
for (e != nil) {
if (streq(e.ename, name)) { return e; };
e = e.etnext;
};
// Module-qualified form: `pkg.enum` → match the leaf scoped to
// its originating module. Mirrors aliaslookup's mod-filter; the
// `emod == pkg` guard is what prevents two modules with same-
// leaf-name enums from collapsing into whichever entry appears
// first in the chain.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == 46u8) { // '.'
let pkg: str;
pkg.ptr = name.ptr;
pkg.len = i;
let leaf: str;
leaf.ptr = name.ptr + ((i + 1): u64);
leaf.len = name.len - (i + 1);
let b: *enumtype = c.enums;
for (b != nil) {
if (streq(b.ename, leaf)) {
if (streq(b.emod, pkg)) {
return b;
};
};
b = b.etnext;
};
return nil;
};
i -= 1;
};
return nil;
};
fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = {
let m: *enummember = en.members;
for (m != nil) {
if (streq(m.mname, mname)) {
*out = m.mval;
return true;
};
m = m.emnext;
};
return false;
};
// resolvetype — follow typedef alias chains to a "canonical" type
// expr (str/slice/array/struct/...). Stops on cycles via depth limit.
fn resolvetype(c: *cgen, t: *node) *node = {
let cur: *node = t;
let depth: i32 = 0;
for (depth < 16) {
if (cur == nil) { return nil; };
if (cur.kind != nkind.N_TNAME) { return cur; };
let nm: str = cur.str;
let next: *node = aliaslookup(c, nm);
if (next == nil) { return cur; };
cur = next;
depth += 1;
};
return cur;
};
// ---- struct registry ------------------------------------------------
//
// Per-file map from struct name → list of fields with computed offsets
// and sizes. Built when cgfile walks nkind.N_TYPEDECL with nkind.N_TSTRUCT lhs.
// nkind.N_DOT and nkind.N_ASSIGN consult this to resolve `s.field` for struct or
// *struct bases.
type fieldinfo = struct {
fname: str,
foff: i32,
fsz: i32,
tnode: *node, // the field type expr, for nested struct lookups
finext: *fieldinfo,
};
type structinfo = struct {
sname: str,
smod: str, // originating module (`// MODULE: foo`), or empty
fields: *fieldinfo,
totsize: i32,
sinext: *structinfo,
};
// ---- locals / frame --------------------------------------------------
type local = struct {
name: str,
off: i32,
tnode: *node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil
lnext: *local,
};
// strlit — interned string literal record. Emitted as a DATA directive
// after all functions; cgexpr nkind.N_STRLIT loads (LEAQ ptr, MOVQ len).
type strlit = struct {
label: str, // "_S_<seq>"
bytes: str,
slnext: *strlit,
};
// ffi — `@symbol("name")` mapping. Body-less fn `foo` with this attr
// gets its CALL target rewritten to `name`.
type ffi = struct {
ident: str,
symbol: str,
fnext: *ffi,
};
// enummember — one (name, value) pair belonging to a registered enum.
// Values are pre-computed at collect time (Hare allows sibling refs
// like `RDWR = READ | WRITE`, so we walk the value expr against the
// already-resolved siblings). Lookup is linear; enum cardinality is
// usually small.
type enummember = struct {
mname: str,
mval: u64,
emnext: *enummember,
};
type enumtype = struct {
ename: str,
emod: str, // originating module (`// MODULE: foo`), or empty
storage: *node, // AST type expr for the storage type (i32 by default)
members: *enummember,
etnext: *enumtype,
};
def LOOP_MAX: i32 = 16;
def DEFER_MAX: i32 = 16;
type cgen = struct {
a: *arena,
locals: *local,
frame: i32,
lastwasreturn: i32,
labelseq: i32,
strlitseq: i32,
strlits: *strlit,
ffis: *ffi,
defs: *defent,
fnrets: *fnret,
aliases: *aliasent,
structs: *structinfo,
enums: *enumtype,
mods: *modent, // fn (any export status) + non-exported
// let/def/type decls → originating module
lets: *letvar, // top-level mutable scalar `let` bindings
fnname: str,
curmod: str, // current fn's `// MODULE: foo` directive (len=0
// when the fn is in the primary file). Drives
// bare-IDENT call mangling — `frob()` from
// inside lib/foo binds to `foo.frob` even when
// other modules also export `frob`. Set in cgfn
// before walking the body.
fnret: *node, // declared return type of current fn (or nil)
looptop: i32,
loopendbuf: *str, // stack of end labels for break
loopcontbuf: *str, // stack of cont labels for continue
yieldtop: i32,
yieldbuf: *str, // stack of match end labels for yield
defertop: i32,
deferbuf: **node, // stack of deferred exprs (LIFO at return)
// Variadic-call gather state. scanlocals walks the body in pre-
// order DFS and assigns per-call scratch names `@vararg_d_N` /
// `@vararg_sl_N` using this counter; cgcall resets and walks in
// the same order so the names line up at emission time.
varargseq: i32,
// Max @tagscr slot_sz across all reservation sites in the current
// function. scanlocals bumps; every emit-time `localadd("@tagscr",
// ...)` passes this same size so the first allocation lands a slot
// big enough for every later user. Single source of truth — pins
// rob's "scan + emit lockstep" invariant. Reset per cgfn.
tagscrsz: i32,
// Live @retscr offset (#14). c.locals-based `@`-prefix dedup in
// localadd is unwound by cgblock save/restore (post-#27), so a
// second `return` in a sibling/outer block reallocates a fresh
// slot — emit grew the frame past what scanlocals reserved, and
// the stomp landed below SP. retscroff is the persistent SSoT:
// 0 means "not yet allocated"; first emit-site sets it, every
// later emit reuses. Mirrors c.tagscrsz pattern (#38) but tracks
// offset, not size (per-fn return type is fixed, so size is too).
retscroff: i32,
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
// with size > 24B are passed via a hidden first-arg pointer
// (RDI) to a caller-prealloc dest; the callee writes through
// that pointer and returns it in RAX.
//
// sretargoff — callee-side @sretarg slot (8B, holds saved RDI).
// Set in cgfn prologue when the fn's return type
// triggers sret. 0 means N/A.
// 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.
// sretscroff — per-fn @sretscr discard slot, used by sret CALLs
// whose result has no named receiver. Single-slot
// SSoT mirroring c.retscroff; the scanlocals walk
// sums c.sretscrsz to pre-reserve.
// sretscrsz — max sret discard size in this fn (sums during
// scanlocals, consumed by localadd("@sretscr", ...)).
// 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.
sretargoff: i32,
sretdestoff: i32,
sretscroff: i32,
sretscrsz: i32,
sretforward: i32,
};
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
// Populated alongside modents; consulted by cgassign, cgdot, cgident
// and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot
// instead of being silently dropped. tnode is the declared type AST
// node — needed to distinguish scalar (8B) from str (16B) globals
// when picking the load/store sequence.
type letvar = struct {
name: str,
tnode: *node,
lvnext: *letvar,
};
fn cgeninit(c: *cgen, a: *arena) void = {
c.a = a;
c.locals = nil;
c.frame = 0;
c.lastwasreturn = 0;
c.labelseq = 0;
c.varargseq = 0;
c.tagscrsz = 0;
c.retscroff = 0;
c.sretargoff = 0;
c.sretdestoff = 0;
c.sretscroff = 0;
c.sretscrsz = 0;
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;
c.loopendbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.loopcontbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.yieldtop = 0;
c.yieldbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.defertop = 0;
c.deferbuf = amalloc(a, (DEFER_MAX: u64) * 8u64): **node;
};
// localalloc — append a slot for `name` without dedup. Used for
// match-arm bindings, which C cgen allocates via cgexpr's by-value
// `locals` list — so two separate matches each get fresh slots even
// when their bind names collide. scanlocals follows the same rule
// for nkind.N_MCASE.
fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
let asz: i32 = sz;
if (asz < 8) { asz = 8; };
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
c.frame += asz;
let off: i32 = 0 - c.frame;
let l: *local = amalloc(c.a, 48u64): *local;
l.name = name;
l.off = off;
l.tnode = tnode;
l.lnext = c.locals;
c.locals = l;
return off;
};
// localaddstack — register a param at a positive BP offset. Used for
// args that overflow the 6 SysV int / 8 float reg windows; the caller
// pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP),
// etc. (after the saved RIP+BP). No spill instruction is emitted; the
// slot IS the caller's stack slot.
fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = {
let l: *local = amalloc(c.a, 48u64): *local;
l.name = name;
l.off = off;
l.tnode = tnode;
l.lnext = c.locals;
c.locals = l;
};
fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
// User-let path (post-#27): always allocate a fresh slot per
// binding. Pre-fix this deduped by name to share one slot
// across same-name lets in disjoint scopes — inherited from
// C cgen'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.
// Localfind walks head-first, so the most-recent binding still
// wins lookups inside its scope. Tnode is carried on the
// freshly-pushed entry, so type dispatch in cgenutil never
// sees a stale predecessor.
//
// Synthetic scratch slots (`@tagscr`, `@retscr`, `@tagbase`)
// keep the per-fn dedup. Each scratch is sized identically
// across its call sites and intended to be shared — the
// scanlocals pre-pass also dedups via scanseenmark, so frame
// reservation and emit-time allocation stay in sync. The
// `@`-prefix carve-out preserves that contract; user names
// can never start with `@` (lexer-rejected).
//
// @retscr (#14) routes through c.retscroff instead of c.locals.
// The c.locals-based dedup is unwound by cgblock save/restore
// (post-#27): a return inside an `if` block adds @retscr to
// c.locals; on block exit, c.locals reverts and a sibling/outer
// return reallocates a fresh slot. Scan had reserved one slot;
// emit grew the frame past the reservation and the second
// site's writes landed below SP. c.retscroff is per-fn state
// that survives cgblock save/restore and pins single-slot.
if (name.len > 0) {
if (name[0] == 64u8) { // '@'
if (streq(name, "@retscr")) {
if (c.retscroff != 0) { return c.retscroff; };
let off: i32 = localalloc(c, name, sz, tnode);
c.retscroff = off;
return off;
};
// @sretarg / @sretscr (#23): same single-slot SSoT
// pattern as @retscr. @sretarg holds the saved hidden
// RDI for sret callees (8B, set once per fn at the
// prologue); @sretscr is the caller-side discard slot
// for sret CALLs whose result is dropped.
if (streq(name, "@sretarg")) {
if (c.sretargoff != 0) { return c.sretargoff; };
let off: i32 = localalloc(c, name, sz, tnode);
c.sretargoff = off;
return off;
};
if (streq(name, "@sretscr")) {
if (c.sretscroff != 0) { return c.sretscroff; };
let off: i32 = localalloc(c, name, sz, tnode);
c.sretscroff = off;
return off;
};
let cur: *local = c.locals;
for (cur != nil) {
let cn: str = cur.name;
if (streq(cn, name)) {
cur.tnode = tnode;
return cur.off;
};
cur = cur.lnext;
};
};
};
return localalloc(c, name, sz, tnode);
};
// scanseenmark — called by scanlocals on every let / match-bind
// site. Returns true if `name` is already tracked in c.locals (so
// the slot will be shared at emission time — no new frame bump).
// Otherwise appends a name-only stub and returns false. Stubs are
// thrown away when cgfn resets c.locals before emission.
fn scanseenmark(c: *cgen, name: str) bool = {
if (localfindnode(c, name) != nil) { return true; };
let l: *local = amalloc(c.a, 48u64): *local;
l.name = name;
l.off = 0;
l.tnode = nil;
l.lnext = c.locals;
c.locals = l;
return false;
};
fn localfindnode(c: *cgen, name: str) *local = {
let l: *local = c.locals;
for (l != nil) {
let ln: str = l.name;
if (streq(ln, name)) { return l; };
l = l.lnext;
};
return nil;
};
fn localfind(c: *cgen, name: str) i32 = {
let l: *local = c.locals;
for (l != nil) {
let ln: str = l.name;
if (ln.len == name.len) {
let i: i32 = 0;
let eq: bool = true;
for (i < name.len) {
if (ln[i] != name[i]) { eq = false; i = name.len; }
else { i += 1; };
};
if (eq) { return l.off; };
};
l = l.lnext;
};
return 0;
};
// ---- emit helpers ---------------------------------------------------
fn emitline(s: str) void = { os.write(1, s.ptr, s.len: u64); };
fn emitint(v: i64) void = {
let s: str = strconv.i64tos(v, strconv.base.DEC);
os.write(1, s.ptr, s.len: u64);
};
fn emituint(v: u64) void = {
let s: str = strconv.u64tos(v, strconv.base.DEC);
os.write(1, 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(")");
};
// 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 "<fnname>_<prefix>_<seq>". Returns an
// arena-owned str. Mirrors C cgen's mklabel so diffs match.
fn mklabel(c: *cgen, prefix: str) str = {
let buf: [128]u8;
let i: i32 = 0;
let fname: str = c.fnname;
let j: i32 = 0;
for (j < fname.len) {
buf[i] = fname[j];
i += 1; j += 1;
};
buf[i] = 95u8; i += 1; // '_'
j = 0;
for (j < prefix.len) {
buf[i] = prefix[j];
i += 1; j += 1;
};
buf[i] = 95u8; i += 1; // '_'
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC);
let n: i32 = ns.len;
let dk: i32 = 0;
for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; };
c.labelseq += 1;
let total: i32 = i + n;
let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8;
let k: i32 = 0;
for (k < total) {
p[k] = buf[k];
k += 1;
};
p[total] = 0u8;
let r: str;
r.ptr = p;
r.len = total;
return r;
};
fn emitlabel(s: str) void = {
os.write(1, 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] = 46u8; i += 1; // '.'
let j: i32 = 0;
for (j < prefix.len) {
buf[i] = prefix[j];
i += 1; j += 1;
};
buf[i] = 95u8; i += 1; // '_'
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC);
let n: i32 = ns.len;
let dk: i32 = 0;
for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; };
c.labelseq += 1;
let total: i32 = i + n;
let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8;
let k: i32 = 0;
for (k < total) {
p[k] = buf[k];
k += 1;
};
p[total] = 0u8;
let r: str;
r.ptr = p;
r.len = total;
return r;
};
// ---- string interning ------------------------------------------------
//
// streq is provided by sym.ww and reused here.
// internstrlit — return a stable label for `bytes`. Dedups by content
// so identical literals share storage.
fn internstrlit(c: *cgen, bytes: str) str = {
let s: *strlit = c.strlits;
for (s != nil) {
let bs: str = s.bytes;
if (streq(bs, bytes)) {
return s.label;
};
s = s.slnext;
};
// New label "_S_<seq>".
let buf: [32]u8;
buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_"
let ns: str = strconv.i64tos(c.strlitseq: i64, strconv.base.DEC);
let n: i32 = ns.len;
let dk: i32 = 0;
for (dk < n) { buf[3 + dk] = ns.ptr[dk]; dk += 1; };
c.strlitseq += 1;
let total: i32 = 3 + n;
let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8;
let i: i32 = 0;
for (i < total) { p[i] = buf[i]; i += 1; };
p[total] = 0u8;
let lab: str;
lab.ptr = p;
lab.len = total;
let nw: *strlit = amalloc(c.a, 48u64): *strlit;
nw.label = lab;
nw.bytes = bytes;
nw.slnext = c.strlits;
c.strlits = nw;
return lab;
};
// letscalarprim — recognise the bare type-name keywords whose values
// fit in an 8-byte .data slot and load back with a plain MOVQ. Float
// types are handled separately by letfloatprim — they need MOVSS/MOVSD
// and use 4-byte (f32) or 8-byte (f64) slots.
fn letscalarprim(nm: str) bool = {
if (streq(nm, "bool")) { return true; };
if (streq(nm, "rune")) { return true; };
if (streq(nm, "i8")) { return true; };
if (streq(nm, "i16")) { return true; };
if (streq(nm, "i32")) { return true; };
if (streq(nm, "i64")) { return true; };
if (streq(nm, "u8")) { return true; };
if (streq(nm, "u16")) { return true; };
if (streq(nm, "u32")) { return true; };
if (streq(nm, "u64")) { return true; };
if (streq(nm, "int")) { return true; };
if (streq(nm, "uint")) { return true; };
if (streq(nm, "uintptr")) { return true; };
return false;
};
// letfloatprim — float type-name keywords. f32 → 4B slot, f64 → 8B.
// Returns the slot size or 0 if not a float type.
fn letfloatprim(nm: str) i32 = {
if (streq(nm, "f32")) { return 4; };
if (streq(nm, "f64")) { return 8; };
return 0;
};
// letemitsize — slot size in bytes for a top-level `let`, or 0 if
// the type isn't yet supported as a writable global. Walks type
// aliases so byte output matches C cgen, which resolves Type kinds.
// 4 → f32 (literal init supported)
// 8 → scalar or f64 (literal init supported)
// 16 → str (only zero-init / nil / "" supported)
// 24 → slice (only zero-init supported)
// varies → struct (zero-init only; field reads/scalar-field writes)
fn letemitsize(c: *cgen, d: *node) i32 = {
if (d == nil) { return 0; };
let t: *node = d.lhs;
for (t != nil) {
if (t.kind == nkind.N_TPTR) { return 8; };
if (t.kind == nkind.N_TSLICE) { return 24; };
if (t.kind == nkind.N_TARRAY) {
let lenn: *node = t.rhs;
let elemn: *node = t.lhs;
let alen: i32 = 1;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i32; };
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
return alen * esz;
};
if (t.kind != nkind.N_TNAME) { return 0; };
let nm: str = t.str;
if (letscalarprim(nm)) { return 8; };
let fsz: i32 = letfloatprim(nm);
if (fsz > 0) { return fsz; };
if (streq(nm, "str")) { return 16; };
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
let next: *node = aliaslookup(c, nm);
if (next == nil) { return 0; };
t = next;
};
return 0;
};
fn collectlets(c: *cgen, file: *node) void = {
c.lets = nil;
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_LET) {
let nm: str = d.str;
if (nm.len > 0) {
if (letemitsize(c, d) > 0) {
let lv: *letvar = amalloc(c.a, 48u64): *letvar;
lv.name = nm;
lv.tnode = d.lhs;
lv.lvnext = c.lets;
c.lets = lv;
};
};
};
d = d.next;
};
};
fn isletvar(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) { return true; };
lv = lv.lvnext;
};
return false;
};
// letvarisstr — is the named top-level let a str global? Resolves
// aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/
// cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare
// MOVQ scalar load.
// letvartnode — direct lookup of a top-level let's tnode. Used by
// cgindex / cgassign to detect global `[N]T` arrays and `*T`
// pointers, where the addressing path needs LEAQ name(SB) (array)
// or MOVQ name(SB) (pointer) and the element size from T.
fn letvartnode(c: *cgen, name: str) *node = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) { return lv.tnode; };
lv = lv.lvnext;
};
return nil;
};
fn letvarisstr(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) {
let t: *node = lv.tnode;
for (t != nil) {
if (t.kind != nkind.N_TNAME) { return false; };
let nm: str = t.str;
if (streq(nm, "str")) { return true; };
let nx: *node = aliaslookup(c, nm);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// letvarisslice — is the named top-level let a slice global?
// Slice headers are 24 bytes; the ABI flows as (AX, BX, CX) so the
// load sequence ends with `MOVQ 16(CX), CX` (overwrites the
// address holder with the cap). Mirrors C cgen's `let_isslice`.
fn letvarisslice(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) {
let t: *node = lv.tnode;
if (t == nil) { return false; };
if (t.kind == nkind.N_TSLICE) { return true; };
return false;
};
lv = lv.lvnext;
};
return false;
};
// letvarisfloat — slot size for a named float global, or 0 if not
// a float-typed let. Walks aliases so the byte-identity contract
// matches C cgen's `let_isfloat` (which resolves Type kinds).
fn letvarisfloat(c: *cgen, name: str) i32 = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) {
let t: *node = lv.tnode;
for (t != nil) {
if (t.kind != nkind.N_TNAME) { return 0; };
let fsz: i32 = letfloatprim(t.str);
if (fsz > 0) { return fsz; };
let nx: *node = aliaslookup(c, t.str);
if (nx == nil) { return 0; };
t = nx;
};
return 0;
};
lv = lv.lvnext;
};
return 0;
};
// letvarisstruct — is the named top-level let a struct global?
// Struct globals use LEAQ name(SB), CX as the field-access base; the
// cgdot read and cgassign write paths branch on this to skip the
// frame-relative addressing they use for locals.
fn letvarisstruct(c: *cgen, name: str) bool = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) {
let t: *node = lv.tnode;
for (t != nil) {
if (t.kind != nkind.N_TNAME) { return false; };
let nm: str = t.str;
if (structlookup(c, nm) != nil) { return true; };
let nx: *node = aliaslookup(c, nm);
if (nx == nil) { return false; };
t = nx;
};
return false;
};
lv = lv.lvnext;
};
return false;
};
// letvarstructinfo — for a struct global, return its structinfo
// so the cgdot/cgassign paths can look up fields. nil if the let
// isn't a struct (or wasn't found).
fn letvarstructinfo(c: *cgen, name: str) *structinfo = {
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, name)) {
let t: *node = lv.tnode;
for (t != nil) {
if (t.kind != nkind.N_TNAME) { return nil; };
let nm: str = t.str;
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si; };
let nx: *node = aliaslookup(c, nm);
if (nx == nil) { return nil; };
t = nx;
};
return nil;
};
lv = lv.lvnext;
};
return nil;
};
// 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; };
os.write(1, 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; };
os.write(1, bb.ptr, 2u64);
return;
};
let bb: [1]u8;
bb[0] = b;
os.write(1, bb.ptr, 1u64);
};
// letpreintern — intern strlits referenced from top-level str-let
// initialisers BEFORE emitdatasection runs. Mirrors cmd/w6c/cgen.c
// let_pre_intern: emitletdataw later looks up the same label, and
// emitdatasection emits the DATA row in the same .s file. Running
// emitletdataw after emitdatasection would flip the (DATA strlits,
// DATAW lets) section order and break byte-identity.
export fn letpreintern(c: *cgen, file: *node) void = {
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_LET) {
let sz: i32 = letemitsize(c, d);
if (sz == 16) {
let r: *node = d.rhs;
for (r != nil) {
if (r.kind != nkind.N_CAST) { break; };
r = r.lhs;
};
if (r != nil) {
if (r.kind == nkind.N_STRLIT) {
if (r.str.len > 0) {
internstrlit(c, r.str);
};
};
};
};
};
d = d.next;
};
};
// emitletdataw — DATAW directive per top-level `let` global.
// 8B scalar with int/rune/bool/nil literal init (or no init).
// 16B str — no init / `nil` / `""` → 16 zero bytes; or non-empty
// strlit init → 8 zero placeholder + 8 LE len bytes plus a
// DATAR slot+0,strlit reloc that the linker patches at load.
// sz struct — zero only.
// Non-literal scalar inits and unsupported shapes are skipped so the
// link surfaces an undefined-symbol error if the binding is used.
fn emitletdataw(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_LET) {
let nm: str = d.str;
if (nm.len > 0) {
let sz: i32 = letemitsize(c, d);
let issg: bool = letvarisstruct(c, nm);
let fsz: i32 = letvarisfloat(c, nm);
if (fsz > 0) {
// Float global: 4B (f32) or 8B (f64).
// Two init shapes:
// - no rhs: emit fsz zero bytes
// - N_FLOATLIT: bake the IEEE bits the
// parser stashed in r.uval (lexer
// bit-casts t.fval into t.uval). f32
// emits the low 4 bytes; f64 emits 8.
let bits: u64 = 0u64;
let ok: bool = true;
if (d.rhs != nil) {
let r: *node = d.rhs;
for (r != nil) {
if (r.kind != nkind.N_CAST) { break; };
r = r.lhs;
};
ok = false;
if (r != nil) {
if (r.kind == nkind.N_FLOATLIT) {
bits = r.uval;
ok = true;
};
};
};
if (ok) {
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
let nb: u64 = bits;
for (i < fsz) {
emitdatawbyte((nb & 255u64): u8);
nb = nb >> 8u64;
i += 1;
};
emitline("\"\n");
};
};
// Skip the scalar 8B path when the global is a
// fixed-size array that just happens to sum to 8
// bytes (e.g. [4]u16, [8]u8) — the array path
// below handles it and the duplicate DATAW would
// otherwise differ across stages on user code.
let isarr8: bool = false;
if (d.lhs != nil) {
if (d.lhs.kind == nkind.N_TARRAY) { isarr8 = true; };
};
if (sz == 8 && !issg && fsz == 0 && !isarr8) {
let v: u64 = 0u64;
let ok: bool = true;
if (d.rhs != nil) {
let r: *node = d.rhs;
for (r != nil) {
if (r.kind != nkind.N_CAST) { break; };
r = r.lhs;
};
// Same helper as emitdefconstants (#24)
// — widens the gate so N_UN over an
// int leaf folds. `let x: i8 = -1i8;`
// arrives as N_UN(TK_MINUS, N_INTLIT)
// after the typed-AST cast peel.
ok = foldintliteral(r, &v);
};
if (ok) {
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
let n: u64 = v;
for (i < 8) {
let b: u8 = (n & 255u64): u8;
n = n >> 8u64;
emitdatawbyte(b);
i += 1;
};
emitline("\"\n");
};
};
if (sz == 16 && !issg) {
let r: *node = d.rhs;
for (r != nil) {
if (r.kind != nkind.N_CAST) { break; };
r = r.lhs;
};
// str-literal init (non-empty): emit
// the 16B payload as 8 placeholder zero
// bytes + 8 LE bytes of length, then a
// DATAR reloc to patch the ptr half with
// the strlit's runtime VA.
let strlitinit: bool = false;
if (r != nil) {
if (r.kind == nkind.N_STRLIT) {
if (r.str.len > 0) { strlitinit = true; };
};
};
if (strlitinit) {
let lab: str = internstrlit(c, r.str);
let v: u64 = r.str.len: u64;
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
for (i < 8) { emitdatawbyte(0u8); i += 1; };
i = 0;
let nv: u64 = v;
for (i < 8) {
emitdatawbyte((nv & 255u64): u8);
nv = nv >> 8u64;
i += 1;
};
emitline("\"\n");
emitline("DATAR ");
emitsymname(c, nm);
emitline("+0(SB),");
os.write(1, lab.ptr, lab.len: u64);
emitline("(SB)\n");
} else {
// zero-init: accept no rhs, nil,
// or empty strlit.
let ok: bool = true;
if (d.rhs != nil) {
ok = false;
if (r != nil) {
if (r.kind == nkind.N_NIL) { ok = true; };
if (r.kind == nkind.N_STRLIT) {
if (r.str.len == 0) { ok = true; };
};
};
};
if (ok) {
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
for (i < 16) {
emitdatawbyte(0u8);
i += 1;
};
emitline("\"\n");
};
};
};
if (sz == 24 && !issg) {
// Slice: zero-init only (no slice-literal
// syntax to honour). Any rhs other than
// `nil` is skipped → undefined symbol at
// link.
let ok: bool = true;
if (d.rhs != nil) {
let r: *node = d.rhs;
for (r != nil) {
if (r.kind != nkind.N_CAST) { break; };
r = r.lhs;
};
ok = false;
if (r != nil) {
if (r.kind == nkind.N_NIL) { ok = true; };
};
};
if (ok) {
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
for (i < 24) {
emitdatawbyte(0u8);
i += 1;
};
emitline("\"\n");
};
};
// Struct globals — any size, zero-init only.
// A struct literal init isn't compile-time
// evaluated yet; skip and the link will surface
// an undefined-symbol error if referenced.
if (issg) {
if (d.rhs == nil) {
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
for (i < sz) {
emitdatawbyte(0u8);
i += 1;
};
emitline("\"\n");
};
};
// Top-level `[N]T = [a, b, ...]` array global.
// Emits N*esz bytes with each element's bytes
// little-endian for the declared primitive width.
// Element fold goes through foldintliteral (same
// helper as emitdefconstants / scalar arm above)
// so `-1i8` and friends emit their two's-complement
// bytes after the leading N_CAST peel — pre-#19
// this arm only matched bare N_INTLIT/N_RUNELIT and
// silently emitted zero for unfoldable elements.
// `...` (N_FIELD with str="...") repeats the last
// folded value across the remaining slots.
if (d.lhs != nil) {
if (d.lhs.kind == nkind.N_TARRAY) {
let elemn: *node = d.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
let total: i32 = sz;
let alen: i32 = total / esz;
let elems: *node = nil;
if (d.rhs != nil) {
if (d.rhs.kind == nkind.N_ARRLIT) {
elems = d.rhs.list;
};
};
emitline("DATAW ");
emitsymname(c, nm);
emitline("(SB),\"");
let i: i32 = 0;
let e: *node = elems;
let last: u64 = 0u64;
let inrepeat: bool = false;
for (i < alen) {
let v: u64 = last;
if (!inrepeat && e != nil) {
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) {
inrepeat = true;
} else {
e = e.next;
};
} else {
let ev: *node = e;
for (ev != nil) {
if (ev.kind != nkind.N_CAST) { break; };
ev = ev.lhs;
};
if (!foldintliteral(ev, &v)) { v = 0u64; };
last = v;
e = e.next;
};
};
let nb: u64 = v;
let b: i32 = 0;
for (b < esz) {
emitdatawbyte((nb & 255u64): u8);
nb = nb >> 8u64;
b += 1;
};
i += 1;
};
emitline("\"\n");
};
};
};
};
d = d.next;
};
};
// emitdefconstants — DATA directive per top-level fold-to-literal
// `def`. 8 bytes little-endian to match what the C cgen emits.
// foldintliteral gates: int/rune literal, true/false/nil, and a
// unary +/-/~ over the same. `def NEG: i32 = -100;` arrives as
// N_UN(TK_MINUS, N_INTLIT) — the unary peel is exactly what the
// gate is for.
fn emitdefconstants(c: *cgen, file: *node) void = {
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_DEF) {
let r: *node = d.rhs;
let v: u64 = 0u64;
let ok: bool = false;
if (r != nil) {
ok = foldintliteral(r, &v);
};
if (ok) {
emitline("DATA ");
if (d.exported == 0) {
if (d.module.len > 0) {
os.write(1, d.module.ptr, d.module.len: u64);
os.write(1, ".".ptr, 1u64);
};
};
let nm: str = d.str;
os.write(1, nm.ptr, nm.len: u64);
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; };
os.write(1, 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; };
os.write(1, bb.ptr, 2u64);
} else {
let bb: [1]u8;
bb[0] = b;
os.write(1, 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;
os.write(1, 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; };
os.write(1, 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; };
os.write(1, bb.ptr, 2u64);
} else {
let bb: [1]u8;
bb[0] = b;
os.write(1, bb.ptr, 1u64);
};
};
};};};};};
i += 1;
};
emitline("\\x00\"\n");
s = s.slnext;
};
};
// ---- fn return-type map ---------------------------------------------
//
// Per-file: ident → ret-type-node. Used to decide whether to shuffle
// (AX, DX) → (AX, BX) after a CALL — needed for str-returning fns so
// the value flows through cgen as the canonical (AX, BX) str pair.
type fnret = struct {
fname: str,
fmod: str,
rtype: *node,
params: *node,
frnext: *fnret,
};
fn collectfnrets(c: *cgen, file: *node) void = {
c.fnrets = nil;
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_FNDECL) {
let f: *fnret = amalloc(c.a, 64u64): *fnret;
f.fname = d.str;
f.fmod = d.module;
f.rtype = d.lhs;
f.params = d.list;
f.frnext = c.fnrets;
c.fnrets = f;
};
d = d.next;
};
};
fn fnretlookup(c: *cgen, name: str) *node = {
let f: *fnret = c.fnrets;
for (f != nil) {
let fn_: str = f.fname;
if (streq(fn_, name)) { return f.rtype; };
f = f.frnext;
};
return nil;
};
// fnretlookupmod — same-module-first walk. Module-qualified `mod.fn(...)`
// callees route here so a leaf collision (same fn name exported from
// multiple modules) resolves to the explicit module. Falls back to the
// first leaf match if no matching module is registered. Mirror of
// fnparamslookupmod (#28); without this, matchscrutt's N_DOT branch
// picks the last-declared `next` regardless of qualifier, so a 4-arm
// `match (utf8.next(d))` inside a `fn next() (rune | done)` resolves
// the scrutinee tagged type to `(rune | done)` — flatvariantidx then
// can't see arms 2/3 and collapses them onto tag 0 (task #31).
fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = {
if (mod.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, mod)) { return f.rtype; };
};
f = f.frnext;
};
};
return fnretlookup(c, name);
};
// fnparamslookup — head of the declared param-list for a fn, or nil
// if the name isn't a registered fn. Used by cgcall / pushargsrev to
// detect implicit widening from a concrete variant into a tagged-union
// parameter slot.
fn fnparamslookup(c: *cgen, name: str) *node = {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) { return f.params; };
f = f.frnext;
};
return nil;
};
// fnparamslookupmod — same-module-first leaf walk. Module-qualified
// `mod.fn(...)` calls go through this so a leaf collision (multiple
// modules export the same name, e.g. `os.read` and `io.read`) resolves
// to the explicit module. Falls back to the first leaf match if no
// matching module is registered — mirrors aliaslookup's two-pass shape
// (cgen.ww:75, fixed in #27).
fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = {
if (mod.len > 0) {
let f: *fnret = c.fnrets;
for (f != nil) {
if (streq(f.fname, name)) {
if (streq(f.fmod, mod)) { return f.params; };
};
f = f.frnext;
};
};
return fnparamslookup(c, name);
};
// ---- def-constant registry ------------------------------------------
//
// `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an
// ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at
// file load and consult on nkind.N_IDENT lookup.
type defent = struct {
dname: str,
drhs: *node,
dnext: *defent,
};
fn collectdefs(c: *cgen, file: *node) void = {
c.defs = nil;
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_DEF) {
let e: *defent = amalloc(c.a, 32u64): *defent;
e.dname = d.str;
e.drhs = d.rhs;
e.dnext = c.defs;
c.defs = e;
};
d = d.next;
};
};
fn deflookup(c: *cgen, name: str) bool = {
let e: *defent = c.defs;
for (e != nil) {
let dn: str = e.dname;
if (streq(dn, 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. Used by cgdot to inline `.ptr`/`.len` on
// `def NAME: str = "..."` — those aren't laid out in memory.
fn deflookuprhs(c: *cgen, name: str) *node = {
let e: *defent = c.defs;
for (e != nil) {
let dn: str = e.dname;
if (streq(dn, name)) { return e.drhs; };
e = e.dnext;
};
return nil;
};
// ---- 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
module: str, // the originating module (`// MODULE: foo`)
mnext: *modent,
};
fn collectmods(c: *cgen, file: *node) void = {
c.mods = nil;
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
// Mirror collectfnrets' shape exactly (plain prepend in one
// branch). Earlier nested-if/early-return variants tickled a
// wwstage cgen bug that dropped most prepends.
if (d.kind == nkind.N_FNDECL) {
// Fns mangle regardless of export status — covers
// lib/os.read vs lib/io.read collision.
if (d.module.len > 0) {
let isffi: bool = false;
let a: *node = d.attr;
for (a != nil) {
if (a.kind == nkind.N_ATTR) {
let an: str = a.str;
if (streq(an, "symbol")) { isffi = true; };
};
a = a.next;
};
if (!isffi) {
if (!streq(d.str, "main")) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.module = d.module;
m.mnext = c.mods;
c.mods = m;
};
};
};
};
if (d.kind == nkind.N_DEF) {
if (d.exported == 0) {
if (d.module.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.module = d.module;
m.mnext = c.mods;
c.mods = m;
};
};
};
if (d.kind == nkind.N_TYPEDECL) {
if (d.exported == 0) {
if (d.module.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.module = d.module;
m.mnext = c.mods;
c.mods = m;
};
};
};
if (d.kind == nkind.N_LET) {
if (d.exported == 0) {
if (d.module.len > 0) {
let m: *modent = amalloc(c.a, 48u64): *modent;
m.mname = d.str;
m.module = d.module;
m.mnext = c.mods;
c.mods = m;
};
};
};
d = d.next;
};
};
fn modlookup(c: *cgen, name: str) str = {
let m: *modent = c.mods;
for (m != nil) {
if (streq(m.mname, name)) { return m.module; };
m = m.mnext;
};
let empty: str;
empty.ptr = nil;
empty.len = 0;
return empty;
};
// modlookupforfn — hint-aware lookup for fn names. Walks c.mods
// preferring entries where module matches `hint`; falls back to the
// first leaf-name match when nothing matches the hint (legacy single-
// owner shape, also covers lookups with hint.len==0). Needed because
// multiple modules can now register the same fn leaf — bare `lookup`
// would otherwise grab whichever module was prepended last.
fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
let m: *modent = c.mods;
let first: str;
first.ptr = nil;
first.len = 0;
for (m != nil) {
if (streq(m.mname, name)) {
if (hint.len > 0 && m.module.len > 0
&& streq(m.module, hint)) {
return m.module;
};
if (first.len == 0 && first.ptr == nil) {
first = m.module;
};
};
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.
os.write(1, resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookup(c, ident);
if (mod.len > 0) {
os.write(1, mod.ptr, mod.len: u64);
os.write(1, ".".ptr, 1u64);
};
os.write(1, 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) {
os.write(1, resolved.ptr, resolved.len: u64);
return;
};
let mod: str = modlookupforfn(c, ident, hint);
if (mod.len > 0) {
os.write(1, mod.ptr, mod.len: u64);
os.write(1, ".".ptr, 1u64);
};
os.write(1, ident.ptr, ident.len: u64);
};
// ---- FFI map ---------------------------------------------------------
fn fficollect(c: *cgen, file: *node) void = {
c.ffis = nil;
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_FNDECL) {
let a: *node = d.attr;
for (a != nil) {
if (a.kind == nkind.N_ATTR) {
let aname: str = a.str;
if (streq(aname, "symbol")) {
let symnode: *node = a.list;
if (symnode != nil) {
if (symnode.kind == nkind.N_STRLIT) {
let f: *ffi = amalloc(c.a, 48u64): *ffi;
f.ident = d.str;
f.symbol = symnode.str;
f.fnext = c.ffis;
c.ffis = f;
};
};
};
};
a = a.next;
};
};
d = d.next;
};
};
fn ffiresolve(c: *cgen, ident: str) str = {
let f: *ffi = c.ffis;
for (f != nil) {
let id: str = f.ident;
if (streq(id, ident)) { return f.symbol; };
f = f.fnext;
};
return ident;
};
// ---- ABI argreg helpers ---------------------------------------------
fn argregname(i: i32) str = {
if (i == 0) { return "DI"; };
if (i == 1) { return "SI"; };
if (i == 2) { return "DX"; };
if (i == 3) { return "CX"; };
if (i == 4) { return "R8"; };
if (i == 5) { return "R9"; };
return "?";
};
// fargregname — XMM scalar-float arg registers (SysV: X0..X7).
// Parallel to argregname / sysv_argregs; float args advance their
// own counter so int and float arg slots don't conflict.
export fn fargregname(i: i32) str = {
if (i == 0) { return "X0"; };
if (i == 1) { return "X1"; };
if (i == 2) { return "X2"; };
if (i == 3) { return "X3"; };
if (i == 4) { return "X4"; };
if (i == 5) { return "X5"; };
if (i == 6) { return "X6"; };
if (i == 7) { return "X7"; };
return "?";
};