Latent silent miscompile: cstage + wwstage mklabel emitted <fn>_<prefix>_<seq> with no module qualification, so two top-level fns sharing a leaf across modules (e.g. bytes.index + strings.index) emitted colliding labels into the same combined .s. Last assembler symbol-definition won; JNE/JMP rel32 resolved to the wrong fn's body. Repro (HEAD pre-fix): two_modules_same_leaf row in 750 — mod1.locate + mod2.locate sharing match-over-(u8|[]u8)+for shape. mod1.locate's JMP misresolved into mod2's body, exit 10. Post-fix: exit 0. Latent already at HEAD: bytes.contains_match_next_1 + strings.contains_match_next_1 collide today but the corpus had no forwarding path that surfaced it. cmd/w6c/cgen.c + selfhost/cmd/wcc/cgen.ww mklabel: prepend <module>. when c->cur_mod / c.curmod non-NULL/non-empty. Plan-9 convention extension: TEXT directive already uses <module>.<fnname> (lex.c:18 a_isidcont accepts '.'); mklabel now mirrors that for local labels. Both stages symmetric per rule 10. Fragment input (no `package`) collapses to pre-fix shape — no cross-unit risk. 750_mklabel_modscoped: table-driven 3 rows x 2 stages = 6 sub-cases (two_modules_same_leaf, bytes_strings_contains, same_module_same_leaf non-regression). All required substrings asserted via grep + runtime rc check. make test 124/124; ww2==ww3==ww4 byte-id holds via 995_self_rebuild. @-prefix slot keys (cg_tagbase, cg_tagscr, @retscr) are orthogonal (local_alloc keys, not mklabel emissions).
2053 lines
61 KiB
Plaintext
2053 lines
61 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 (- ! ~ & *),
|
|
// 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 mem;
|
|
import ast;
|
|
import tok;
|
|
import typ;
|
|
import sym;
|
|
import 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.
|
|
import cgenutil;
|
|
import cgenexpr;
|
|
import cgenstmt;
|
|
import cgendecl;
|
|
|
|
// ---- typedef alias registry -----------------------------------------
|
|
//
|
|
// `type error = str;` makes `error` a struct-shape alias. We track
|
|
// alias→target so isstrtype / isslicetype / structlookup can
|
|
// resolve through the chain. Only direct nkind.N_TNAME aliases are mapped;
|
|
// `type p = struct {...}` is handled by collectstructs.
|
|
|
|
type aliasent = struct {
|
|
aname: str,
|
|
amod: str, // originating module (`// MODULE: foo`), or empty
|
|
target: *node, // the rhs type expr
|
|
aanext: *aliasent,
|
|
};
|
|
|
|
fn collectaliases(c: *cgen, file: *node) void = {
|
|
c.aliases = nil;
|
|
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.nmod;
|
|
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.nmod;
|
|
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 = {
|
|
// Same-module first, then any. Trio-leaf graduation mirroring
|
|
// aliaslookup (#27) and fnret/fnparamslookupmod (#28/#31): without
|
|
// the prefer pass a bare-leaf enum ident in module M can collapse
|
|
// onto another module's same-leaf enum prepended earlier in
|
|
// c.enums, silently folding `Foo.MEMBER` to the wrong constant.
|
|
let e: *enumtype = c.enums;
|
|
for (e != nil) {
|
|
if (streq(e.ename, name)) {
|
|
if (streq(e.emod, c.curmod)) { return e; };
|
|
};
|
|
e = e.etnext;
|
|
};
|
|
e = c.enums;
|
|
for (e != nil) {
|
|
if (streq(e.ename, name)) { return e; };
|
|
e = e.etnext;
|
|
};
|
|
// Module-qualified form embedded in name (`pkg.enum`): scope the
|
|
// leaf to its originating module. The `emod == pkg` guard prevents
|
|
// same-leaf enums in two modules from collapsing.
|
|
let i: i32 = name.len - 1;
|
|
for (i >= 0) {
|
|
if (name[i] == 46u8) { // '.'
|
|
let pkg: str;
|
|
pkg.ptr = name.ptr;
|
|
pkg.len = i;
|
|
let leaf: str;
|
|
leaf.ptr = name.ptr + ((i + 1): u64);
|
|
leaf.len = name.len - (i + 1);
|
|
let b: *enumtype = c.enums;
|
|
for (b != nil) {
|
|
if (streq(b.ename, leaf)) {
|
|
if (streq(b.emod, pkg)) {
|
|
return b;
|
|
};
|
|
};
|
|
b = b.etnext;
|
|
};
|
|
return nil;
|
|
};
|
|
i -= 1;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// enumlookupmod — same-module-first leaf walk for `pkg.Enum.MEMBER`
|
|
// where the qualifier is an explicit N_IDENT module name. Mirrors
|
|
// fnparamslookupmod / fnretlookupmod (#28 / #31). Falls back to the
|
|
// bare enumlookup so a missing or empty mod still finds the leaf.
|
|
fn enumlookupmod(c: *cgen, name: str, mod: str) *enumtype = {
|
|
if (mod.len > 0) {
|
|
let e: *enumtype = c.enums;
|
|
for (e != nil) {
|
|
if (streq(e.ename, name)) {
|
|
if (streq(e.emod, mod)) { return e; };
|
|
};
|
|
e = e.etnext;
|
|
};
|
|
};
|
|
return enumlookup(c, name);
|
|
};
|
|
|
|
fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = {
|
|
let m: *enummember = en.members;
|
|
for (m != nil) {
|
|
if (streq(m.mname, mname)) {
|
|
*out = m.mval;
|
|
return true;
|
|
};
|
|
m = m.emnext;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// resolvetype — follow typedef alias chains to a "canonical" type
|
|
// expr (str/slice/array/struct/...). Stops on cycles via depth limit.
|
|
fn resolvetype(c: *cgen, t: *node) *node = {
|
|
let cur: *node = t;
|
|
let depth: i32 = 0;
|
|
for (depth < 16) {
|
|
if (cur == nil) { return nil; };
|
|
if (cur.kind != nkind.N_TNAME) { return cur; };
|
|
let nm: str = cur.str;
|
|
let next: *node = aliaslookup(c, nm);
|
|
if (next == nil) { return cur; };
|
|
cur = next;
|
|
depth += 1;
|
|
};
|
|
return cur;
|
|
};
|
|
|
|
// ---- struct registry ------------------------------------------------
|
|
//
|
|
// Per-file map from struct name → list of fields with computed offsets
|
|
// and sizes. Built when cgfile walks nkind.N_TYPEDECL with nkind.N_TSTRUCT lhs.
|
|
// nkind.N_DOT and nkind.N_ASSIGN consult this to resolve `s.field` for struct or
|
|
// *struct bases.
|
|
|
|
type fieldinfo = struct {
|
|
fname: str,
|
|
foff: i32,
|
|
fsz: i32,
|
|
tnode: *node, // the field type expr, for nested struct lookups
|
|
finext: *fieldinfo,
|
|
};
|
|
|
|
type structinfo = struct {
|
|
sname: str,
|
|
smod: str, // originating module (`// MODULE: foo`), or empty
|
|
fields: *fieldinfo,
|
|
totsize: i32,
|
|
sinext: *structinfo,
|
|
};
|
|
|
|
// ---- locals / frame --------------------------------------------------
|
|
|
|
type local = struct {
|
|
name: str,
|
|
off: i32,
|
|
sz: i32, // allocated slot size; carried so @-prefix reuse can
|
|
// fail-loud (rule 7) if a later site needs a larger
|
|
// slot than the first allocation pinned. Per #15/#26c
|
|
// size-strategy convergence — wwstage dropped its
|
|
// scanlocals pre-pass, so @tagscr/@retscr/@sretscr/
|
|
// @tagbase are sized at first-use; subsequent uses
|
|
// must fit.
|
|
tnode: *node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil
|
|
lnext: *local,
|
|
};
|
|
|
|
// strlit — interned string literal record. Emitted as a DATA directive
|
|
// after all functions; cgexpr nkind.N_STRLIT loads (LEAQ ptr, MOVQ len).
|
|
type strlit = struct {
|
|
label: str, // "_S_<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,
|
|
// atlocals — persistent registry of `@`-prefix scratch slots
|
|
// for the current fn. cgblock save/restores c.locals to scope
|
|
// inner shadows (post-#27); a return/cgindex/cgwidentaggedstore
|
|
// inside one block must not reallocate @retscr/@tagscr when a
|
|
// sibling block uses them again. cgblock leaves atlocals alone
|
|
// so the slot offsets survive. localadd checks here first for
|
|
// @-prefix names; localfind falls back here when c.locals misses
|
|
// an @-name. Pre-#15 this was a handful of named offsets on the
|
|
// cgen (c.retscroff / c.sretargoff / c.sretscroff); post-#15
|
|
// every @-name flows through the same registry.
|
|
atlocals: *local,
|
|
frame: i32,
|
|
lastwasreturn: i32,
|
|
labelseq: i32,
|
|
strlitseq: i32,
|
|
strlits: *strlit,
|
|
ffis: *ffi,
|
|
defs: *defent,
|
|
fnrets: *fnret,
|
|
aliases: *aliasent,
|
|
structs: *structinfo,
|
|
enums: *enumtype,
|
|
mods: *modent, // fn (any export status) + non-exported
|
|
// let/def/type decls → originating module
|
|
lets: *letvar, // top-level mutable scalar `let` bindings
|
|
fnname: str,
|
|
curmod: str, // current fn's `// MODULE: foo` directive (len=0
|
|
// when the fn is in the primary file). Drives
|
|
// bare-IDENT call mangling — `frob()` from
|
|
// inside lib/foo binds to `foo.frob` even when
|
|
// other modules also export `frob`. Set in cgfn
|
|
// before walking the body.
|
|
fnret: *node, // declared return type of current fn (or nil)
|
|
looptop: i32,
|
|
loopendbuf: *str, // stack of end labels for break
|
|
loopcontbuf: *str, // stack of cont labels for continue
|
|
yieldtop: i32,
|
|
yieldbuf: *str, // stack of match end labels for yield
|
|
defertop: i32,
|
|
deferbuf: **node, // stack of deferred exprs (LIFO at return)
|
|
// Variadic-call gather state. cgcall assigns per-call scratch
|
|
// names `@vararg_d_N` / `@vararg_sl_N` using this counter;
|
|
// post #15 the seq is bumped only at emit time so a single
|
|
// sequence is observed (the scanlocals pre-pass was dropped).
|
|
varargseq: i32,
|
|
// System V AMD64 sret discipline (#23). Plain TY_STRUCT returns
|
|
// with size > 24B are passed via a hidden first-arg pointer
|
|
// (RDI) to a caller-prealloc dest; the callee writes through
|
|
// that pointer and returns it in RAX.
|
|
//
|
|
// sretdestoff — caller-side dest BP offset, propagated from a
|
|
// receive site (cglet / cgassign ident) to the
|
|
// nested cgexpr → cgcall so the call emits
|
|
// `LEAQ off(BP), DI` instead of allocating a
|
|
// scratch. 0 means no receiver wired.
|
|
// sretforward — set by cgreturn `return f();` from an sret callee to
|
|
// signal cgcall: source RDI for inner from outer's
|
|
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
|
|
// dest. Inner writes into outer's caller-prealloc;
|
|
// inner's RAX (the dest pointer) is already outer's
|
|
// return value. Cleared after cgcall consumes it.
|
|
//
|
|
// The single-slot caches for @sretarg / @sretscr / @retscr that
|
|
// used to live here are gone: localadd's `@`-prefix dedup against
|
|
// c.locals (fail-loud on size grow) is the SSoT now. cgenstmt /
|
|
// cgenexpr resolve `@sretarg` via localfind when they need the
|
|
// saved RDI.
|
|
sretdestoff: i32,
|
|
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.atlocals = nil;
|
|
c.frame = 0;
|
|
c.lastwasreturn = 0;
|
|
c.labelseq = 0;
|
|
c.varargseq = 0;
|
|
c.sretdestoff = 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 cstage allocates via cgexpr's by-value
|
|
// `locals` list — so two separate matches each get fresh slots even
|
|
// when their bind names collide.
|
|
fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
|
let asz: i32 = sz;
|
|
if (asz < 8) { asz = 8; };
|
|
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
|
|
c.frame += asz;
|
|
let off: i32 = 0 - c.frame;
|
|
let l: *local = amalloc(c.a, 48u64): *local;
|
|
l.name = name;
|
|
l.off = off;
|
|
l.sz = asz;
|
|
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.sz = 0;
|
|
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
|
|
// cstage's localoff. Both stages had the same silent-stack-
|
|
// corruption bug: an inner 8B `let a: i64` allocated first
|
|
// would force a later outer `let a: [128]u8` onto the 8B slot,
|
|
// and `a[127]` would write at +119(BP), past the saved RIP.
|
|
//
|
|
// `@`-prefix scratch slots (`@tagscr`, `@retscr`, `@tagbase`,
|
|
// `@sretarg`, `@sretscr`, `@match_spill`, `@vararg_*`) share
|
|
// one slot per name per fn. Post #15/#26c the slot is sized
|
|
// at first use and reused by every later caller; a later
|
|
// caller asking for a larger slot than the first allocation
|
|
// pinned fatals (rule 7 — surface, don't silently corrupt
|
|
// the frame: the pinned offset already neighbours other
|
|
// locals so the slot can't grow in place). Mirrors cstage's
|
|
// cg_tagscr / cg_retscr / cg_sretscr same-fn caches in
|
|
// cmd/w6c/cgen.c (#26 / #15).
|
|
if (name.len > 0) {
|
|
if (name[0] == 64u8) { // '@'
|
|
let asz: i32 = sz;
|
|
if (asz < 8) { asz = 8; };
|
|
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
|
|
let cur: *local = c.atlocals;
|
|
for (cur != nil) {
|
|
let cn: str = cur.name;
|
|
if (streq(cn, name)) {
|
|
if (asz > cur.sz) {
|
|
// rule-7 surface, post-#15: pinned slot
|
|
// offset can't grow in place.
|
|
let msg: str = "localadd: @-prefix slot grew within fn\n";
|
|
os.write(2, msg.ptr, msg.len: u64);
|
|
os.exit(1);
|
|
};
|
|
cur.tnode = tnode;
|
|
return cur.off;
|
|
};
|
|
cur = cur.lnext;
|
|
};
|
|
// First use: allocate via localalloc (bumps c.frame +
|
|
// pushes to c.locals so localfind sees it within this
|
|
// block) and pin a parallel entry in c.atlocals so the
|
|
// allocation survives cgblock save/restore.
|
|
let off: i32 = localalloc(c, name, sz, tnode);
|
|
let at: *local = amalloc(c.a, 48u64): *local;
|
|
at.name = name;
|
|
at.off = off;
|
|
at.sz = asz;
|
|
at.tnode = tnode;
|
|
at.lnext = c.atlocals;
|
|
c.atlocals = at;
|
|
return off;
|
|
};
|
|
};
|
|
return localalloc(c, name, sz, tnode);
|
|
};
|
|
|
|
fn localfindnode(c: *cgen, name: str) *local = {
|
|
let l: *local = c.locals;
|
|
for (l != nil) {
|
|
let ln: str = l.name;
|
|
if (streq(ln, name)) { return l; };
|
|
l = l.lnext;
|
|
};
|
|
// @-prefix scratch slots survive cgblock save/restore via
|
|
// c.atlocals; a localfindnode from a sibling/outer block must
|
|
// still resolve them.
|
|
if (name.len > 0) {
|
|
if (name[0] == 64u8) {
|
|
let a: *local = c.atlocals;
|
|
for (a != nil) {
|
|
if (streq(a.name, name)) { return a; };
|
|
a = a.lnext;
|
|
};
|
|
};
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
fn localfind(c: *cgen, name: str) i32 = {
|
|
let l: *local = c.locals;
|
|
for (l != nil) {
|
|
let ln: str = l.name;
|
|
if (ln.len == name.len) {
|
|
let i: i32 = 0;
|
|
let eq: bool = true;
|
|
for (i < name.len) {
|
|
if (ln[i] != name[i]) { eq = false; i = name.len; }
|
|
else { i += 1; };
|
|
};
|
|
if (eq) { return l.off; };
|
|
};
|
|
l = l.lnext;
|
|
};
|
|
if (name.len > 0) {
|
|
if (name[0] == 64u8) {
|
|
let a: *local = c.atlocals;
|
|
for (a != nil) {
|
|
if (streq(a.name, name)) { return a.off; };
|
|
a = a.lnext;
|
|
};
|
|
};
|
|
};
|
|
return 0;
|
|
};
|
|
|
|
// ---- emit helpers ---------------------------------------------------
|
|
|
|
// Cgfn defers its prologue (TEXT / SUBQ) until after the body so the
|
|
// frame size reflects every emit-time localadd — the scanlocals pre-
|
|
// pass that previously pre-computed it was dropped per #15/#26c. The
|
|
// body is captured into cgoutbuf 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).
|
|
let cgoutbuf: *u8 = nil;
|
|
let cgoutbufcap: i32 = 0;
|
|
let cgoutbuflen: i32 = 0;
|
|
let cgoutmode: i32 = 0;
|
|
let cgoutarena: *arena = nil;
|
|
|
|
def CGOUT_INIT_CAP: i32 = 65536;
|
|
|
|
fn cgout_grow(need: i32) void = {
|
|
if (need <= cgoutbufcap) { return; };
|
|
let want: i32 = cgoutbufcap;
|
|
if (want == 0) { want = CGOUT_INIT_CAP; };
|
|
for (want < need) { want = want * 2; };
|
|
let p: *u8 = amalloc(cgoutarena, want: u64): *u8;
|
|
let i: i32 = 0;
|
|
for (i < cgoutbuflen) {
|
|
p[i] = cgoutbuf[i];
|
|
i += 1;
|
|
};
|
|
cgoutbuf = p;
|
|
cgoutbufcap = want;
|
|
};
|
|
|
|
fn cgout_enable(a: *arena) void = {
|
|
cgoutarena = a;
|
|
cgoutbuflen = 0;
|
|
cgoutmode = 1;
|
|
};
|
|
|
|
fn cgout_disable() void = { cgoutmode = 0; };
|
|
|
|
fn cgout_flush() void = {
|
|
if (cgoutbuflen > 0) {
|
|
os.write(1, cgoutbuf, cgoutbuflen: u64);
|
|
cgoutbuflen = 0;
|
|
};
|
|
};
|
|
|
|
fn emitbytes(p: *u8, n: u64) void = {
|
|
if (cgoutmode != 0) {
|
|
let nn: i32 = n: i32;
|
|
cgout_grow(cgoutbuflen + nn);
|
|
let i: i32 = 0;
|
|
for (i < nn) {
|
|
cgoutbuf[cgoutbuflen + i] = p[i];
|
|
i += 1;
|
|
};
|
|
cgoutbuflen += nn;
|
|
} 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(")");
|
|
};
|
|
|
|
// 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] = 46u8; i += 1; }; // '.'
|
|
let fname: str = c.fnname;
|
|
j = 0;
|
|
for (j < fname.len) {
|
|
buf[i] = fname[j];
|
|
i += 1; j += 1;
|
|
};
|
|
buf[i] = 95u8; i += 1; // '_'
|
|
j = 0;
|
|
for (j < prefix.len) {
|
|
buf[i] = prefix[j];
|
|
i += 1; j += 1;
|
|
};
|
|
buf[i] = 95u8; i += 1; // '_'
|
|
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC);
|
|
let n: i32 = ns.len;
|
|
let dk: i32 = 0;
|
|
for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; };
|
|
c.labelseq += 1;
|
|
let total: i32 = i + n;
|
|
let p: *u8 = 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 = {
|
|
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] = 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; };
|
|
emitbytes( bb.ptr, 2u64);
|
|
return;
|
|
};
|
|
if (b >= 127u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
emitbytes( bb.ptr, 2u64);
|
|
return;
|
|
};
|
|
let bb: [1]u8;
|
|
bb[0] = b;
|
|
emitbytes( bb.ptr, 1u64);
|
|
};
|
|
|
|
// letpreintern — intern strlits referenced from top-level str-let
|
|
// initialisers BEFORE emitdatasection runs. Mirrors cmd/w6c/cgen.c
|
|
// let_pre_intern: emitletdataw later looks up the same label, and
|
|
// emitdatasection emits the DATA row in the same .s file. Running
|
|
// emitletdataw after emitdatasection would flip the (DATA strlits,
|
|
// DATAW lets) section order and break byte-identity.
|
|
export fn letpreintern(c: *cgen, file: *node) void = {
|
|
if (file == nil) { return; };
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_LET) {
|
|
let sz: i32 = letemitsize(c, d);
|
|
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),");
|
|
emitbytes( lab.ptr, lab.len: u64);
|
|
emitline("(SB)\n");
|
|
} else {
|
|
// zero-init: accept no rhs, nil,
|
|
// or empty strlit.
|
|
let ok: bool = true;
|
|
if (d.rhs != nil) {
|
|
ok = false;
|
|
if (r != nil) {
|
|
if (r.kind == nkind.N_NIL) { ok = true; };
|
|
if (r.kind == nkind.N_STRLIT) {
|
|
if (r.str.len == 0) { ok = true; };
|
|
};
|
|
};
|
|
};
|
|
if (ok) {
|
|
emitline("DATAW ");
|
|
emitsymname(c, nm);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
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.nmod.len > 0) {
|
|
emitbytes( d.nmod.ptr, d.nmod.len: u64);
|
|
emitbytes( ".".ptr, 1u64);
|
|
};
|
|
};
|
|
let nm: str = d.str;
|
|
emitbytes( 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; };
|
|
emitbytes( bb.ptr, 2u64);
|
|
} else {
|
|
if (b >= 127u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
emitbytes( bb.ptr, 2u64);
|
|
} else {
|
|
let bb: [1]u8;
|
|
bb[0] = b;
|
|
emitbytes( bb.ptr, 1u64);
|
|
};
|
|
};
|
|
};};
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
// emitdatasection — DATA directives for every interned strlit.
|
|
// Trailing NUL appended so .ptr can be used as a C string by syscalls.
|
|
fn emitdatasection(c: *cgen) void = {
|
|
let s: *strlit = c.strlits;
|
|
for (s != nil) {
|
|
emitline("DATA ");
|
|
let lab: str = s.label;
|
|
emitbytes( lab.ptr, lab.len: u64);
|
|
emitline("(SB),\"");
|
|
let bs: str = s.bytes;
|
|
let i: i32 = 0;
|
|
for (i < bs.len) {
|
|
let b: u8 = bs[i];
|
|
if (b == 34u8) { emitline("\\\""); } // "
|
|
else { if (b == 92u8) { emitline("\\\\"); } // \
|
|
else { if (b == 10u8) { emitline("\\n"); }
|
|
else { if (b == 9u8) { emitline("\\t"); }
|
|
else { if (b == 13u8) { emitline("\\r"); }
|
|
else {
|
|
if (b < 32u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
emitbytes( bb.ptr, 2u64);
|
|
} else {
|
|
if (b >= 127u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
emitbytes( bb.ptr, 2u64);
|
|
} else {
|
|
let bb: [1]u8;
|
|
bb[0] = b;
|
|
emitbytes( bb.ptr, 1u64);
|
|
};
|
|
};
|
|
};};};};};
|
|
i += 1;
|
|
};
|
|
emitline("\\x00\"\n");
|
|
s = s.slnext;
|
|
};
|
|
};
|
|
|
|
// ---- fn return-type map ---------------------------------------------
|
|
//
|
|
// Per-file: ident → ret-type-node. Used to decide whether to shuffle
|
|
// (AX, DX) → (AX, BX) after a CALL — needed for str-returning fns so
|
|
// the value flows through cgen as the canonical (AX, BX) str pair.
|
|
|
|
type fnret = struct {
|
|
fname: str,
|
|
fmod: str,
|
|
rtype: *node,
|
|
params: *node,
|
|
frnext: *fnret,
|
|
};
|
|
|
|
fn collectfnrets(c: *cgen, file: *node) void = {
|
|
c.fnrets = nil;
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_FNDECL) {
|
|
let f: *fnret = amalloc(c.a, 64u64): *fnret;
|
|
f.fname = d.str;
|
|
f.fmod = d.nmod;
|
|
f.rtype = d.lhs;
|
|
f.params = d.list;
|
|
f.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.
|
|
fn fnretlookup(c: *cgen, name: str) *node = {
|
|
let f: *fnret = c.fnrets;
|
|
for (f != nil) {
|
|
if (streq(f.fname, name)) {
|
|
if (streq(f.fmod, c.curmod)) { return f.rtype; };
|
|
};
|
|
f = f.frnext;
|
|
};
|
|
f = c.fnrets;
|
|
for (f != nil) {
|
|
if (streq(f.fname, name)) { return f.rtype; };
|
|
f = f.frnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// fnretlookupmod — same-module-first walk. Module-qualified `mod.fn(...)`
|
|
// callees route here so a leaf collision (same fn name exported from
|
|
// multiple modules) resolves to the explicit module. Falls back to the
|
|
// first leaf match if no matching module is registered. Mirror of
|
|
// fnparamslookupmod (#28); without this, matchscrutt's N_DOT branch
|
|
// picks the last-declared `next` regardless of qualifier, so a 4-arm
|
|
// `match (utf8.next(d))` inside a `fn next() (rune | done)` resolves
|
|
// the scrutinee tagged type to `(rune | done)` — flatvariantidx then
|
|
// can't see arms 2/3 and collapses them onto tag 0 (task #31).
|
|
fn fnretlookupmod(c: *cgen, name: str, mod: str) *node = {
|
|
if (mod.len > 0) {
|
|
let f: *fnret = c.fnrets;
|
|
for (f != nil) {
|
|
if (streq(f.fname, name)) {
|
|
if (streq(f.fmod, mod)) { return f.rtype; };
|
|
};
|
|
f = f.frnext;
|
|
};
|
|
};
|
|
return fnretlookup(c, name);
|
|
};
|
|
|
|
// fnparamslookup — head of the declared param-list for a fn, or nil
|
|
// if the name isn't a registered fn. Same-module-first walk before the
|
|
// head-walk fallback. Trio-leaf graduation (#4d) mirroring aliaslookup
|
|
// (#27), fnret/fnparamslookupmod (#28/#31), enum/struct/deflookup
|
|
// (#4a/#4b/#4c): without the prefer pass a bare-leaf `foo(x)` call in
|
|
// module M (callee N_IDENT) silently picks another module's same-leaf
|
|
// `foo` from the head of c.fnrets, then pushargsrev's widening
|
|
// detection fires (or doesn't) against the wrong param-type — `foo(7)`
|
|
// against a same-leaf `(i32 | void)` param re-layouts 7 into a 2-word
|
|
// tagged slot vs the same-module `i32` param's single push.
|
|
fn fnparamslookup(c: *cgen, name: str) *node = {
|
|
let f: *fnret = c.fnrets;
|
|
for (f != nil) {
|
|
if (streq(f.fname, name)) {
|
|
if (streq(f.fmod, c.curmod)) { return f.params; };
|
|
};
|
|
f = f.frnext;
|
|
};
|
|
f = c.fnrets;
|
|
for (f != nil) {
|
|
if (streq(f.fname, name)) { return f.params; };
|
|
f = f.frnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// fnparamslookupmod — same-module-first leaf walk. Module-qualified
|
|
// `mod.fn(...)` calls go through this so a leaf collision (multiple
|
|
// modules export the same name, e.g. `os.read` and `io.read`) resolves
|
|
// to the explicit module. Falls back to the first leaf match if no
|
|
// matching module is registered — mirrors aliaslookup's two-pass shape
|
|
// (cgen.ww:75, fixed in #27).
|
|
fn fnparamslookupmod(c: *cgen, name: str, mod: str) *node = {
|
|
if (mod.len > 0) {
|
|
let f: *fnret = c.fnrets;
|
|
for (f != nil) {
|
|
if (streq(f.fname, name)) {
|
|
if (streq(f.fmod, mod)) { return f.params; };
|
|
};
|
|
f = f.frnext;
|
|
};
|
|
};
|
|
return fnparamslookup(c, name);
|
|
};
|
|
|
|
// ---- def-constant registry ------------------------------------------
|
|
//
|
|
// `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an
|
|
// ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at
|
|
// file load and consult on nkind.N_IDENT lookup.
|
|
|
|
type defent = struct {
|
|
dname: str,
|
|
dmod: str, // originating module (`// MODULE: foo`), or empty
|
|
drhs: *node,
|
|
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, 64u64): *defent;
|
|
e.dname = d.str;
|
|
e.dmod = d.nmod;
|
|
e.drhs = d.rhs;
|
|
e.dnext = c.defs;
|
|
c.defs = e;
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
// Same-module-first walk, then any. Trio-leaf graduation mirroring
|
|
// aliaslookup (#27) and enum/structlookup (#4a/#4b): bool answer is
|
|
// invariant either way, but the structural shape mirrors deflookuprhs
|
|
// where the entry's drhs IS module-sensitive.
|
|
fn deflookup(c: *cgen, name: str) bool = {
|
|
let e: *defent = c.defs;
|
|
for (e != nil) {
|
|
if (streq(e.dname, name)) {
|
|
if (streq(e.dmod, c.curmod)) { return true; };
|
|
};
|
|
e = e.dnext;
|
|
};
|
|
e = c.defs;
|
|
for (e != nil) {
|
|
if (streq(e.dname, name)) { return true; };
|
|
e = e.dnext;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// Returns the rhs init node for a top-level `def`, or nil if `name`
|
|
// doesn't name a def. Same-module-first walk: without the prefer pass
|
|
// `MSG.ptr`/`MSG.len` in module M can collapse onto another module's
|
|
// same-leaf `def MSG: str = ...` sitting at the head of c.defs and
|
|
// inline the wrong strlit. Used by cgdot to inline `.ptr`/`.len` on
|
|
// `def NAME: str = "..."` — those aren't laid out in memory.
|
|
fn deflookuprhs(c: *cgen, name: str) *node = {
|
|
let e: *defent = c.defs;
|
|
for (e != nil) {
|
|
if (streq(e.dname, name)) {
|
|
if (streq(e.dmod, c.curmod)) { return e.drhs; };
|
|
};
|
|
e = e.dnext;
|
|
};
|
|
e = c.defs;
|
|
for (e != nil) {
|
|
if (streq(e.dname, name)) { return e.drhs; };
|
|
e = e.dnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// deflookuprhsmod — same-module-first walk for `mod.NAME` references.
|
|
// Trio-leaf *mod variant mirroring fnretlookupmod (#31) / fnparamslookupmod
|
|
// (#28) / enumlookupmod (#4a). Module-qualified `alpha.MSG` from a third
|
|
// module needs the explicit alpha hint; deflookuprhs prefers c.curmod
|
|
// (which doesn't match either source module on a 3rd-module qualifier)
|
|
// and falls back to head-pick, possibly inlining beta.MSG's strlit when
|
|
// both alpha and beta declare same-leaf str defs. cgdot's mod-qualified
|
|
// str-def value-load routes here so a cross-module N_DOT collision
|
|
// resolves to the explicit module. Falls back to deflookuprhs's bare-
|
|
// leaf two-pass when no module matches.
|
|
fn deflookuprhsmod(c: *cgen, name: str, mod: str) *node = {
|
|
if (mod.len > 0) {
|
|
let e: *defent = c.defs;
|
|
for (e != nil) {
|
|
if (streq(e.dname, name)) {
|
|
if (streq(e.dmod, mod)) { return e.drhs; };
|
|
};
|
|
e = e.dnext;
|
|
};
|
|
};
|
|
return deflookuprhs(c, name);
|
|
};
|
|
|
|
// ---- 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`)
|
|
mnext: *modent,
|
|
};
|
|
|
|
fn collectmods(c: *cgen, file: *node) void = {
|
|
c.mods = nil;
|
|
if (file == nil) { return; };
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
// Mirror collectfnrets' shape exactly (plain prepend in one
|
|
// branch). Earlier nested-if/early-return variants tickled a
|
|
// wwstage cgen bug that dropped most prepends.
|
|
if (d.kind == nkind.N_FNDECL) {
|
|
// Fns mangle regardless of export status — covers
|
|
// lib/os.read vs lib/io.read collision.
|
|
if (d.nmod.len > 0) {
|
|
let isffi: bool = false;
|
|
let a: *node = d.attr;
|
|
for (a != nil) {
|
|
if (a.kind == nkind.N_ATTR) {
|
|
let an: str = a.str;
|
|
if (streq(an, "symbol")) { isffi = true; };
|
|
};
|
|
a = a.next;
|
|
};
|
|
if (!isffi) {
|
|
if (!streq(d.str, "main")) {
|
|
let m: *modent = amalloc(c.a, 48u64): *modent;
|
|
m.mname = d.str;
|
|
m.nmod = d.nmod;
|
|
m.mnext = c.mods;
|
|
c.mods = m;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
if (d.kind == nkind.N_DEF) {
|
|
if (d.exported == 0) {
|
|
if (d.nmod.len > 0) {
|
|
let m: *modent = amalloc(c.a, 48u64): *modent;
|
|
m.mname = d.str;
|
|
m.nmod = d.nmod;
|
|
m.mnext = c.mods;
|
|
c.mods = m;
|
|
};
|
|
};
|
|
};
|
|
if (d.kind == nkind.N_TYPEDECL) {
|
|
if (d.exported == 0) {
|
|
if (d.nmod.len > 0) {
|
|
let m: *modent = amalloc(c.a, 48u64): *modent;
|
|
m.mname = d.str;
|
|
m.nmod = d.nmod;
|
|
m.mnext = c.mods;
|
|
c.mods = m;
|
|
};
|
|
};
|
|
};
|
|
if (d.kind == nkind.N_LET) {
|
|
if (d.exported == 0) {
|
|
if (d.nmod.len > 0) {
|
|
let m: *modent = amalloc(c.a, 48u64): *modent;
|
|
m.mname = d.str;
|
|
m.nmod = d.nmod;
|
|
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.nmod; };
|
|
m = m.mnext;
|
|
};
|
|
let empty: str;
|
|
empty.ptr = nil;
|
|
empty.len = 0;
|
|
return empty;
|
|
};
|
|
|
|
// modlookupforfn — hint-aware lookup for fn names. Walks c.mods
|
|
// preferring entries where module matches `hint`; falls back to the
|
|
// first leaf-name match when nothing matches the hint (legacy single-
|
|
// owner shape, also covers lookups with hint.len==0). Needed because
|
|
// multiple modules can now register the same fn leaf — bare `lookup`
|
|
// would otherwise grab whichever module was prepended last.
|
|
fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
|
|
let m: *modent = c.mods;
|
|
let first: str;
|
|
first.ptr = nil;
|
|
first.len = 0;
|
|
for (m != nil) {
|
|
if (streq(m.mname, name)) {
|
|
if (hint.len > 0 && m.nmod.len > 0
|
|
&& streq(m.nmod, hint)) {
|
|
return m.nmod;
|
|
};
|
|
if (first.len == 0 && first.ptr == nil) {
|
|
first = m.nmod;
|
|
};
|
|
};
|
|
m = m.mnext;
|
|
};
|
|
return first;
|
|
};
|
|
|
|
// emitsymname — write the asm symbol name for `ident`. Honours, in
|
|
// order: FFI mapping (@symbol), module mangling (private decls), bare
|
|
// name. Use everywhere a top-level non-fn name is emitted before `(SB)`
|
|
// — DATA labels for top-level lets/defs, address-of-let, etc. Fn names
|
|
// (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates
|
|
// cross-module same-leaf fn exports.
|
|
fn emitsymname(c: *cgen, ident: str) void = {
|
|
let resolved: str = ffiresolve(c, ident);
|
|
if (resolved.ptr != ident.ptr) {
|
|
// FFI hit — emit the mapped linker symbol verbatim.
|
|
emitbytes( resolved.ptr, resolved.len: u64);
|
|
return;
|
|
};
|
|
let mod: str = modlookup(c, ident);
|
|
if (mod.len > 0) {
|
|
emitbytes( mod.ptr, mod.len: u64);
|
|
emitbytes( ".".ptr, 1u64);
|
|
};
|
|
emitbytes( ident.ptr, ident.len: u64);
|
|
};
|
|
|
|
// emitfnname — write the asm symbol name for a fn `ident`, threading
|
|
// `hint` (the explicit module from a `mod.fn` use site, or c.curmod
|
|
// for bare-IDENT calls) through modlookupforfn. Same FFI override
|
|
// semantics as emitsymname; same dot-separator format. Use at every
|
|
// CALL / LEAQ-of-fn / TEXT-def site.
|
|
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
|
|
let resolved: str = ffiresolve(c, ident);
|
|
if (resolved.ptr != ident.ptr) {
|
|
emitbytes( resolved.ptr, resolved.len: u64);
|
|
return;
|
|
};
|
|
let mod: str = modlookupforfn(c, ident, hint);
|
|
if (mod.len > 0) {
|
|
emitbytes( mod.ptr, mod.len: u64);
|
|
emitbytes( ".".ptr, 1u64);
|
|
};
|
|
emitbytes( ident.ptr, ident.len: u64);
|
|
};
|
|
|
|
// ---- FFI map ---------------------------------------------------------
|
|
|
|
fn fficollect(c: *cgen, file: *node) void = {
|
|
c.ffis = nil;
|
|
if (file == nil) { return; };
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_FNDECL) {
|
|
let a: *node = d.attr;
|
|
for (a != nil) {
|
|
if (a.kind == nkind.N_ATTR) {
|
|
let aname: str = a.str;
|
|
if (streq(aname, "symbol")) {
|
|
let symnode: *node = a.list;
|
|
if (symnode != nil) {
|
|
if (symnode.kind == nkind.N_STRLIT) {
|
|
let f: *ffi = 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 "?";
|
|
};
|