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