Top-level `def NEG: i32 = -100;` skipped DATA emission in both stages — cstage's emit_defs and wwstage's emitdefconstants each carried a literal-leaf whitelist that excluded N_UN nodes. Same gap in check.c's eval_enum_value cstage-side. Surfaced during #10 (lib/os forced an `at` enum for AT_FDCWD=-100 etc. as workaround). Factor a single fold_int_literal helper (cstage check.c; wwstage cgen.ww). Handles N_INTLIT / N_RUNELIT / N_TRUE / N_FALSE / N_NIL plus N_UN with TK_MINUS / TK_TILDE / TK_PLUS recursively. Consume from eval_enum_value, emit_defs, emitdefconstants, enumevalmember — single source of truth for "is this a literal-leaf foldable". Side effect: cstage's def-emit set widens from {INTLIT, RUNELIT, TRUE} to match wwstage's pre-existing 5-shape set plus the new unary peel. Bootstrap byte-id holds (995_self_rebuild green). Test 631 (def_neg_global): 6 rows × cstage/wwstage run + asm byte-identity diff. Covers all three unary arms (-, ~, +), positive regression-pin, i32 + i64 + u32 slots. Follows up #26: revert lib/os.ww `at` enum to three top-level defs.
1744 lines
50 KiB
Plaintext
1744 lines
50 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.
|
|
|
|
use os;
|
|
use mem;
|
|
use ast;
|
|
use tok;
|
|
use typ;
|
|
use sym;
|
|
use strconv;
|
|
// Split files. Bundler pulls these in transitively so consumers only
|
|
// need `use cgen;`. Order matters for the flat-bundle concat — utils
|
|
// first so cgenexpr/stmt/decl can reference helpers defined here.
|
|
use cgenutil;
|
|
use cgenexpr;
|
|
use cgenstmt;
|
|
use cgendecl;
|
|
|
|
// ---- typedef alias registry -----------------------------------------
|
|
//
|
|
// `type error = str;` makes `error` a struct-shape alias. We track
|
|
// alias→target so isstrtype / isslicetype / structlookup can
|
|
// resolve through the chain. Only direct nkind.N_TNAME aliases are mapped;
|
|
// `type p = struct {...}` is handled by collectstructs.
|
|
|
|
type aliasent = struct {
|
|
aname: str,
|
|
amod: str, // originating module (`// MODULE: foo`), or empty
|
|
target: *node, // the rhs type expr
|
|
aanext: *aliasent,
|
|
};
|
|
|
|
fn collectaliases(c: *cgen, file: *node) void = {
|
|
c.aliases = nil;
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_TYPEDECL) {
|
|
let body: *node = d.lhs;
|
|
if (body != nil) {
|
|
if (body.kind != nkind.N_TSTRUCT) {
|
|
let a: *aliasent = amalloc(c.a, 64u64): *aliasent;
|
|
a.aname = d.str;
|
|
a.amod = d.module;
|
|
a.target = body;
|
|
a.aanext = c.aliases;
|
|
c.aliases = a;
|
|
};
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
fn aliaslookup(c: *cgen, name: str) *node = {
|
|
let a: *aliasent = c.aliases;
|
|
for (a != nil) {
|
|
let an: str = a.aname;
|
|
if (streq(an, name)) { return a.target; };
|
|
a = a.aanext;
|
|
};
|
|
// Module-qualified form: `pkg.alias` → match the leaf name
|
|
// scoped to its originating module. Mirrors check.c's module-
|
|
// qualified type resolution; requiring `amod == pkg` is what
|
|
// prevents two modules with same-leaf-name aliases from
|
|
// collapsing into whichever entry appears first in the chain.
|
|
let i: i32 = name.len - 1;
|
|
for (i >= 0) {
|
|
if (name[i] == 46u8) { // '.'
|
|
let pkg: str;
|
|
pkg.ptr = name.ptr;
|
|
pkg.len = i;
|
|
let leaf: str;
|
|
leaf.ptr = name.ptr + ((i + 1): u64);
|
|
leaf.len = name.len - (i + 1);
|
|
let b: *aliasent = c.aliases;
|
|
for (b != nil) {
|
|
if (streq(b.aname, leaf)) {
|
|
if (streq(b.amod, pkg)) {
|
|
return b.target;
|
|
};
|
|
};
|
|
b = b.aanext;
|
|
};
|
|
i = -1;
|
|
} else {
|
|
i -= 1;
|
|
};
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// ---- enum registry --------------------------------------------------
|
|
//
|
|
// Mirrors cmd/wcc/check.c's enum resolution at collect time: walk
|
|
// every `type Foo = enum [storage] { ... }`, pre-compute each
|
|
// member's u64 value (supporting auto-increment and sibling refs),
|
|
// and stash them so cgdot can fold `Foo.MEMBER` → MOVQ $value, AX.
|
|
|
|
// foldintliteral — fold the literal subset usable for top-level
|
|
// constant slots: int/rune literal, true/false/nil, and a unary
|
|
// +/-/~ over the same (any depth). No sibling-ident, no binary op.
|
|
// Shared between enumevalmember (literal leaves) and
|
|
// emitdefconstants (top-level def rhs).
|
|
//
|
|
// Whitelist kept tight on purpose: anything richer (sibling refs,
|
|
// arithmetic) belongs in enumevalmember, which calls this for its
|
|
// literal leaves and handles the rest itself.
|
|
fn foldintliteral(e: *node, out: *u64) bool = {
|
|
if (e == nil) { return false; };
|
|
let k: nkind = e.kind;
|
|
if (k == nkind.N_INTLIT) { *out = e.uval; return true; };
|
|
if (k == nkind.N_RUNELIT) { *out = e.uval; return true; };
|
|
if (k == nkind.N_TRUE) { *out = 1u64; return true; };
|
|
if (k == nkind.N_FALSE) { *out = 0u64; return true; };
|
|
if (k == nkind.N_NIL) { *out = 0u64; return true; };
|
|
if (k == nkind.N_UN) {
|
|
let v: u64;
|
|
if (!foldintliteral(e.lhs, &v)) { return false; };
|
|
let op: tkind = e.op;
|
|
if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
|
|
if (op == tkind.TK_TILDE) { *out = ~v; return true; };
|
|
if (op == tkind.TK_PLUS) { *out = v; return true; };
|
|
return false;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
fn enumevalmember(prev: *enummember, e: *node, out: *u64) bool = {
|
|
if (e == nil) { return false; };
|
|
if (foldintliteral(e, out)) { return true; };
|
|
let k: nkind = e.kind;
|
|
if (k == nkind.N_IDENT) {
|
|
let m: *enummember = prev;
|
|
for (m != nil) {
|
|
if (streq(m.mname, e.str)) {
|
|
*out = m.mval;
|
|
return true;
|
|
};
|
|
m = m.emnext;
|
|
};
|
|
return false;
|
|
};
|
|
if (k == nkind.N_BIN) {
|
|
let a: u64;
|
|
let b: u64;
|
|
if (!enumevalmember(prev, e.lhs, &a)) { return false; };
|
|
if (!enumevalmember(prev, e.rhs, &b)) { return false; };
|
|
let op: tkind = e.op;
|
|
if (op == tkind.TK_PLUS) { *out = a + b; return true; };
|
|
if (op == tkind.TK_MINUS) { *out = a - b; return true; };
|
|
if (op == tkind.TK_STAR) { *out = a * b; return true; };
|
|
if (op == tkind.TK_SLASH) {
|
|
if (b == 0u64) { return false; };
|
|
*out = a / b; return true;
|
|
};
|
|
if (op == tkind.TK_PERCENT) {
|
|
if (b == 0u64) { return false; };
|
|
*out = a % b; return true;
|
|
};
|
|
if (op == tkind.TK_AMP) { *out = a & b; return true; };
|
|
if (op == tkind.TK_PIPE) { *out = a | b; return true; };
|
|
if (op == tkind.TK_CARET) { *out = a ^ b; return true; };
|
|
if (op == tkind.TK_LSHIFT) { *out = a << b; return true; };
|
|
if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; };
|
|
return false;
|
|
};
|
|
if (k == nkind.N_UN) {
|
|
let v: u64;
|
|
if (!enumevalmember(prev, e.lhs, &v)) { return false; };
|
|
let op: tkind = e.op;
|
|
if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
|
|
if (op == tkind.TK_TILDE) { *out = ~v; return true; };
|
|
if (op == tkind.TK_PLUS) { *out = v; return true; };
|
|
return false;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
fn collectenums(c: *cgen, file: *node) void = {
|
|
c.enums = nil;
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_TYPEDECL) {
|
|
let body: *node = d.lhs;
|
|
if (body != nil) {
|
|
if (body.kind == nkind.N_TENUM) {
|
|
let et: *enumtype = amalloc(c.a, 64u64): *enumtype;
|
|
et.ename = d.str;
|
|
et.emod = d.module;
|
|
et.storage = body.lhs;
|
|
et.members = nil;
|
|
let prev: u64 = (-1i64): u64;
|
|
let mhead: *enummember = nil;
|
|
let mtail: *enummember = nil;
|
|
let m: *node = body.list;
|
|
for (m != nil) {
|
|
let val: u64;
|
|
if (m.lhs == nil) {
|
|
val = prev + 1u64;
|
|
} else {
|
|
if (!enumevalmember(mhead, m.lhs, &val)) {
|
|
val = prev + 1u64;
|
|
};
|
|
};
|
|
prev = val;
|
|
let em: *enummember = amalloc(c.a, 32u64): *enummember;
|
|
em.mname = m.str;
|
|
em.mval = val;
|
|
em.emnext = nil;
|
|
if (mhead == nil) { mhead = em; mtail = em; }
|
|
else { mtail.emnext = em; mtail = em; };
|
|
m = m.next;
|
|
};
|
|
et.members = mhead;
|
|
et.etnext = c.enums;
|
|
c.enums = et;
|
|
};
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
fn enumlookup(c: *cgen, name: str) *enumtype = {
|
|
// Exact match first: bare-from-source idents and already-leafed
|
|
// names hit here directly.
|
|
let e: *enumtype = c.enums;
|
|
for (e != nil) {
|
|
if (streq(e.ename, name)) { return e; };
|
|
e = e.etnext;
|
|
};
|
|
// Module-qualified form: `pkg.enum` → match the leaf scoped to
|
|
// its originating module. Mirrors aliaslookup's mod-filter; the
|
|
// `emod == pkg` guard is what prevents two modules with same-
|
|
// leaf-name enums from collapsing into whichever entry appears
|
|
// first in the chain.
|
|
let i: i32 = name.len - 1;
|
|
for (i >= 0) {
|
|
if (name[i] == 46u8) { // '.'
|
|
let pkg: str;
|
|
pkg.ptr = name.ptr;
|
|
pkg.len = i;
|
|
let leaf: str;
|
|
leaf.ptr = name.ptr + ((i + 1): u64);
|
|
leaf.len = name.len - (i + 1);
|
|
let b: *enumtype = c.enums;
|
|
for (b != nil) {
|
|
if (streq(b.ename, leaf)) {
|
|
if (streq(b.emod, pkg)) {
|
|
return b;
|
|
};
|
|
};
|
|
b = b.etnext;
|
|
};
|
|
return nil;
|
|
};
|
|
i -= 1;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
fn enummemberval(en: *enumtype, mname: str, out: *u64) bool = {
|
|
let m: *enummember = en.members;
|
|
for (m != nil) {
|
|
if (streq(m.mname, mname)) {
|
|
*out = m.mval;
|
|
return true;
|
|
};
|
|
m = m.emnext;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// resolvetype — follow typedef alias chains to a "canonical" type
|
|
// expr (str/slice/array/struct/...). Stops on cycles via depth limit.
|
|
fn resolvetype(c: *cgen, t: *node) *node = {
|
|
let cur: *node = t;
|
|
let depth: i32 = 0;
|
|
for (depth < 16) {
|
|
if (cur == nil) { return nil; };
|
|
if (cur.kind != nkind.N_TNAME) { return cur; };
|
|
let nm: str = cur.str;
|
|
let next: *node = aliaslookup(c, nm);
|
|
if (next == nil) { return cur; };
|
|
cur = next;
|
|
depth += 1;
|
|
};
|
|
return cur;
|
|
};
|
|
|
|
// ---- struct registry ------------------------------------------------
|
|
//
|
|
// Per-file map from struct name → list of fields with computed offsets
|
|
// and sizes. Built when cgfile walks nkind.N_TYPEDECL with nkind.N_TSTRUCT lhs.
|
|
// nkind.N_DOT and nkind.N_ASSIGN consult this to resolve `s.field` for struct or
|
|
// *struct bases.
|
|
|
|
type fieldinfo = struct {
|
|
fname: str,
|
|
foff: i32,
|
|
fsz: i32,
|
|
tnode: *node, // the field type expr, for nested struct lookups
|
|
finext: *fieldinfo,
|
|
};
|
|
|
|
type structinfo = struct {
|
|
sname: str,
|
|
smod: str, // originating module (`// MODULE: foo`), or empty
|
|
fields: *fieldinfo,
|
|
totsize: i32,
|
|
sinext: *structinfo,
|
|
};
|
|
|
|
// ---- locals / frame --------------------------------------------------
|
|
|
|
type local = struct {
|
|
name: str,
|
|
off: i32,
|
|
tnode: *node, // declared type expr (nkind.N_TNAME / nkind.N_TPTR / ...) or nil
|
|
lnext: *local,
|
|
};
|
|
|
|
// strlit — interned string literal record. Emitted as a DATA directive
|
|
// after all functions; cgexpr nkind.N_STRLIT loads (LEAQ ptr, MOVQ len).
|
|
type strlit = struct {
|
|
label: str, // "_S_<seq>"
|
|
bytes: str,
|
|
slnext: *strlit,
|
|
};
|
|
|
|
// ffi — `@symbol("name")` mapping. Body-less fn `foo` with this attr
|
|
// gets its CALL target rewritten to `name`.
|
|
type ffi = struct {
|
|
ident: str,
|
|
symbol: str,
|
|
fnext: *ffi,
|
|
};
|
|
|
|
// enummember — one (name, value) pair belonging to a registered enum.
|
|
// Values are pre-computed at collect time (Hare allows sibling refs
|
|
// like `RDWR = READ | WRITE`, so we walk the value expr against the
|
|
// already-resolved siblings). Lookup is linear; enum cardinality is
|
|
// usually small.
|
|
type enummember = struct {
|
|
mname: str,
|
|
mval: u64,
|
|
emnext: *enummember,
|
|
};
|
|
|
|
type enumtype = struct {
|
|
ename: str,
|
|
emod: str, // originating module (`// MODULE: foo`), or empty
|
|
storage: *node, // AST type expr for the storage type (i32 by default)
|
|
members: *enummember,
|
|
etnext: *enumtype,
|
|
};
|
|
|
|
def LOOP_MAX: i32 = 16;
|
|
def DEFER_MAX: i32 = 16;
|
|
|
|
type cgen = struct {
|
|
a: *arena,
|
|
locals: *local,
|
|
frame: i32,
|
|
lastwasreturn: i32,
|
|
labelseq: i32,
|
|
strlitseq: i32,
|
|
strlits: *strlit,
|
|
ffis: *ffi,
|
|
defs: *defent,
|
|
fnrets: *fnret,
|
|
aliases: *aliasent,
|
|
structs: *structinfo,
|
|
enums: *enumtype,
|
|
mods: *modent, // fn (any export status) + non-exported
|
|
// let/def/type decls → originating module
|
|
lets: *letvar, // top-level mutable scalar `let` bindings
|
|
fnname: str,
|
|
curmod: str, // current fn's `// MODULE: foo` directive (len=0
|
|
// when the fn is in the primary file). Drives
|
|
// bare-IDENT call mangling — `frob()` from
|
|
// inside lib/foo binds to `foo.frob` even when
|
|
// other modules also export `frob`. Set in cgfn
|
|
// before walking the body.
|
|
fnret: *node, // declared return type of current fn (or nil)
|
|
looptop: i32,
|
|
loopendbuf: *str, // stack of end labels for break
|
|
loopcontbuf: *str, // stack of cont labels for continue
|
|
yieldtop: i32,
|
|
yieldbuf: *str, // stack of match end labels for yield
|
|
defertop: i32,
|
|
deferbuf: **node, // stack of deferred exprs (LIFO at return)
|
|
// Variadic-call gather state. scanlocals walks the body in pre-
|
|
// order DFS and assigns per-call scratch names `@vararg_d_N` /
|
|
// `@vararg_sl_N` using this counter; cgcall resets and walks in
|
|
// the same order so the names line up at emission time.
|
|
varargseq: i32,
|
|
};
|
|
|
|
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
|
|
// Populated alongside modents; consulted by cgassign, cgdot, cgident
|
|
// and the TK_AMP path so reads/writes hit a RIP-relative DATAW slot
|
|
// instead of being silently dropped. tnode is the declared type AST
|
|
// node — needed to distinguish scalar (8B) from str (16B) globals
|
|
// when picking the load/store sequence.
|
|
type letvar = struct {
|
|
name: str,
|
|
tnode: *node,
|
|
lvnext: *letvar,
|
|
};
|
|
|
|
fn cgeninit(c: *cgen, a: *arena) void = {
|
|
c.a = a;
|
|
c.locals = nil;
|
|
c.frame = 0;
|
|
c.lastwasreturn = 0;
|
|
c.labelseq = 0;
|
|
c.varargseq = 0;
|
|
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
|
// persist across cgfn calls within one file. cgfile resets them
|
|
// at the start of each compilation unit.
|
|
c.looptop = 0;
|
|
c.loopendbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
|
|
c.loopcontbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
|
|
c.yieldtop = 0;
|
|
c.yieldbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
|
|
c.defertop = 0;
|
|
c.deferbuf = amalloc(a, (DEFER_MAX: u64) * 8u64): **node;
|
|
};
|
|
|
|
// localalloc — append a slot for `name` without dedup. Used for
|
|
// match-arm bindings, which C cgen allocates via cgexpr's by-value
|
|
// `locals` list — so two separate matches each get fresh slots even
|
|
// when their bind names collide. scanlocals follows the same rule
|
|
// for nkind.N_MCASE.
|
|
fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
|
let asz: i32 = sz;
|
|
if (asz < 8) { asz = 8; };
|
|
if ((asz & 7) != 0) { asz = (asz + 7) & ~7; };
|
|
c.frame += asz;
|
|
let off: i32 = 0 - c.frame;
|
|
let l: *local = amalloc(c.a, 48u64): *local;
|
|
l.name = name;
|
|
l.off = off;
|
|
l.tnode = tnode;
|
|
l.lnext = c.locals;
|
|
c.locals = l;
|
|
return off;
|
|
};
|
|
|
|
// localaddstack — register a param at a positive BP offset. Used for
|
|
// args that overflow the 6 SysV int / 8 float reg windows; the caller
|
|
// pushes them in reverse, so each spilled arg lives at 16(BP), 24(BP),
|
|
// etc. (after the saved RIP+BP). No spill instruction is emitted; the
|
|
// slot IS the caller's stack slot.
|
|
fn localaddstack(c: *cgen, name: str, tnode: *node, off: i32) void = {
|
|
let l: *local = amalloc(c.a, 48u64): *local;
|
|
l.name = name;
|
|
l.off = off;
|
|
l.tnode = tnode;
|
|
l.lnext = c.locals;
|
|
c.locals = l;
|
|
};
|
|
|
|
fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
|
|
// Name-based slot reuse for N_LETs and params: if `name` is
|
|
// already declared in this function, return its existing
|
|
// offset. Mirrors C cgen (cmd/w6c/cgen.c:localoff). Two
|
|
// disjoint scopes that declare the same name share one slot —
|
|
// so `escape` in wwdump (three `let cp: pos;` across separate
|
|
// branches) reserves one slot, not three. scanlocals does
|
|
// the matching dedup at prologue time so the SUBQ stays in
|
|
// sync.
|
|
//
|
|
// On a dedup hit we also overwrite the stored tnode to match
|
|
// the new declaration's type. C reads `n->lhs->type` (filled
|
|
// by the checker) at every nkind.N_DOT/nkind.N_CAST site; we read
|
|
// `lc.tnode`, so it must follow source order. Without this,
|
|
// a later `let m: *node` inside a branch keeps an earlier
|
|
// `let m: i32`'s tnode and `m.next` falls into the SB fallback.
|
|
let cur: *local = c.locals;
|
|
for (cur != nil) {
|
|
let cn: str = cur.name;
|
|
if (streq(cn, name)) {
|
|
cur.tnode = tnode;
|
|
return cur.off;
|
|
};
|
|
cur = cur.lnext;
|
|
};
|
|
return localalloc(c, name, sz, tnode);
|
|
};
|
|
|
|
// scanseenmark — called by scanlocals on every let / match-bind
|
|
// site. Returns true if `name` is already tracked in c.locals (so
|
|
// the slot will be shared at emission time — no new frame bump).
|
|
// Otherwise appends a name-only stub and returns false. Stubs are
|
|
// thrown away when cgfn resets c.locals before emission.
|
|
fn scanseenmark(c: *cgen, name: str) bool = {
|
|
if (localfindnode(c, name) != nil) { return true; };
|
|
let l: *local = amalloc(c.a, 48u64): *local;
|
|
l.name = name;
|
|
l.off = 0;
|
|
l.tnode = nil;
|
|
l.lnext = c.locals;
|
|
c.locals = l;
|
|
return false;
|
|
};
|
|
|
|
fn localfindnode(c: *cgen, name: str) *local = {
|
|
let l: *local = c.locals;
|
|
for (l != nil) {
|
|
let ln: str = l.name;
|
|
if (streq(ln, name)) { return l; };
|
|
l = l.lnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
fn localfind(c: *cgen, name: str) i32 = {
|
|
let l: *local = c.locals;
|
|
for (l != nil) {
|
|
let ln: str = l.name;
|
|
if (ln.len == name.len) {
|
|
let i: i32 = 0;
|
|
let eq: bool = true;
|
|
for (i < name.len) {
|
|
if (ln[i] != name[i]) { eq = false; i = name.len; }
|
|
else { i += 1; };
|
|
};
|
|
if (eq) { return l.off; };
|
|
};
|
|
l = l.lnext;
|
|
};
|
|
return 0;
|
|
};
|
|
|
|
// ---- emit helpers ---------------------------------------------------
|
|
|
|
fn emitline(s: str) void = { os.write(1, s.ptr, s.len: u64); };
|
|
|
|
fn emitint(v: i64) void = {
|
|
let s: str = strconv.i64tos(v, strconv.base.DEC);
|
|
os.write(1, s.ptr, s.len: u64);
|
|
};
|
|
|
|
fn emituint(v: u64) void = {
|
|
let s: str = strconv.u64tos(v, strconv.base.DEC);
|
|
os.write(1, s.ptr, s.len: u64);
|
|
};
|
|
|
|
// emitdispreg — print "disp(reg)" or "(reg)" when disp == 0, the
|
|
// way Plan 9 6c/6a do.
|
|
fn emitdispreg(off: i64, reg: str) void = {
|
|
if (off != 0i64) { emitint(off); };
|
|
emitline("(");
|
|
emitline(reg);
|
|
emitline(")");
|
|
};
|
|
|
|
// emitoff — print an integer offset, suppressing it entirely when 0.
|
|
// Use before any emitline("(BP)...") or emitline("(SB)...") sequence.
|
|
// Plan 9 cc convention: "(BP)" not "0(BP)".
|
|
fn emitoff(v: i64) void = {
|
|
if (v != 0i64) { emitint(v); };
|
|
};
|
|
|
|
// mklabel — fresh label "<fnname>_<prefix>_<seq>". Returns an
|
|
// arena-owned str. Mirrors C cgen's mklabel so diffs match.
|
|
fn mklabel(c: *cgen, prefix: str) str = {
|
|
let buf: [128]u8;
|
|
let i: i32 = 0;
|
|
let fname: str = c.fnname;
|
|
let j: i32 = 0;
|
|
for (j < fname.len) {
|
|
buf[i] = fname[j];
|
|
i += 1; j += 1;
|
|
};
|
|
buf[i] = 95u8; i += 1; // '_'
|
|
j = 0;
|
|
for (j < prefix.len) {
|
|
buf[i] = prefix[j];
|
|
i += 1; j += 1;
|
|
};
|
|
buf[i] = 95u8; i += 1; // '_'
|
|
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC);
|
|
let n: i32 = ns.len;
|
|
let dk: i32 = 0;
|
|
for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; };
|
|
c.labelseq += 1;
|
|
let total: i32 = i + n;
|
|
let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8;
|
|
let k: i32 = 0;
|
|
for (k < total) {
|
|
p[k] = buf[k];
|
|
k += 1;
|
|
};
|
|
p[total] = 0u8;
|
|
let r: str;
|
|
r.ptr = p;
|
|
r.len = total;
|
|
return r;
|
|
};
|
|
|
|
fn emitlabel(s: str) void = {
|
|
os.write(1, s.ptr, s.len: u64);
|
|
emitline(":\n");
|
|
};
|
|
|
|
// mkscratchname — fresh local-slot name ".<prefix>_<labelseq>". Used for
|
|
// compiler-synthesised slots (switch scrutinee, forrange index/len)
|
|
// that need to be unique per use site but are never referenced by user
|
|
// code. Increments labelseq so the same source position lines up with
|
|
// C cgen's labelseq stream.
|
|
fn mkscratchname(c: *cgen, prefix: str) str = {
|
|
let buf: [128]u8;
|
|
let i: i32 = 0;
|
|
buf[i] = 46u8; i += 1; // '.'
|
|
let j: i32 = 0;
|
|
for (j < prefix.len) {
|
|
buf[i] = prefix[j];
|
|
i += 1; j += 1;
|
|
};
|
|
buf[i] = 95u8; i += 1; // '_'
|
|
let ns: str = strconv.i64tos(c.labelseq: i64, strconv.base.DEC);
|
|
let n: i32 = ns.len;
|
|
let dk: i32 = 0;
|
|
for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; };
|
|
c.labelseq += 1;
|
|
let total: i32 = i + n;
|
|
let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8;
|
|
let k: i32 = 0;
|
|
for (k < total) {
|
|
p[k] = buf[k];
|
|
k += 1;
|
|
};
|
|
p[total] = 0u8;
|
|
let r: str;
|
|
r.ptr = p;
|
|
r.len = total;
|
|
return r;
|
|
};
|
|
|
|
// ---- string interning ------------------------------------------------
|
|
//
|
|
// streq is provided by sym.ww and reused here.
|
|
|
|
// internstrlit — return a stable label for `bytes`. Dedups by content
|
|
// so identical literals share storage.
|
|
fn internstrlit(c: *cgen, bytes: str) str = {
|
|
let s: *strlit = c.strlits;
|
|
for (s != nil) {
|
|
let bs: str = s.bytes;
|
|
if (streq(bs, bytes)) {
|
|
return s.label;
|
|
};
|
|
s = s.slnext;
|
|
};
|
|
// New label "_S_<seq>".
|
|
let buf: [32]u8;
|
|
buf[0] = 95u8; buf[1] = 83u8; buf[2] = 95u8; // "_S_"
|
|
let ns: str = strconv.i64tos(c.strlitseq: i64, strconv.base.DEC);
|
|
let n: i32 = ns.len;
|
|
let dk: i32 = 0;
|
|
for (dk < n) { buf[3 + dk] = ns.ptr[dk]; dk += 1; };
|
|
c.strlitseq += 1;
|
|
let total: i32 = 3 + n;
|
|
let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8;
|
|
let i: i32 = 0;
|
|
for (i < total) { p[i] = buf[i]; i += 1; };
|
|
p[total] = 0u8;
|
|
let lab: str;
|
|
lab.ptr = p;
|
|
lab.len = total;
|
|
let nw: *strlit = amalloc(c.a, 48u64): *strlit;
|
|
nw.label = lab;
|
|
nw.bytes = bytes;
|
|
nw.slnext = c.strlits;
|
|
c.strlits = nw;
|
|
return lab;
|
|
};
|
|
|
|
// letscalarprim — recognise the bare type-name keywords whose values
|
|
// fit in an 8-byte .data slot and load back with a plain MOVQ. Float
|
|
// types are handled separately by letfloatprim — they need MOVSS/MOVSD
|
|
// and use 4-byte (f32) or 8-byte (f64) slots.
|
|
fn letscalarprim(nm: str) bool = {
|
|
if (streq(nm, "bool")) { return true; };
|
|
if (streq(nm, "rune")) { return true; };
|
|
if (streq(nm, "i8")) { return true; };
|
|
if (streq(nm, "i16")) { return true; };
|
|
if (streq(nm, "i32")) { return true; };
|
|
if (streq(nm, "i64")) { return true; };
|
|
if (streq(nm, "u8")) { return true; };
|
|
if (streq(nm, "u16")) { return true; };
|
|
if (streq(nm, "u32")) { return true; };
|
|
if (streq(nm, "u64")) { return true; };
|
|
if (streq(nm, "int")) { return true; };
|
|
if (streq(nm, "uint")) { return true; };
|
|
if (streq(nm, "uintptr")) { return true; };
|
|
return false;
|
|
};
|
|
|
|
// letfloatprim — float type-name keywords. f32 → 4B slot, f64 → 8B.
|
|
// Returns the slot size or 0 if not a float type.
|
|
fn letfloatprim(nm: str) i32 = {
|
|
if (streq(nm, "f32")) { return 4; };
|
|
if (streq(nm, "f64")) { return 8; };
|
|
return 0;
|
|
};
|
|
|
|
// letemitsize — slot size in bytes for a top-level `let`, or 0 if
|
|
// the type isn't yet supported as a writable global. Walks type
|
|
// aliases so byte output matches C cgen, which resolves Type kinds.
|
|
// 4 → f32 (literal init supported)
|
|
// 8 → scalar or f64 (literal init supported)
|
|
// 16 → str (only zero-init / nil / "" supported)
|
|
// 24 → slice (only zero-init supported)
|
|
// varies → struct (zero-init only; field reads/scalar-field writes)
|
|
fn letemitsize(c: *cgen, d: *node) i32 = {
|
|
if (d == nil) { return 0; };
|
|
let t: *node = d.lhs;
|
|
for (t != nil) {
|
|
if (t.kind == nkind.N_TPTR) { return 8; };
|
|
if (t.kind == nkind.N_TSLICE) { return 24; };
|
|
if (t.kind == nkind.N_TARRAY) {
|
|
let lenn: *node = t.rhs;
|
|
let elemn: *node = t.lhs;
|
|
let alen: i32 = 1;
|
|
if (lenn != nil) {
|
|
if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i32; };
|
|
};
|
|
let esz: i32 = 8;
|
|
if (elemn != nil) {
|
|
if (elemn.kind == nkind.N_TNAME) {
|
|
let ps: i32 = primsize(elemn.str);
|
|
if (ps > 0) { esz = ps; };
|
|
};
|
|
};
|
|
return alen * esz;
|
|
};
|
|
if (t.kind != nkind.N_TNAME) { return 0; };
|
|
let nm: str = t.str;
|
|
if (letscalarprim(nm)) { return 8; };
|
|
let fsz: i32 = letfloatprim(nm);
|
|
if (fsz > 0) { return fsz; };
|
|
if (streq(nm, "str")) { return 16; };
|
|
let si: *structinfo = structlookup(c, nm);
|
|
if (si != nil) { return si.totsize; };
|
|
let next: *node = aliaslookup(c, nm);
|
|
if (next == nil) { return 0; };
|
|
t = next;
|
|
};
|
|
return 0;
|
|
};
|
|
|
|
fn collectlets(c: *cgen, file: *node) void = {
|
|
c.lets = nil;
|
|
if (file == nil) { return; };
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_LET) {
|
|
let nm: str = d.str;
|
|
if (nm.len > 0) {
|
|
if (letemitsize(c, d) > 0) {
|
|
let lv: *letvar = amalloc(c.a, 48u64): *letvar;
|
|
lv.name = nm;
|
|
lv.tnode = d.lhs;
|
|
lv.lvnext = c.lets;
|
|
c.lets = lv;
|
|
};
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
fn isletvar(c: *cgen, name: str) bool = {
|
|
let lv: *letvar = c.lets;
|
|
for (lv != nil) {
|
|
if (streq(lv.name, name)) { return true; };
|
|
lv = lv.lvnext;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// letvarisstr — is the named top-level let a str global? Resolves
|
|
// aliases to mirror C cgen's `let_isstr`. Used by cgident/cgdot/
|
|
// cgassign to pick the (LEAQ, MOVQ, MOVQ) sequence over the bare
|
|
// MOVQ scalar load.
|
|
// letvartnode — direct lookup of a top-level let's tnode. Used by
|
|
// cgindex / cgassign to detect global `[N]T` arrays and `*T`
|
|
// pointers, where the addressing path needs LEAQ name(SB) (array)
|
|
// or MOVQ name(SB) (pointer) and the element size from T.
|
|
fn letvartnode(c: *cgen, name: str) *node = {
|
|
let lv: *letvar = c.lets;
|
|
for (lv != nil) {
|
|
if (streq(lv.name, name)) { return lv.tnode; };
|
|
lv = lv.lvnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
fn letvarisstr(c: *cgen, name: str) bool = {
|
|
let lv: *letvar = c.lets;
|
|
for (lv != nil) {
|
|
if (streq(lv.name, name)) {
|
|
let t: *node = lv.tnode;
|
|
for (t != nil) {
|
|
if (t.kind != nkind.N_TNAME) { return false; };
|
|
let nm: str = t.str;
|
|
if (streq(nm, "str")) { return true; };
|
|
let nx: *node = aliaslookup(c, nm);
|
|
if (nx == nil) { return false; };
|
|
t = nx;
|
|
};
|
|
return false;
|
|
};
|
|
lv = lv.lvnext;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// letvarisslice — is the named top-level let a slice global?
|
|
// Slice headers are 24 bytes; the ABI flows as (AX, BX, CX) so the
|
|
// load sequence ends with `MOVQ 16(CX), CX` (overwrites the
|
|
// address holder with the cap). Mirrors C cgen's `let_isslice`.
|
|
fn letvarisslice(c: *cgen, name: str) bool = {
|
|
let lv: *letvar = c.lets;
|
|
for (lv != nil) {
|
|
if (streq(lv.name, name)) {
|
|
let t: *node = lv.tnode;
|
|
if (t == nil) { return false; };
|
|
if (t.kind == nkind.N_TSLICE) { return true; };
|
|
return false;
|
|
};
|
|
lv = lv.lvnext;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// letvarisfloat — slot size for a named float global, or 0 if not
|
|
// a float-typed let. Walks aliases so the byte-identity contract
|
|
// matches C cgen's `let_isfloat` (which resolves Type kinds).
|
|
fn letvarisfloat(c: *cgen, name: str) i32 = {
|
|
let lv: *letvar = c.lets;
|
|
for (lv != nil) {
|
|
if (streq(lv.name, name)) {
|
|
let t: *node = lv.tnode;
|
|
for (t != nil) {
|
|
if (t.kind != nkind.N_TNAME) { return 0; };
|
|
let fsz: i32 = letfloatprim(t.str);
|
|
if (fsz > 0) { return fsz; };
|
|
let nx: *node = aliaslookup(c, t.str);
|
|
if (nx == nil) { return 0; };
|
|
t = nx;
|
|
};
|
|
return 0;
|
|
};
|
|
lv = lv.lvnext;
|
|
};
|
|
return 0;
|
|
};
|
|
|
|
// letvarisstruct — is the named top-level let a struct global?
|
|
// Struct globals use LEAQ name(SB), CX as the field-access base; the
|
|
// cgdot read and cgassign write paths branch on this to skip the
|
|
// frame-relative addressing they use for locals.
|
|
fn letvarisstruct(c: *cgen, name: str) bool = {
|
|
let lv: *letvar = c.lets;
|
|
for (lv != nil) {
|
|
if (streq(lv.name, name)) {
|
|
let t: *node = lv.tnode;
|
|
for (t != nil) {
|
|
if (t.kind != nkind.N_TNAME) { return false; };
|
|
let nm: str = t.str;
|
|
if (structlookup(c, nm) != nil) { return true; };
|
|
let nx: *node = aliaslookup(c, nm);
|
|
if (nx == nil) { return false; };
|
|
t = nx;
|
|
};
|
|
return false;
|
|
};
|
|
lv = lv.lvnext;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// letvarstructinfo — for a struct global, return its structinfo
|
|
// so the cgdot/cgassign paths can look up fields. nil if the let
|
|
// isn't a struct (or wasn't found).
|
|
fn letvarstructinfo(c: *cgen, name: str) *structinfo = {
|
|
let lv: *letvar = c.lets;
|
|
for (lv != nil) {
|
|
if (streq(lv.name, name)) {
|
|
let t: *node = lv.tnode;
|
|
for (t != nil) {
|
|
if (t.kind != nkind.N_TNAME) { return nil; };
|
|
let nm: str = t.str;
|
|
let si: *structinfo = structlookup(c, nm);
|
|
if (si != nil) { return si; };
|
|
let nx: *node = aliaslookup(c, nm);
|
|
if (nx == nil) { return nil; };
|
|
t = nx;
|
|
};
|
|
return nil;
|
|
};
|
|
lv = lv.lvnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// emitdatawbyte — write one byte of an asm string literal using
|
|
// the same escape rules as emitdefconstants / emitdatasection.
|
|
fn emitdatawbyte(b: u8) void = {
|
|
if (b == 34u8) { emitline("\\\""); return; };
|
|
if (b == 92u8) { emitline("\\\\"); return; };
|
|
if (b < 32u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
os.write(1, bb.ptr, 2u64);
|
|
return;
|
|
};
|
|
if (b >= 127u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
os.write(1, bb.ptr, 2u64);
|
|
return;
|
|
};
|
|
let bb: [1]u8;
|
|
bb[0] = b;
|
|
os.write(1, bb.ptr, 1u64);
|
|
};
|
|
|
|
// letpreintern — intern strlits referenced from top-level str-let
|
|
// initialisers BEFORE emitdatasection runs. Mirrors cmd/w6c/cgen.c
|
|
// let_pre_intern: emitletdataw later looks up the same label, and
|
|
// emitdatasection emits the DATA row in the same .s file. Running
|
|
// emitletdataw after emitdatasection would flip the (DATA strlits,
|
|
// DATAW lets) section order and break byte-identity.
|
|
export fn letpreintern(c: *cgen, file: *node) void = {
|
|
if (file == nil) { return; };
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_LET) {
|
|
let sz: i32 = letemitsize(c, d);
|
|
if (sz == 16) {
|
|
let r: *node = d.rhs;
|
|
for (r != nil) {
|
|
if (r.kind != nkind.N_CAST) { break; };
|
|
r = r.lhs;
|
|
};
|
|
if (r != nil) {
|
|
if (r.kind == nkind.N_STRLIT) {
|
|
if (r.str.len > 0) {
|
|
internstrlit(c, r.str);
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
// emitletdataw — DATAW directive per top-level `let` global.
|
|
// 8B scalar with int/rune/bool/nil literal init (or no init).
|
|
// 16B str — no init / `nil` / `""` → 16 zero bytes; or non-empty
|
|
// strlit init → 8 zero placeholder + 8 LE len bytes plus a
|
|
// DATAR slot+0,strlit reloc that the linker patches at load.
|
|
// sz struct — zero only.
|
|
// Non-literal scalar inits and unsupported shapes are skipped so the
|
|
// link surfaces an undefined-symbol error if the binding is used.
|
|
fn emitletdataw(c: *cgen, file: *node) void = {
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_LET) {
|
|
let nm: str = d.str;
|
|
if (nm.len > 0) {
|
|
let sz: i32 = letemitsize(c, d);
|
|
let issg: bool = letvarisstruct(c, nm);
|
|
let fsz: i32 = letvarisfloat(c, nm);
|
|
if (fsz > 0) {
|
|
// Float global: 4B (f32) or 8B (f64).
|
|
// Two init shapes:
|
|
// - no rhs: emit fsz zero bytes
|
|
// - N_FLOATLIT: bake the IEEE bits the
|
|
// parser stashed in r.uval (lexer
|
|
// bit-casts t.fval into t.uval). f32
|
|
// emits the low 4 bytes; f64 emits 8.
|
|
let bits: u64 = 0u64;
|
|
let ok: bool = true;
|
|
if (d.rhs != nil) {
|
|
let r: *node = d.rhs;
|
|
for (r != nil) {
|
|
if (r.kind != nkind.N_CAST) { break; };
|
|
r = r.lhs;
|
|
};
|
|
ok = false;
|
|
if (r != nil) {
|
|
if (r.kind == nkind.N_FLOATLIT) {
|
|
bits = r.uval;
|
|
ok = true;
|
|
};
|
|
};
|
|
};
|
|
if (ok) {
|
|
emitline("DATAW ");
|
|
emitsymname(c, nm);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
let nb: u64 = bits;
|
|
for (i < fsz) {
|
|
emitdatawbyte((nb & 255u64): u8);
|
|
nb = nb >> 8u64;
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
};
|
|
};
|
|
// Skip the scalar 8B path when the global is a
|
|
// fixed-size array that just happens to sum to 8
|
|
// bytes (e.g. [4]u16, [8]u8) — the array path
|
|
// below handles it and the duplicate DATAW would
|
|
// otherwise differ across stages on user code.
|
|
let isarr8: bool = false;
|
|
if (d.lhs != nil) {
|
|
if (d.lhs.kind == nkind.N_TARRAY) { isarr8 = true; };
|
|
};
|
|
if (sz == 8 && !issg && fsz == 0 && !isarr8) {
|
|
let v: u64 = 0u64;
|
|
let ok: bool = true;
|
|
if (d.rhs != nil) {
|
|
let r: *node = d.rhs;
|
|
for (r != nil) {
|
|
if (r.kind != nkind.N_CAST) { break; };
|
|
r = r.lhs;
|
|
};
|
|
ok = false;
|
|
if (r != nil) {
|
|
if (r.kind == nkind.N_INTLIT) { v = r.uval; ok = true; };
|
|
if (r.kind == nkind.N_RUNELIT) { v = r.uval; ok = true; };
|
|
if (r.kind == nkind.N_TRUE) { v = 1u64; ok = true; };
|
|
if (r.kind == nkind.N_FALSE) { v = 0u64; ok = true; };
|
|
if (r.kind == nkind.N_NIL) { v = 0u64; ok = true; };
|
|
};
|
|
};
|
|
if (ok) {
|
|
emitline("DATAW ");
|
|
emitsymname(c, nm);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
let n: u64 = v;
|
|
for (i < 8) {
|
|
let b: u8 = (n & 255u64): u8;
|
|
n = n >> 8u64;
|
|
emitdatawbyte(b);
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
};
|
|
};
|
|
if (sz == 16 && !issg) {
|
|
let r: *node = d.rhs;
|
|
for (r != nil) {
|
|
if (r.kind != nkind.N_CAST) { break; };
|
|
r = r.lhs;
|
|
};
|
|
// str-literal init (non-empty): emit
|
|
// the 16B payload as 8 placeholder zero
|
|
// bytes + 8 LE bytes of length, then a
|
|
// DATAR reloc to patch the ptr half with
|
|
// the strlit's runtime VA.
|
|
let strlitinit: bool = false;
|
|
if (r != nil) {
|
|
if (r.kind == nkind.N_STRLIT) {
|
|
if (r.str.len > 0) { strlitinit = true; };
|
|
};
|
|
};
|
|
if (strlitinit) {
|
|
let lab: str = internstrlit(c, r.str);
|
|
let v: u64 = r.str.len: u64;
|
|
emitline("DATAW ");
|
|
emitsymname(c, nm);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
for (i < 8) { emitdatawbyte(0u8); i += 1; };
|
|
i = 0;
|
|
let nv: u64 = v;
|
|
for (i < 8) {
|
|
emitdatawbyte((nv & 255u64): u8);
|
|
nv = nv >> 8u64;
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
emitline("DATAR ");
|
|
emitsymname(c, nm);
|
|
emitline("+0(SB),");
|
|
os.write(1, lab.ptr, lab.len: u64);
|
|
emitline("(SB)\n");
|
|
} else {
|
|
// zero-init: accept no rhs, nil,
|
|
// or empty strlit.
|
|
let ok: bool = true;
|
|
if (d.rhs != nil) {
|
|
ok = false;
|
|
if (r != nil) {
|
|
if (r.kind == nkind.N_NIL) { ok = true; };
|
|
if (r.kind == nkind.N_STRLIT) {
|
|
if (r.str.len == 0) { ok = true; };
|
|
};
|
|
};
|
|
};
|
|
if (ok) {
|
|
emitline("DATAW ");
|
|
emitsymname(c, nm);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
for (i < 16) {
|
|
emitdatawbyte(0u8);
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
};
|
|
};
|
|
};
|
|
if (sz == 24 && !issg) {
|
|
// Slice: zero-init only (no slice-literal
|
|
// syntax to honour). Any rhs other than
|
|
// `nil` is skipped → undefined symbol at
|
|
// link.
|
|
let ok: bool = true;
|
|
if (d.rhs != nil) {
|
|
let r: *node = d.rhs;
|
|
for (r != nil) {
|
|
if (r.kind != nkind.N_CAST) { break; };
|
|
r = r.lhs;
|
|
};
|
|
ok = false;
|
|
if (r != nil) {
|
|
if (r.kind == nkind.N_NIL) { ok = true; };
|
|
};
|
|
};
|
|
if (ok) {
|
|
emitline("DATAW ");
|
|
emitsymname(c, nm);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
for (i < 24) {
|
|
emitdatawbyte(0u8);
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
};
|
|
};
|
|
// Struct globals — any size, zero-init only.
|
|
// A struct literal init isn't compile-time
|
|
// evaluated yet; skip and the link will surface
|
|
// an undefined-symbol error if referenced.
|
|
if (issg) {
|
|
if (d.rhs == nil) {
|
|
emitline("DATAW ");
|
|
emitsymname(c, nm);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
for (i < sz) {
|
|
emitdatawbyte(0u8);
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
};
|
|
};
|
|
// Top-level `[N]T = [a, b, ...]` array global.
|
|
// Emits N*esz bytes with each element's bytes
|
|
// little-endian for the declared primitive width.
|
|
// Without this, `let arr: [N]T = ...` references
|
|
// from function bodies link-fail with `undefined
|
|
// reference to arr`, and bare-name addressing
|
|
// (LEAQ arr(SB)) inside cgindex / cgassign has no
|
|
// symbol to bind to.
|
|
if (d.lhs != nil) {
|
|
if (d.lhs.kind == nkind.N_TARRAY) {
|
|
let elemn: *node = d.lhs.lhs;
|
|
let esz: i32 = 8;
|
|
if (elemn != nil) {
|
|
if (elemn.kind == nkind.N_TNAME) {
|
|
let ps: i32 = primsize(elemn.str);
|
|
if (ps > 0) { esz = ps; };
|
|
};
|
|
};
|
|
let total: i32 = sz;
|
|
let alen: i32 = total / esz;
|
|
let elems: *node = nil;
|
|
if (d.rhs != nil) {
|
|
if (d.rhs.kind == nkind.N_ARRLIT) {
|
|
elems = d.rhs.list;
|
|
};
|
|
};
|
|
emitline("DATAW ");
|
|
emitsymname(c, nm);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
let e: *node = elems;
|
|
let fillv: u64 = 0u64;
|
|
let inrepeat: bool = false;
|
|
for (i < alen) {
|
|
let v: u64 = fillv;
|
|
if (!inrepeat && e != nil) {
|
|
if (e.kind == nkind.N_FIELD) {
|
|
if (streq(e.str, "...")) {
|
|
// `..., ...` repeat marker: prior v stays.
|
|
inrepeat = true;
|
|
} else {
|
|
if (e.lhs != nil) {
|
|
if (e.lhs.kind == nkind.N_INTLIT) { v = e.lhs.uval; };
|
|
if (e.lhs.kind == nkind.N_RUNELIT) { v = e.lhs.uval; };
|
|
};
|
|
fillv = v;
|
|
e = e.next;
|
|
};
|
|
} else {
|
|
if (e.kind == nkind.N_INTLIT) { v = e.uval; };
|
|
if (e.kind == nkind.N_RUNELIT) { v = e.uval; };
|
|
fillv = v;
|
|
e = e.next;
|
|
};
|
|
};
|
|
let nb: u64 = v;
|
|
let b: i32 = 0;
|
|
for (b < esz) {
|
|
emitdatawbyte((nb & 255u64): u8);
|
|
nb = nb >> 8u64;
|
|
b += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
};
|
|
};
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
// emitdefconstants — DATA directive per top-level fold-to-literal
|
|
// `def`. 8 bytes little-endian to match what the C cgen emits.
|
|
// foldintliteral gates: int/rune literal, true/false/nil, and a
|
|
// unary +/-/~ over the same. `def NEG: i32 = -100;` arrives as
|
|
// N_UN(TK_MINUS, N_INTLIT) — the unary peel is exactly what the
|
|
// gate is for.
|
|
fn emitdefconstants(c: *cgen, file: *node) void = {
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_DEF) {
|
|
let r: *node = d.rhs;
|
|
let v: u64 = 0u64;
|
|
let ok: bool = false;
|
|
if (r != nil) {
|
|
ok = foldintliteral(r, &v);
|
|
};
|
|
if (ok) {
|
|
emitline("DATA ");
|
|
if (d.exported == 0) {
|
|
if (d.module.len > 0) {
|
|
os.write(1, d.module.ptr, d.module.len: u64);
|
|
os.write(1, ".".ptr, 1u64);
|
|
};
|
|
};
|
|
let nm: str = d.str;
|
|
os.write(1, nm.ptr, nm.len: u64);
|
|
emitline("(SB),\"");
|
|
let i: i32 = 0;
|
|
let n: u64 = v;
|
|
for (i < 8) {
|
|
let b: u8 = (n & 255u64): u8;
|
|
n = n >> 8u64;
|
|
// C emit_defs only special-cases " and \;
|
|
// every other non-printable goes as \xHH.
|
|
if (b == 34u8) { emitline("\\\""); }
|
|
else { if (b == 92u8) { emitline("\\\\"); }
|
|
else {
|
|
if (b < 32u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
os.write(1, bb.ptr, 2u64);
|
|
} else {
|
|
if (b >= 127u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
os.write(1, bb.ptr, 2u64);
|
|
} else {
|
|
let bb: [1]u8;
|
|
bb[0] = b;
|
|
os.write(1, bb.ptr, 1u64);
|
|
};
|
|
};
|
|
};};
|
|
i += 1;
|
|
};
|
|
emitline("\"\n");
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
// emitdatasection — DATA directives for every interned strlit.
|
|
// Trailing NUL appended so .ptr can be used as a C string by syscalls.
|
|
fn emitdatasection(c: *cgen) void = {
|
|
let s: *strlit = c.strlits;
|
|
for (s != nil) {
|
|
emitline("DATA ");
|
|
let lab: str = s.label;
|
|
os.write(1, lab.ptr, lab.len: u64);
|
|
emitline("(SB),\"");
|
|
let bs: str = s.bytes;
|
|
let i: i32 = 0;
|
|
for (i < bs.len) {
|
|
let b: u8 = bs[i];
|
|
if (b == 34u8) { emitline("\\\""); } // "
|
|
else { if (b == 92u8) { emitline("\\\\"); } // \
|
|
else { if (b == 10u8) { emitline("\\n"); }
|
|
else { if (b == 9u8) { emitline("\\t"); }
|
|
else { if (b == 13u8) { emitline("\\r"); }
|
|
else {
|
|
if (b < 32u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
os.write(1, bb.ptr, 2u64);
|
|
} else {
|
|
if (b >= 127u8) {
|
|
emitline("\\x");
|
|
let hi: u8 = b >> 4u8;
|
|
let lo: u8 = b & 15u8;
|
|
let bb: [2]u8;
|
|
if (hi < 10u8) { bb[0] = hi + 48u8; }
|
|
else { bb[0] = (hi - 10u8) + 97u8; };
|
|
if (lo < 10u8) { bb[1] = lo + 48u8; }
|
|
else { bb[1] = (lo - 10u8) + 97u8; };
|
|
os.write(1, bb.ptr, 2u64);
|
|
} else {
|
|
let bb: [1]u8;
|
|
bb[0] = b;
|
|
os.write(1, bb.ptr, 1u64);
|
|
};
|
|
};
|
|
};};};};};
|
|
i += 1;
|
|
};
|
|
emitline("\\x00\"\n");
|
|
s = s.slnext;
|
|
};
|
|
};
|
|
|
|
// ---- fn return-type map ---------------------------------------------
|
|
//
|
|
// Per-file: ident → ret-type-node. Used to decide whether to shuffle
|
|
// (AX, DX) → (AX, BX) after a CALL — needed for str-returning fns so
|
|
// the value flows through cgen as the canonical (AX, BX) str pair.
|
|
|
|
type fnret = struct {
|
|
fname: str,
|
|
rtype: *node,
|
|
params: *node,
|
|
frnext: *fnret,
|
|
};
|
|
|
|
fn collectfnrets(c: *cgen, file: *node) void = {
|
|
c.fnrets = nil;
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_FNDECL) {
|
|
let f: *fnret = amalloc(c.a, 48u64): *fnret;
|
|
f.fname = d.str;
|
|
f.rtype = d.lhs;
|
|
f.params = d.list;
|
|
f.frnext = c.fnrets;
|
|
c.fnrets = f;
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
fn fnretlookup(c: *cgen, name: str) *node = {
|
|
let f: *fnret = c.fnrets;
|
|
for (f != nil) {
|
|
let fn_: str = f.fname;
|
|
if (streq(fn_, name)) { return f.rtype; };
|
|
f = f.frnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// fnparamslookup — head of the declared param-list for a fn, or nil
|
|
// if the name isn't a registered fn. Used by cgcall / pushargsrev to
|
|
// detect implicit widening from a concrete variant into a tagged-union
|
|
// parameter slot.
|
|
fn fnparamslookup(c: *cgen, name: str) *node = {
|
|
let f: *fnret = c.fnrets;
|
|
for (f != nil) {
|
|
if (streq(f.fname, name)) { return f.params; };
|
|
f = f.frnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// ---- def-constant registry ------------------------------------------
|
|
//
|
|
// `def NAME: T = LIT;` becomes a DATA symbol the C-side w6c emits; an
|
|
// ident reference loads it via `MOVQ NAME(SB), AX`. We collect them at
|
|
// file load and consult on nkind.N_IDENT lookup.
|
|
|
|
type defent = struct {
|
|
dname: str,
|
|
drhs: *node,
|
|
dnext: *defent,
|
|
};
|
|
|
|
fn collectdefs(c: *cgen, file: *node) void = {
|
|
c.defs = nil;
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_DEF) {
|
|
let e: *defent = amalloc(c.a, 32u64): *defent;
|
|
e.dname = d.str;
|
|
e.drhs = d.rhs;
|
|
e.dnext = c.defs;
|
|
c.defs = e;
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
fn deflookup(c: *cgen, name: str) bool = {
|
|
let e: *defent = c.defs;
|
|
for (e != nil) {
|
|
let dn: str = e.dname;
|
|
if (streq(dn, name)) { return true; };
|
|
e = e.dnext;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// Returns the rhs init node for a top-level `def`, or nil if `name`
|
|
// doesn't name a def. Used by cgdot to inline `.ptr`/`.len` on
|
|
// `def NAME: str = "..."` — those aren't laid out in memory.
|
|
fn deflookuprhs(c: *cgen, name: str) *node = {
|
|
let e: *defent = c.defs;
|
|
for (e != nil) {
|
|
let dn: str = e.dname;
|
|
if (streq(dn, name)) { return e.drhs; };
|
|
e = e.dnext;
|
|
};
|
|
return nil;
|
|
};
|
|
|
|
// ---- module-private symbol map --------------------------------------
|
|
//
|
|
// Every non-FFI top-level fn decl lives in its module's namespace —
|
|
// cgen mangles the leaf to `<module>.<name>` at the def site (TEXT)
|
|
// and at every call/load site, so cross-module same-leaf fns (lib/os
|
|
// `read` vs lib/io `read`, both exported) coexist at link time.
|
|
// Non-fn decls (let/def/type) stick to the older "non-exported only"
|
|
// rule: their export-side namespace is the user-facing data ABI and
|
|
// mangling them changes the surface. FFI-bound decls (@symbol) keep
|
|
// their explicit C symbol regardless of kind.
|
|
//
|
|
// Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export`
|
|
// for fns. Both stages must match exactly — ww2/ww3/ww4 byte-identity
|
|
// depends on it.
|
|
|
|
type modent = struct {
|
|
mname: str, // the bare ident as it appears in source
|
|
module: str, // the originating module (`// MODULE: foo`)
|
|
mnext: *modent,
|
|
};
|
|
|
|
fn collectmods(c: *cgen, file: *node) void = {
|
|
c.mods = nil;
|
|
if (file == nil) { return; };
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
// Mirror collectfnrets' shape exactly (plain prepend in one
|
|
// branch). Earlier nested-if/early-return variants tickled a
|
|
// wwstage cgen bug that dropped most prepends.
|
|
if (d.kind == nkind.N_FNDECL) {
|
|
// Fns mangle regardless of export status — covers
|
|
// lib/os.read vs lib/io.read collision.
|
|
if (d.module.len > 0) {
|
|
let isffi: bool = false;
|
|
let a: *node = d.attr;
|
|
for (a != nil) {
|
|
if (a.kind == nkind.N_ATTR) {
|
|
let an: str = a.str;
|
|
if (streq(an, "symbol")) { isffi = true; };
|
|
};
|
|
a = a.next;
|
|
};
|
|
if (!isffi) {
|
|
if (!streq(d.str, "main")) {
|
|
let m: *modent = amalloc(c.a, 48u64): *modent;
|
|
m.mname = d.str;
|
|
m.module = d.module;
|
|
m.mnext = c.mods;
|
|
c.mods = m;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
if (d.kind == nkind.N_DEF) {
|
|
if (d.exported == 0) {
|
|
if (d.module.len > 0) {
|
|
let m: *modent = amalloc(c.a, 48u64): *modent;
|
|
m.mname = d.str;
|
|
m.module = d.module;
|
|
m.mnext = c.mods;
|
|
c.mods = m;
|
|
};
|
|
};
|
|
};
|
|
if (d.kind == nkind.N_TYPEDECL) {
|
|
if (d.exported == 0) {
|
|
if (d.module.len > 0) {
|
|
let m: *modent = amalloc(c.a, 48u64): *modent;
|
|
m.mname = d.str;
|
|
m.module = d.module;
|
|
m.mnext = c.mods;
|
|
c.mods = m;
|
|
};
|
|
};
|
|
};
|
|
if (d.kind == nkind.N_LET) {
|
|
if (d.exported == 0) {
|
|
if (d.module.len > 0) {
|
|
let m: *modent = amalloc(c.a, 48u64): *modent;
|
|
m.mname = d.str;
|
|
m.module = d.module;
|
|
m.mnext = c.mods;
|
|
c.mods = m;
|
|
};
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
fn modlookup(c: *cgen, name: str) str = {
|
|
let m: *modent = c.mods;
|
|
for (m != nil) {
|
|
if (streq(m.mname, name)) { return m.module; };
|
|
m = m.mnext;
|
|
};
|
|
let empty: str;
|
|
empty.ptr = nil;
|
|
empty.len = 0;
|
|
return empty;
|
|
};
|
|
|
|
// modlookupforfn — hint-aware lookup for fn names. Walks c.mods
|
|
// preferring entries where module matches `hint`; falls back to the
|
|
// first leaf-name match when nothing matches the hint (legacy single-
|
|
// owner shape, also covers lookups with hint.len==0). Needed because
|
|
// multiple modules can now register the same fn leaf — bare `lookup`
|
|
// would otherwise grab whichever module was prepended last.
|
|
fn modlookupforfn(c: *cgen, name: str, hint: str) str = {
|
|
let m: *modent = c.mods;
|
|
let first: str;
|
|
first.ptr = nil;
|
|
first.len = 0;
|
|
for (m != nil) {
|
|
if (streq(m.mname, name)) {
|
|
if (hint.len > 0 && m.module.len > 0
|
|
&& streq(m.module, hint)) {
|
|
return m.module;
|
|
};
|
|
if (first.len == 0 && first.ptr == nil) {
|
|
first = m.module;
|
|
};
|
|
};
|
|
m = m.mnext;
|
|
};
|
|
return first;
|
|
};
|
|
|
|
// emitsymname — write the asm symbol name for `ident`. Honours, in
|
|
// order: FFI mapping (@symbol), module mangling (private decls), bare
|
|
// name. Use everywhere a top-level non-fn name is emitted before `(SB)`
|
|
// — DATA labels for top-level lets/defs, address-of-let, etc. Fn names
|
|
// (CALL/LEAQ-of-fn/TEXT) go through emitfnname so the hint disambiguates
|
|
// cross-module same-leaf fn exports.
|
|
fn emitsymname(c: *cgen, ident: str) void = {
|
|
let resolved: str = ffiresolve(c, ident);
|
|
if (resolved.ptr != ident.ptr) {
|
|
// FFI hit — emit the mapped linker symbol verbatim.
|
|
os.write(1, resolved.ptr, resolved.len: u64);
|
|
return;
|
|
};
|
|
let mod: str = modlookup(c, ident);
|
|
if (mod.len > 0) {
|
|
os.write(1, mod.ptr, mod.len: u64);
|
|
os.write(1, ".".ptr, 1u64);
|
|
};
|
|
os.write(1, ident.ptr, ident.len: u64);
|
|
};
|
|
|
|
// emitfnname — write the asm symbol name for a fn `ident`, threading
|
|
// `hint` (the explicit module from a `mod.fn` use site, or c.curmod
|
|
// for bare-IDENT calls) through modlookupforfn. Same FFI override
|
|
// semantics as emitsymname; same dot-separator format. Use at every
|
|
// CALL / LEAQ-of-fn / TEXT-def site.
|
|
fn emitfnname(c: *cgen, ident: str, hint: str) void = {
|
|
let resolved: str = ffiresolve(c, ident);
|
|
if (resolved.ptr != ident.ptr) {
|
|
os.write(1, resolved.ptr, resolved.len: u64);
|
|
return;
|
|
};
|
|
let mod: str = modlookupforfn(c, ident, hint);
|
|
if (mod.len > 0) {
|
|
os.write(1, mod.ptr, mod.len: u64);
|
|
os.write(1, ".".ptr, 1u64);
|
|
};
|
|
os.write(1, ident.ptr, ident.len: u64);
|
|
};
|
|
|
|
// ---- FFI map ---------------------------------------------------------
|
|
|
|
fn fficollect(c: *cgen, file: *node) void = {
|
|
c.ffis = nil;
|
|
if (file == nil) { return; };
|
|
let d: *node = file.list;
|
|
for (d != nil) {
|
|
if (d.kind == nkind.N_FNDECL) {
|
|
let a: *node = d.attr;
|
|
for (a != nil) {
|
|
if (a.kind == nkind.N_ATTR) {
|
|
let aname: str = a.str;
|
|
if (streq(aname, "symbol")) {
|
|
let symnode: *node = a.list;
|
|
if (symnode != nil) {
|
|
if (symnode.kind == nkind.N_STRLIT) {
|
|
let f: *ffi = amalloc(c.a, 48u64): *ffi;
|
|
f.ident = d.str;
|
|
f.symbol = symnode.str;
|
|
f.fnext = c.ffis;
|
|
c.ffis = f;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
a = a.next;
|
|
};
|
|
};
|
|
d = d.next;
|
|
};
|
|
};
|
|
|
|
fn ffiresolve(c: *cgen, ident: str) str = {
|
|
let f: *ffi = c.ffis;
|
|
for (f != nil) {
|
|
let id: str = f.ident;
|
|
if (streq(id, ident)) { return f.symbol; };
|
|
f = f.fnext;
|
|
};
|
|
return ident;
|
|
};
|
|
|
|
// ---- ABI argreg helpers ---------------------------------------------
|
|
|
|
fn argregname(i: i32) str = {
|
|
if (i == 0) { return "DI"; };
|
|
if (i == 1) { return "SI"; };
|
|
if (i == 2) { return "DX"; };
|
|
if (i == 3) { return "CX"; };
|
|
if (i == 4) { return "R8"; };
|
|
if (i == 5) { return "R9"; };
|
|
return "?";
|
|
};
|
|
|
|
// fargregname — XMM scalar-float arg registers (SysV: X0..X7).
|
|
// Parallel to argregname / sysv_argregs; float args advance their
|
|
// own counter so int and float arg slots don't conflict.
|
|
export fn fargregname(i: i32) str = {
|
|
if (i == 0) { return "X0"; };
|
|
if (i == 1) { return "X1"; };
|
|
if (i == 2) { return "X2"; };
|
|
if (i == 3) { return "X3"; };
|
|
if (i == 4) { return "X4"; };
|
|
if (i == 5) { return "X5"; };
|
|
if (i == 6) { return "X6"; };
|
|
if (i == 7) { return "X7"; };
|
|
return "?";
|
|
};
|