Files
ww/selfhost/cmd/wcc/cgenutil.ww
Hojun-Cho 7c75dd218a selfhost: fix several wwstage cgen miscompilations
Surfaced via examples/lisp, which had to work around the following in
source. Each lowering now matches cstage on the same shape.

- cgassign / cgdot: two-level field through a non-pointer sub-struct.
  `(*L).cur.kind = k` (cur a struct-by-value field of L) silently
  dropped the store; the corresponding read fell into the SB-symbol
  fallback and the linker reported `undefined reference to kind`. The
  two new branches resolve outer-field offset + inner-field offset
  and emit a single direct store/load at the combined slot, both for
  T-by-value and *T-base shapes.

- cgdot: `xs[i].field` chains the trailing field load through the
  N_INDEX result for [N]T / []T / *T element-of-struct-ptr. The
  cgforrange loop variable now carries the elem tnode so the same
  fast path covers `for (let x .. xs) { x.field }`.

- cgindex / cgassign: top-level `[N]T` array and `*T` pointer used
  as an index base. cgindex now emits LEAQ name(SB) (array) or
  MOVQ name(SB) (pointer) with the correct element scaling; without
  this the fallback emitted neither base and walked off the saved
  BP slot. Adds letvartnode() helper, an N_TARRAY branch to
  letemitsize so the array shows up in c.lets, and an N_TARRAY
  initialiser path in emitletdataw that lays the literal bytes into
  DATAW.

- cglet / scanlocals: infer the local's tnode for an unannotated
  `let x = f()` / `let x = f()?`. inferletcalltype() reads the
  callee's declared return; `?` and `!` strip to the success variant
  so a tagged-union let allocates the full 24B slot and the
  struct-field dispatch in cgdot/cgassign sees the right type.
  letslotsize now defers to slotsize on the inferred type.

- slotsize: follow type aliases for tagged-union variants. With
  `type parserr = !str;`, the variant slot was 8B instead of the
  required 16B; the tagged let stomped on the next slot at the
  AX/DX/CX spill.

- cgreturn: tagged-union return forwarding. `return f();` where f
  also returns a tagged union now passes the (tag, payload1,
  payload2) triple through unchanged instead of re-wrapping it.

- cgreturn / cglet / taggedvariantindex: dispatch by variant name
  with module-qualified-vs-bare matching, and recognise N_STRUCTLIT
  as the variant tag for `return eof{};`. cgexpr default emits
  `MOVQ $0, AX` so the surrounding return shuffle isn't left with
  a stale AX.

- isstrtype / nodeisstr: resolve through `!T` aliases. `parserr =
  !str` was not propagating the str-shape to the rhs check and the
  MOVQ BX,CX shuffle was being dropped from str-typed local
  returns.

- exprfloatkind: recognise `p.field` as f64/f32 when the struct
  field is so declared, so `v.fval: i64` lowers to CVTTSD2SI on X0.

- cgassign: str field on a direct struct local writes both halves.
  `L.src = s;` previously dropped s.len.

- cgcall: pop into the int reg window only up to 6 (DI..R9); rest
  stays on the stack and the caller emits ADDQ to clean up.
  cgfnparams accepts >6-arg signatures by registering the overflow
  params at positive BP offsets (16+8*k(BP)), no spill instruction
  emitted.

All 26 harness tests pass; bootstrap reaches a byte-stable fixed
point at ww3 == ww4.
2026-05-13 03:06:46 +09:00

1340 lines
42 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww.
//
// General helpers used across cgenexpr / cgenstmt / cgendecl:
// - pushargsrev: per-call arg pushing
// - type predicates: isstr*/isslice*/istagged*/nodeis* families
// - field ops: fieldloadop, fieldstoreop
// - index helpers: indexbaseesz, dotinnerstructptr, elemsizeof
// - slot sizing: structlookup, primsize, slotsize, fieldsize,
// registerstruct, collectstructs
// - rhs helpers: rhstargetname, taggedvariantindex
//
// Bundler pulls this in transitively via cgen.ww; consumers don't
// need to `use cgenutil;` directly.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
// ---- expression cgen -------------------------------------------------
// pushargsrev — recursively walks the arg list, evaluates rightmost
// first, and pushes. str args take two slots (ptr in AX, len in BX);
// the order on the stack so a left-to-right pop into argregs lands
// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the
// pop sequence then yields AX, then BX.
fn pushargsrev(c: *cgen, arg: *node) i32 = {
if (arg == nil) { return 0; };
let rest: i32 = pushargsrev(c, arg.next);
// nkind.N_SLICE expression as arg: `buf[lo:hi]` builds a slice header
// on the stack matching C cgen's sequence — push base, push hi,
// compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr).
if (arg.kind == nkind.N_SLICE) {
let base: *node = arg.lhs;
let lo: *node = arg.rhs;
let hi: *node = arg.cond;
let baselocal: *local = nil;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
};
};
// base address → push
if (baselocal != nil) {
let tn: *node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
};
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
};
} else {
cgexpr(c, base);
};
emitline("\tPUSHQ\tAX\n");
// hi (default base length) → push
if (hi != nil) {
cgexpr(c, hi);
} else { if (baselocal != nil) {
let tn: *node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
let lenn: *node = tn.rhs;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", AX\n");
};
};
} else { if (tn.kind == nkind.N_TSLICE) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
} else { if (tn.kind == nkind.N_TNAME) {
if (streq(tn.str, "str")) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
};
};};};
};
} else {
emitline("\tMOVQ\t$0, AX\n");
};};
emitline("\tPUSHQ\tAX\n");
// lo (default 0) → AX
if (lo != nil) { cgexpr(c, lo); }
else { emitline("\tMOVQ\t$0, AX\n"); };
emitline("\tPOPQ\tBX\n"); // hi
emitline("\tPOPQ\tCX\n"); // base
emitline("\tMOVQ\tBX, DX\n"); // DX = hi
emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len
emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr
emitline("\tPUSHQ\tDX\n"); // cap
emitline("\tPUSHQ\tDX\n"); // len
emitline("\tPUSHQ\tCX\n"); // ptr (top)
return rest + 3;
};
// Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in
// reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop
// into argregs lands the canonical (ptr/tag, len/v0, cap/v1).
if (arg.kind == nkind.N_IDENT) {
let nm: str = arg.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
let off: i32 = lc.off;
if (isslicetype(c, lc.tnode) || istaggedtype(lc.tnode)) {
emitline("\tMOVQ\t");
emitoff((off + 16): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
};
};
// Float arg: cgexpr leaves the value in X0. Push 8 bytes from
// X0 via SUBQ+MOVSD so cgcall's pop side can drain into the
// XMM stream (X0..X7). f32 still occupies 8B on the stack —
// the MOVSS load on the pop side touches only the low 4.
let fk: i32 = exprfloatkind(c, arg);
if (fk != 0) {
cgexpr(c, arg);
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
emitline("\tSUBQ\t$8, SP\n");
emitline("\t");
emitline(mov);
emitline("\tX0, (SP)\n");
return rest + 1;
};
cgexpr(c, arg);
if (nodeisslice(c, arg)) {
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
if (nodeisstr(c, arg)) {
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 2;
};
emitline("\tPUSHQ\tAX\n");
return rest + 1;
};
fn nodeisslice(c: *cgen, n: *node) bool = {
if (n == nil) { return false; };
let k: nkind = n.kind;
if (k == nkind.N_IDENT) {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) { return isslicetype(c, lc.tnode); };
return false;
};
if (k == nkind.N_SLICE) { return true; };
return false;
};
// nodeisstr — best-effort surface check: does this expression
// evaluate to a str value? Used to drive the call-arg push convention
// (str args take two slots: ptr + len).
fn nodeisstr(c: *cgen, n: *node) bool = {
if (n == nil) { return false; };
let k: nkind = n.kind;
if (k == nkind.N_STRLIT) { return true; };
if (k == nkind.N_IDENT) {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
// Use isstrtype so `!str` aliases (parserr = !str) and
// `type foo = str;` chains resolve through. The bare
// `streq("str", ...)` test missed them and dropped the
// MOVQ BX,CX shuffle on returns of str-aliased locals.
if (isstrtype(c, lc.tnode)) { return true; };
};
return false;
};
if (k == nkind.N_CALL) {
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let cnm: str = callee.str;
let rt: *node = fnretlookup(c, cnm);
return isstrtype(c, rt);
};
};
return false;
};
if (k == nkind.N_DOT) {
let base: *node = n.lhs;
let fld: str = n.str;
// `<expr>.ptr` is *u8 not str; `<expr>.len` is i32 not str.
if (streq(fld, "ptr")) { return false; };
if (streq(fld, "len")) { return false; };
if (streq(fld, "cap")) { return false; };
if (base != nil) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = nkind.N_NONE;
if (tn != nil) { lkind = tn.kind; };
if (lkind == nkind.N_TNAME) { sname = tn.str; };
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) { sname = inner.str; };
};
};
};
};
// Chained dot (`p.foo.bar`): use dotinnerstructptr
// to resolve the inner chain to the *struct it lands
// on, then look up `fld` in that struct.
if (base.kind == nkind.N_DOT) {
let innert: *node = dotinnerstructptr(c, base);
if (innert != nil) {
if (innert.kind == nkind.N_TNAME) { sname = innert.str; };
};
};
if (sname.len > 0) {
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
return isstrtype(c, fi.tnode);
};
fi = fi.finext;
};
};
};
};
return false;
};
if (k == nkind.N_CAST) {
return isstrtype(c, n.rhs);
};
return false;
};
// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr.
fn typenameisunsigned(nm: str) bool = {
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, "uint")) { return true; };
if (streq(nm, "uintptr")) { return true; };
return false;
};
// typenodeisunsigned — recurse through TNAME / TPTR / TSLICE etc.
fn typenodeisunsigned(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TNAME) { return typenameisunsigned(t.str); };
return false;
};
// typeis8byteprimitive — does this type take exactly one 8-byte
// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive
// padded up to 8) rather than a wider aggregate? Used by nkind.N_LET
// zero-init to mirror C cgen's "only zero if sz == 8 at the type
// level" rule. Strings (16), slices (24), tagged unions (>=16),
// tuples (16), structs (varies), arrays — all fall through to
// false here even when their *slot* rounds up to 8.
fn typeis8byteprimitive(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let k: nkind = t.kind;
if (k == nkind.N_TPTR) { return true; };
if (k == nkind.N_TFN) { return true; };
if (k == nkind.N_TCHAN) { return true; };
if (k == nkind.N_TSLICE) { return false; };
if (k == nkind.N_TARRAY) {
// C cgen (cmd/w6c/cgen.c:3317) zero-inits TY_ARRAY whenever
// its raw byte size is 8 — e.g. `[8]bool`, `[2]i32`, `[4]i16`,
// `[1]i64`. Mirror that here so the wwstage matches.
let lenn: *node = t.rhs;
let elemn: *node = t.lhs;
if (lenn == nil) { return false; };
if (lenn.kind != nkind.N_INTLIT) { return false; };
let elen: i64 = lenn.uval: i64;
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 (esz: i64 * elen) == 8i64;
};
if (k == nkind.N_TTUPLE) { return false; };
if (k == nkind.N_TTAGGED){ return false; };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return false; };
// Struct alias: not a primitive even if the slot is 8B.
if (structlookup(c, nm) != nil) { return false; };
// Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...).
// All of these get slot-padded to 8 and zero-init in C.
if (primsize(nm) > 0) { return true; };
return false;
};
return false;
};
// elemissigned — given an indexable type (`*T`, `[]T`, `[N]T`), is
// its element a signed narrow primitive (i8/i16/i32/rune)? Used by
// cgindex to pick MOVSXD vs MOVL at esz=4. Mirrors C cgen's
// `signed_elem` check.
fn elemissigned(t: *node) bool = {
if (t == nil) { return false; };
let elem: *node = nil;
let k: nkind = t.kind;
if (k == nkind.N_TPTR) { elem = t.lhs; };
if (k == nkind.N_TSLICE) { elem = t.lhs; };
if (k == nkind.N_TARRAY) { elem = t.lhs; };
if (elem == nil) { return false; };
if (elem.kind != nkind.N_TNAME) { return false; };
return typenameissigned(elem.str);
};
// typenameissigned — true for i8/i16/i32/i64/int/rune.
fn typenameissigned(nm: str) bool = {
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, "int")) { return true; };
if (streq(nm, "rune")) { return true; };
return false;
};
// fieldloadop — pick the load instruction for a non-str struct
// field by its declared size + signedness. Mirrors the C cgen op
// dispatch (MOVZBQ for u8/bool/i8, MOVSXD for i32, MOVL for u32, MOVQ
// for 8-byte). f might be nil for fields outside our struct registry.
fn fieldloadop(f: *fieldinfo) str = {
if (f == nil) { return "MOVQ"; };
let sz: i32 = f.fsz;
if (sz == 1) { return "MOVZBQ"; };
if (sz == 4) {
let t: *node = f.tnode;
if (t != nil) {
if (t.kind == nkind.N_TNAME) {
if (typenameissigned(t.str)) { return "MOVSXD"; };
};
};
return "MOVL";
};
return "MOVQ";
};
// fieldstoreop — pick the store instruction for a non-str struct
// field by its declared size. MOVB for 1, MOVL for 4, MOVQ for 8.
fn fieldstoreop(f: *fieldinfo) str = {
if (f == nil) { return "MOVQ"; };
let sz: i32 = f.fsz;
if (sz == 1) { return "MOVB"; };
if (sz == 4) { return "MOVL"; };
return "MOVQ";
};
// indexbaseesz — element size for `arr[i]` where the base is a
// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice).
// For str the element is one byte; for `[]T` / `*[]T` we drill into
// the slice element type.
fn indexbaseesz(c: *cgen, base: *node) i32 = {
if (base == nil) { return 8; };
if (base.kind != nkind.N_DOT) { return 8; };
let fld: str = base.str;
let inner: *node = base.lhs;
if (inner == nil) { return 8; };
if (inner.kind != nkind.N_IDENT) { return 8; };
let nm: str = inner.str;
let lc: *local = localfindnode(c, nm);
if (lc == nil) { return 8; };
let tn: *node = lc.tnode;
if (tn == nil) { return 8; };
// `.ptr` pseudo-field on str/slice → element of the str/slice.
if (streq(fld, "ptr")) {
let innert: *node = tn;
if (tn.kind == nkind.N_TPTR) { innert = tn.lhs; };
if (innert == nil) { return 8; };
if (innert.kind == nkind.N_TNAME) {
if (streq(innert.str, "str")) { return 1; };
};
if (innert.kind == nkind.N_TSLICE) { return elemsizeof(innert); };
return 8;
};
// Generic struct field: if it's *T, element size is T's size.
let lkind: nkind = tn.kind;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (lkind == nkind.N_TNAME) { sname = tn.str; };
if (lkind == nkind.N_TPTR) {
let pinner: *node = tn.lhs;
if (pinner != nil) {
if (pinner.kind == nkind.N_TNAME) { sname = pinner.str; };
};
};
if (sname.len == 0) { return 8; };
let si: *structinfo = structlookup(c, sname);
if (si == nil) { return 8; };
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
let ft: *node = fi.tnode;
if (ft == nil) { return 8; };
if (ft.kind == nkind.N_TPTR) {
let elem: *node = ft.lhs;
if (elem != nil) {
if (elem.kind == nkind.N_TNAME) {
if (streq(elem.str, "str")) { return 16; };
let ps: i32 = primsize(elem.str);
if (ps > 0) { return ps; };
};
};
return 8;
};
if (ft.kind == nkind.N_TSLICE) { return elemsizeof(ft); };
// str-typed field: indexing yields one byte
// (`n.s[i]` where .s is str — matches C cgen's
// MOVZBQ for byte indexing).
if (ft.kind == nkind.N_TNAME) {
if (streq(ft.str, "str")) { return 1; };
};
return 8;
};
fi = fi.finext;
};
return 8;
};
// dotinnerstructptr — for an nkind.N_DOT whose lhs is a chain of dots
// or an nkind.N_IDENT, walk the chain and return the nkind.N_TNAME tnode of the
// struct that the chain dereferences to (i.e., for `r.sym` where
// .sym is *lsym, return nkind.N_TNAME("lsym")). Returns nil if the chain
// doesn't resolve to a *struct.
//
// Used by the chained-DOT cgen path so `r.sym.val` knows the outer
// is a field of `lsym`.
fn dotinnerstructptr(c: *cgen, n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind != nkind.N_DOT) { return nil; };
let base: *node = n.lhs;
let fld: str = n.str;
if (base == nil) { return nil; };
// Resolve base's struct tnode.
let baset: *node = nil;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc == nil) { return nil; };
let tn: *node = lc.tnode;
if (tn == nil) { return nil; };
// base could be either struct-by-value (nkind.N_TNAME) or *struct (nkind.N_TPTR).
if (tn.kind == nkind.N_TNAME) { baset = tn; };
if (tn.kind == nkind.N_TPTR) { baset = tn.lhs; };
} else { if (base.kind == nkind.N_DOT) {
baset = dotinnerstructptr(c, base);
};};
if (baset == nil) { return nil; };
if (baset.kind != nkind.N_TNAME) { return nil; };
// Look up the struct, find the field, return the field's *struct.
let si: *structinfo = structlookup(c, baset.str);
if (si == nil) { return nil; };
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
let ft: *node = fi.tnode;
if (ft == nil) { return nil; };
if (ft.kind != nkind.N_TPTR) { return nil; };
let inner: *node = ft.lhs;
if (inner == nil) { return nil; };
if (inner.kind != nkind.N_TNAME) { return nil; };
return inner;
};
fi = fi.finext;
};
return nil;
};
// elemsizeof — given the type node of an indexable (`*T`, `[]T`,
// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/
// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback).
fn elemsizeof(t: *node) i32 = {
if (t == nil) { return 1; };
let k: nkind = t.kind;
let elem: *node = nil;
if (k == nkind.N_TPTR) { elem = t.lhs; };
if (k == nkind.N_TSLICE) { elem = t.lhs; };
if (k == nkind.N_TARRAY) { elem = t.lhs; };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return 1; };
// Indexing a primitive name (rare): element size = the prim.
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
return 1;
};
if (elem == nil) { return 1; };
if (elem.kind == nkind.N_TNAME) {
let nm: str = elem.str;
// str element is 16B (ptr+len). primsize returns 0 for it.
if (streq(nm, "str")) { return 16; };
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
};
return 8;
};
// nodeisunsigned — best-effort cgen-time inference from the AST. We
// don't have a typed AST yet, so we walk surface nodes:
// nkind.N_INTLIT — never marked unsigned (no tsuffix plumbing yet)
// nkind.N_IDENT — look up the local's declared type
// nkind.N_DOT — look up the field's declared type via struct reg
// nkind.N_BIN / nkind.N_UN — recurse: unsigned if either operand is unsigned
// nkind.N_CAST — use the cast target type
//
// Conservative: if we can't tell, return false (signed). The cost of
// being wrong here is byte-different asm vs C, not bad runtime.
fn nodeisunsigned(c: *cgen, n: *node) bool = {
if (n == nil) { return false; };
let k: nkind = n.kind;
if (k == nkind.N_IDENT) {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) { return typenodeisunsigned(lc.tnode); };
return false;
};
if (k == nkind.N_DOT) {
let base: *node = n.lhs;
let fld: str = n.str;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bn: str = base.str;
let lc: *local = localfindnode(c, bn);
if (lc != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = nkind.N_NONE;
if (tn != nil) { lkind = tn.kind; };
let sname: str;
sname.ptr = nil; sname.len = 0;
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) { sname = inner.str; };
};
};
if (lkind == nkind.N_TNAME) { sname = tn.str; };
if (sname.len > 0) {
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
return typenodeisunsigned(fi.tnode);
};
fi = fi.finext;
};
};
};
};
};
};
return false;
};
if (k == nkind.N_CAST) { return typenodeisunsigned(n.rhs); };
if (k == nkind.N_BIN) {
if (nodeisunsigned(c, n.lhs)) { return true; };
return nodeisunsigned(c, n.rhs);
};
if (k == nkind.N_UN) { return nodeisunsigned(c, n.lhs); };
// nkind.N_INDEX: `p[i]` is unsigned iff p's element type is unsigned.
// Walks the base local's declared type and pulls the element
// out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the
// compare-codegen for `p[i] >= 48u8` falls back to signed JGE
// instead of JAE, diverging from C w6c on byte indexing.
if (k == nkind.N_INDEX) {
let base: *node = n.lhs;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
let elem: *node = nil;
if (tn.kind == nkind.N_TPTR) { elem = tn.lhs; };
if (tn.kind == nkind.N_TARRAY) { elem = tn.lhs; };
if (tn.kind == nkind.N_TSLICE) { elem = tn.lhs; };
if (elem != nil) {
return typenodeisunsigned(elem);
};
};
};
};
};
return false;
};
return false;
};
// ---- type-driven slot sizing ----------------------------------------
fn structlookup(c: *cgen, name: str) *structinfo = {
let s: *structinfo = c.structs;
for (s != nil) {
let sn: str = s.sname;
if (streq(sn, name)) { return s; };
s = s.sinext;
};
return nil;
};
// primsize — size in bytes of a primitive type name (or 0 if not
// recognised as a primitive — the caller falls back to other paths).
// fldnumidx — parse a tuple field name like "0" / "1" / "12" into an
// index, or -1 if not all-digits. Used by cgdot to dispatch
// `t.0` / `t.1` against an nkind.N_TTUPLE local without pulling in strconv.
fn fldnumidx(s: str) i32 = {
if (s.len == 0) { return -1; };
let r: i32 = 0;
let i: i32 = 0;
for (i < s.len) {
let b: u8 = s[i];
if (b < 48u8) { return -1; };
if (b > 57u8) { return -1; };
r = r * 10 + ((b - 48u8): i32);
i += 1;
};
return r;
};
fn primsize(name: str) i32 = {
if (streq(name, "u8")) { return 1; };
if (streq(name, "i8")) { return 1; };
if (streq(name, "bool")) { return 1; };
if (streq(name, "u16")) { return 2; };
if (streq(name, "i16")) { return 2; };
if (streq(name, "u32")) { return 4; };
if (streq(name, "i32")) { return 4; };
if (streq(name, "f32")) { return 4; };
if (streq(name, "u64")) { return 8; };
if (streq(name, "i64")) { return 8; };
if (streq(name, "uint")) { return 8; };
if (streq(name, "int")) { return 8; };
if (streq(name, "uintptr")) { return 8; };
if (streq(name, "f64")) { return 8; };
if (streq(name, "rune")) { return 4; };
if (streq(name, "void")) { return 0; };
return 0;
};
// variantnamematch — tagged-union variant names are compared as if
// they'd been alias-resolved. Pattern names can be module-qualified
// (`strconv.invalid` from a `case let e: strconv.invalid =>`),
// while the variant's declared name inside its own module is bare
// (`invalid`). With no checker the cgen can't follow imports, so we
// accept exact match plus suffix-after-`.` on either side. Mirrors
// the C cgen's type_eq, which goes through resolved Type pointers.
fn variantnamematch(vname: str, pname: str) bool = {
if (streq(vname, pname)) { return true; };
// `pname` is qualified, `vname` is bare: drop module prefix.
let i: i32 = 0;
for (i < pname.len) {
if (pname[i] == '.': u8) {
let tail: str;
tail.ptr = pname.ptr + i + 1;
tail.len = pname.len - i - 1;
if (streq(tail, vname)) { return true; };
};
i += 1;
};
// `vname` is qualified, `pname` is bare: same trick in reverse.
let j: i32 = 0;
for (j < vname.len) {
if (vname[j] == '.': u8) {
let tail: str;
tail.ptr = vname.ptr + j + 1;
tail.len = vname.len - j - 1;
if (streq(tail, pname)) { return true; };
};
j += 1;
};
return false;
};
// inferletcalltype — for an annotation-less `let x = expr;`, return
// a usable tnode for cgen's struct-aware paths. Today: `let x =
// f()?` infers x's type from the success variant of f's tagged
// return; without this, x has tnode = nil and `x.field` falls into
// the SB-symbol fallback (linker reports `undefined reference to
// <fieldname>`). We don't infer for plain `let x = f()` yet —
// non-tagged returns don't carry their type back the same way.
fn inferletcalltype(c: *cgen, rhs: *node) *node = {
if (rhs == nil) { return nil; };
// `?` (N_TRYPROP) and `!` (N_TRYUNW) both unwrap a tagged
// return to its success variant; the rhs we want the type of
// is the inner call expression.
let unwrap: bool = false;
let call: *node = rhs;
if (rhs.kind == nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; };
if (rhs.kind == nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; };
if (call == nil) { return nil; };
if (call.kind != nkind.N_CALL) { return nil; };
let callee: *node = call.lhs;
if (callee == nil) { return nil; };
let cname: str;
cname.ptr = nil; cname.len = 0;
if (callee.kind == nkind.N_IDENT) { cname = callee.str; };
if (callee.kind == nkind.N_DOT) { cname = callee.str; };
if (cname.len == 0) { return nil; };
let rt: *node = fnretlookup(c, cname);
if (rt == nil) { return nil; };
if (unwrap) {
// Strip error variants — success type is the first
// variant of the tagged return.
if (rt.kind != nkind.N_TTAGGED) { return nil; };
return rt.list;
};
// Plain call: declared return type is the local's type.
return rt;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
//
// `let x = f();` (no annotation): infer from `f`'s declared return
// type so a 24B tagged-union return reserves all three spill slots,
// not the default 8B. Without this, the AX:DX:CX spill in cglet's
// tagged-init branch writes past the local and tramples the next
// slot.
export fn letslotsize(c: *cgen, n: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_ARRLIT) {
let elemn: *node = n.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 cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
if (n.lhs != nil) { return slotsize(c, n.lhs); };
// Annotation-less init: defer to the call's return type if we
// can infer it. Tagged-union returns need 24B; everything else
// matches slotsize on the inferred type.
let inferred: *node = inferletcalltype(c, n.rhs);
if (inferred != nil) { return slotsize(c, inferred); };
return 8;
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: nkind = typn.kind;
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TFN) { return 8; };
if (k == nkind.N_TCHAN) { return 8; };
if (k == nkind.N_TSLICE) { return 24; };
if (k == nkind.N_TTUPLE) {
// Sum element sizes. Mirrors C cgen which uses raw type
// sizes; padding to 8 happens inside slotsize for primitives,
// so a `(i64, str)` resolves to 8 + 16 = 24 (matches the C
// cgen 24B init / positional-access layout).
let total: i32 = 0;
let p: *node = typn.list;
for (p != nil) {
total += slotsize(c, p);
p = p.next;
};
return total;
};
if (k == nkind.N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
// cgen's resolve_type for nkind.N_TTAGGED.
let v: *node = typn.list;
let maxsz: i32 = 0;
for (v != nil) {
let sz: i32 = slotsize(c, v);
if (sz > maxsz) { maxsz = sz; };
v = v.next;
};
let pad: i32 = (maxsz + 7) & ~7;
return 8 + pad;
};
if (k == nkind.N_TNAME) {
let nm: str = typn.str;
if (streq(nm, "str")) { return 16; };
let ps: i32 = primsize(nm);
if (ps > 0) {
// Pad to 8 for stack slots — matches C cgen which spills
// every primitive into an 8-byte slot.
return 8;
};
// Named struct lookup.
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
// Type alias (`type foo = !str;` / `type foo = bar;`):
// follow it so a tagged-union variant of a !str-aliased
// error type contributes 16 bytes to the max payload
// rather than 8 (the default).
if (c != nil) {
let aliased: *node = aliaslookup(c, nm);
if (aliased != nil) {
if (aliased.kind == nkind.N_TBANG) {
return slotsize(c, aliased.lhs);
};
return slotsize(c, aliased);
};
};
return 8;
};
if (k == nkind.N_TARRAY) {
let lenn: *node = typn.rhs;
let elemn: *node = typn.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let en: str = elemn.str;
let ps: i32 = primsize(en);
if (ps > 0) { esz = ps; };
};
};
return (esz: i64 * elen): i32;
};
if (k == nkind.N_TSTRUCT) {
// Inline anonymous struct — sum of field sizes.
let f: *node = typn.list;
let total: i32 = 0;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
total += slotsize(c, f.lhs);
};
f = f.next;
};
return total;
};
return 8;
};
// registerstruct — compute field offsets + total size for a struct
// type-decl, store in c.structs. Field type sizes use the same
// slotsize logic (with primitives kept at their natural width — we
// only round to 8 for stack slots, not struct interiors).
fn fieldsize(c: *cgen, tnode: *node) i32 = {
if (tnode == nil) { return 8; };
let k: nkind = tnode.kind;
if (k == nkind.N_TNAME) {
let nm: str = tnode.str;
if (streq(nm, "str")) { return 16; };
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
// Enum: size of its storage type. Mirrors the C cgen, which
// reads Type.size off the TY_ENUM (which inherits from .sub).
let en: *enumtype = enumlookup(c, nm);
if (en != nil) {
if (en.storage != nil) {
if (en.storage.kind == nkind.N_TNAME) {
let sps: i32 = primsize(en.storage.str);
if (sps > 0) { return sps; };
};
};
return 4; // default storage is i32
};
return 8;
};
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TSLICE) { return 24; };
if (k == nkind.N_TARRAY) {
// Same shape as slotsize's TARRAY branch.
let lenn: *node = tnode.rhs;
let elemn: *node = tnode.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = fieldsize(c, elemn);
return (esz: i64 * elen): i32;
};
return 8;
};
fn registerstruct(c: *cgen, name: str, tstruct: *node) void = {
let si: *structinfo = amalloc(c.a, 64u64): *structinfo;
si.sname = name;
si.fields = nil;
si.totsize = 0;
let head: *fieldinfo = nil;
let tail: *fieldinfo = nil;
let off: i32 = 0;
let f: *node = tstruct.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
let sz: i32 = fieldsize(c, f.lhs);
// Align to 8 for any field >= 4 bytes (matches our other
// cgen choices). i8/u8/bool may sit on odd byte offsets;
// the C cgen does similar best-effort packing.
let aln: i32 = 1;
if (sz >= 8) { aln = 8; }
else { if (sz >= 4) { aln = 4; }
else { if (sz >= 2) { aln = 2; }; }; };
if ((off & (aln - 1)) != 0) {
off = (off + aln - 1) & ~(aln - 1);
};
let fi: *fieldinfo = amalloc(c.a, 48u64): *fieldinfo;
fi.fname = f.str;
fi.foff = off;
fi.fsz = sz;
fi.tnode = f.lhs;
if (head == nil) { head = fi; tail = fi; }
else { tail.finext = fi; tail = fi; };
off += sz;
};
f = f.next;
};
// Round total to 8 for stack-slot use.
if ((off & 7) != 0) { off = (off + 7) & ~7; };
si.fields = head;
si.totsize = off;
si.sinext = c.structs;
c.structs = si;
};
fn collectstructs(c: *cgen, file: *node) void = {
c.structs = nil;
if (file == nil) { return; };
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) {
registerstruct(c, d.str, body);
};
};
};
d = d.next;
};
};
// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the
// alias chain registered at file load.
fn isstrtyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return true; };
};
return false;
};
fn isstrtype(c: *cgen, t: *node) bool = {
if (isstrtyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
if (isstrtyperaw(r)) { return true; };
// `parserr = !str` — `!T` aliases shouldn't hide their
// underlying type from str-routing. Unwrap and re-check.
if (r != nil) {
if (r.kind == nkind.N_TBANG) {
let inner: *node = r.lhs;
if (isstrtyperaw(inner)) { return true; };
if (inner != nil) {
let r2: *node = resolvetype(c, inner);
if (isstrtyperaw(r2)) { return true; };
};
};
};
return false;
};
fn isslicetyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TSLICE) { return true; };
return false;
};
fn isslicetype(c: *cgen, t: *node) bool = {
if (isslicetyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
return isslicetyperaw(r);
};
fn istaggedtype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TTAGGED) { return true; };
return false;
};
// isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution.
fn isf32typeraw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "f32");
};
fn isf64typeraw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "f64");
};
// isfloattype — f32 / f64 (and aliases of those). Used by cglet,
// cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-prologue to
// dispatch the MOVSS/MOVSD-shaped paths.
export fn isfloattype(c: *cgen, t: *node) bool = {
if (isf32typeraw(t)) { return true; };
if (isf64typeraw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
if (isf32typeraw(r)) { return true; };
if (isf64typeraw(r)) { return true; };
return false;
};
// isf32type — narrower predicate: true only for f32 (after alias
// resolution). f64 returns false. Used to pick MOVSS vs MOVSD and
// the SS-variant arithmetic / cast opcodes.
export fn isf32type(c: *cgen, t: *node) bool = {
if (isf32typeraw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
return isf32typeraw(r);
};
// exprfloatkind — classify an expression's value-class so callers can
// pick float vs integer codegen without a full type system. Returns:
// 0 — integer-like (or unknown — same fallback the existing cgen
// takes today)
// 1 — f32
// 2 — f64
// Recognises: float literals, idents bound to float lets/locals,
// chained casts whose target is float, and (recursively) the inner
// expr of a non-narrowing wrapping construct. Anything we can't
// pin down conservatively reports integer — the worst case is that
// CVT* is skipped for an exotic case the user can still spell with
// an explicit local.
export fn exprfloatkind(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let k: nkind = n.kind;
if (k == nkind.N_FLOATLIT) { return 2; };
if (k == nkind.N_CAST) {
if (isf32type(c, n.rhs)) { return 1; };
if (isfloattype(c, n.rhs)) { return 2; };
return 0;
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
if (isf32type(c, lc.tnode)) { return 1; };
if (isfloattype(c, lc.tnode)) { return 2; };
return 0;
};
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, n.str)) {
if (isf32type(c, lv.tnode)) { return 1; };
if (isfloattype(c, lv.tnode)) { return 2; };
return 0;
};
lv = lv.lvnext;
};
return 0;
};
if (k == nkind.N_UN) {
// Unary on a float (TK_MINUS) returns float; everything
// else is integer-coded.
if (n.op == tkind.TK_MINUS) {
return exprfloatkind(c, n.lhs);
};
return 0;
};
if (k == nkind.N_BIN) {
// Arithmetic binops inherit the operands' kind. Comparison
// (eq/ne/lt/...) returns bool — integer.
let op: tkind = n.op;
if (op == tkind.TK_PLUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_MINUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_STAR) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_SLASH) { return exprfloatkind(c, n.lhs); };
return 0;
};
if (k == nkind.N_CALL) {
// Look up the callee's declared return type — fnretlookup
// returns the type-AST. Routes float-returning fns through
// the X0 ABI so cglet / cgassign know to spill from X0.
let nm: str;
nm.ptr = nil; nm.len = 0;
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_IDENT) { nm = n.lhs.str; };
};
if (nm.len > 0) {
let rt: *node = fnretlookup(c, nm);
if (isf32type(c, rt)) { return 1; };
if (isfloattype(c, rt)) { return 2; };
};
return 0;
};
if (k == nkind.N_DOT) {
// `p.field` where the struct field is f64/f32. Without this,
// `v.fval: i64` lowers to CVTSI on an integer-load value
// instead of CVTTSD2SI on the X0 the cgdot path actually
// emits for an f64 field.
let base: *node = n.lhs;
let fld: str = n.str;
if (base != nil) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { sname = tn.str; };
if (tn.kind == nkind.N_TPTR) {
let pe: *node = tn.lhs;
if (pe != nil) {
if (pe.kind == nkind.N_TNAME) { sname = pe.str; };
};
};
};
};
};
if (sname.len > 0) {
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
if (isf32type(c, fi.tnode)) { return 1; };
if (isfloattype(c, fi.tnode)) { return 2; };
return 0;
};
fi = fi.finext;
};
};
};
};
return 0;
};
return 0;
};
// isnullabletype — nkind.N_TTAGGED with exactly two children, one *T and
// one `void`. Folds to a single 8-byte pointer slot per Hare's
// `(*T | null)` semantics. Mirrors check.c's resolve_type detection.
export fn isnullabletype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TTAGGED) { return false; };
let a: *node = t.list;
if (a == nil) { return false; };
let b: *node = a.next;
if (b == nil) { return false; };
if (b.next != nil) { return false; };
let aptr: bool = (a.kind == nkind.N_TPTR);
let bptr: bool = (b.kind == nkind.N_TPTR);
let avoid: bool = (a.kind == nkind.N_TNAME);
if (avoid) { avoid = streq(a.str, "void"); };
let bvoid: bool = (b.kind == nkind.N_TNAME);
if (bvoid) { bvoid = streq(b.str, "void"); };
if (aptr) { if (bvoid) { return true; }; };
if (avoid) { if (bptr) { return true; }; };
return false;
};
// nullableptrtag — 0-based index of the *T variant in a nullable
// union. The void variant takes the other slot (0 or 1).
export fn nullableptrtag(t: *node) i32 = {
if (t == nil) { return 0; };
if (t.kind != nkind.N_TTAGGED) { return 0; };
let a: *node = t.list;
if (a != nil) { if (a.kind == nkind.N_TPTR) { return 0; }; };
return 1;
};
// voidvariantindex — find the 0-based index of the `void` variant in a
// tagged-union type expr, -1 if absent. Used by cgreturn to map bare
// `return;` in a tagged-union-returning fn to the void variant's tag.
fn voidvariantindex(tagged: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == nkind.N_TNAME) {
if (streq(v.str, "void")) { return idx; };
};
v = v.next;
idx += 1;
};
return -1;
};
// rhstargetname — for a returned value, what's its declared (or
// surface-inferred) type name? `expr: T` casts dictate T directly;
// bare strlit/intlit fall back to a primitive name.
fn rhstargetname(c: *cgen, rhs: *node) str = {
let nm: str;
nm.ptr = nil; nm.len = 0;
if (rhs == nil) { return nm; };
if (rhs.kind == nkind.N_CAST) {
let t: *node = rhs.rhs;
if (t != nil) {
if (t.kind == nkind.N_TNAME) { return t.str; };
};
return nm;
};
if (rhs.kind == nkind.N_STRLIT) { return "str"; };
// `T{}` carries its type name on the lhs N_IDENT — the parser
// builds `N_STRUCTLIT{ lhs = N_IDENT("T"), list = fields }`.
// Needed so `return eof{};` (variant of a tagged union) resolves
// to the `eof` variant index rather than falling through to the
// "first non-str variant" fallback in taggedvariantindex.
if (rhs.kind == nkind.N_STRUCTLIT) {
let tref: *node = rhs.lhs;
if (tref != nil) {
if (tref.kind == nkind.N_IDENT) { return tref.str; };
if (tref.kind == nkind.N_TNAME) { return tref.str; };
};
return nm;
};
if (rhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, rhs.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { return tn.str; };
};
};
};
return nm;
};
// taggedvariantindex — given the tagged-union type expr and the
// returned value's surface type, find the matching variant's 0-based
// index. Compare by exact type name first; if no match, fall back to
// "any str-shape variant matches an str-typed value".
fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = {
if (tagged == nil) { return -1; };
if (rhs == nil) { return -1; };
let wantname: str = rhstargetname(c, rhs);
if (wantname.len > 0) {
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == nkind.N_TNAME) {
if (variantnamematch(v.str, wantname)) { return idx; };
};
v = v.next;
idx += 1;
};
};
// Fallback: by str-shape (resolves aliases).
let wantstr: bool = nodeisstr(c, rhs);
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
let visstr: bool = false;
if (v.kind == nkind.N_TNAME) {
if (isstrtype(c, v)) { visstr = true; };
};
if (visstr == wantstr) { return idx; };
v = v.next;
idx += 1;
};
return -1;
};