A >32B tagged-union field (slice payload) read through a direct *struct pointer byte-diverged: wwstage's cgloadtaggedfield always loaded R8@+24 before CX@+16, but cstage's direct-*struct-ptr arm (cgen.c ~11926) loads in offset order CX@+16 then R8@+24. Both ran correct -- a pre-existing rule-10 asm divergence, for a local *struct ptr as well as a global one. Thread a cxlast flag through cgloadtaggedfield: the direct-ptr site (cgptrfieldload, the shared local+global chokepoint) passes cxlast=false to match cstage's offset order; the other 5 callers keep cxlast=true (byte unchanged). A global flip was rejected -- it would clobber the CX-base callers (CX@+16 first destroys the base before the R8@+24 read), and the chained-BX caller must stay R8-first to mirror cstage's chained twin (cgen.c ~12021); the order is a genuine per-arm property of cstage, not derivable from the base register. Test: +2 rows (tagged_slice_field via global *struct ptr, _local via local *struct ptr), runtime + byte-id; both proven to fail byte-id with only the compiler files reverted.
5647 lines
232 KiB
Plaintext
5647 lines
232 KiB
Plaintext
// 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: elemsizeof, elemsizeofc
|
||
// - slot sizing: structlookup, primsize, slotsize, fieldsize,
|
||
// registerstruct, collectstructs
|
||
// - rhs helpers: taggedvariantindex
|
||
//
|
||
// Bundler pulls this in transitively via cgen.ww; consumers don't
|
||
// need to `use cgenutil;` directly.
|
||
|
||
package wcc;
|
||
|
||
import os;
|
||
import syntax;
|
||
import strconv;
|
||
|
||
// ---- variadic-call helpers (Hare-style `T...` param) -----------------
|
||
|
||
// slicewrap — synthesise an N_TSLICE node wrapping the given element
|
||
// type AST. Used by the Hare-style variadic path so the local entry
|
||
// for the param (callee side) and the call-site slice descriptor
|
||
// (caller side) both advertise their effective type as []ELEM —
|
||
// every isslicetype / nodeisslice check then succeeds naturally.
|
||
fn slicewrap(c: *cgen, elem: *syntax.node) *syntax.node = {
|
||
let s: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
|
||
s.lhs = elem;
|
||
return s;
|
||
};
|
||
|
||
// findvariadicparam — walk a param-list head and return the variadic
|
||
// param node (the one with op == TK_ELLIPSIS) plus the count of
|
||
// non-variadic params before it. Returns nil/0 when no variadic.
|
||
// nfixed_out cannot be nil.
|
||
fn findvariadicparam(ps: *syntax.node, nfixed_out: *i32) *syntax.node = {
|
||
*nfixed_out = 0;
|
||
let p: *syntax.node = ps;
|
||
for (p != nil) {
|
||
if (p.kind == syntax.nkind.N_PARAM) {
|
||
if (p.op == syntax.tkind.TK_ELLIPSIS) {
|
||
return p;
|
||
};
|
||
*nfixed_out += 1;
|
||
};
|
||
p = p.next;
|
||
};
|
||
return nil;
|
||
};
|
||
|
||
// callee_variadic_param — convenience wrapper: looks up the callee
|
||
// by name and finds its variadic param + nfixed. Returns nil if the
|
||
// callee isn't registered or has no variadic param.
|
||
//
|
||
// N_DOT routes through fnparamslookupmod with the module hint
|
||
// (callee.lhs.str) — bare fnparamslookup walks same-module-first
|
||
// (#4d) which is wrong for a cross-module N_DOT call into a module
|
||
// whose same-leaf fn has divergent variadic-vs-non-variadic shape.
|
||
// #4d explicitly deferred this re-routing; surfaced by #16 when
|
||
// strings.contains gained a variadic shape and a caller's
|
||
// bytes.contains call site picked strings.contains' variadic
|
||
// params for arg-prep while emitting CALL bytes.contains.
|
||
fn callee_variadic_param(c: *cgen, callee: *syntax.node, nfixed_out: *i32) *syntax.node = {
|
||
*nfixed_out = 0;
|
||
if (callee == nil) { return nil; };
|
||
let ps: *syntax.node = nil;
|
||
if (callee.kind == syntax.nkind.N_IDENT) {
|
||
if (callee.str.len == 0) { return nil; };
|
||
ps = fnparamslookup(c, callee.str);
|
||
} else { if (callee.kind == syntax.nkind.N_DOT) {
|
||
if (callee.str.len == 0) { return nil; };
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
ps = fnparamslookupmod(c, callee.str, cmod);
|
||
}; };
|
||
return findvariadicparam(ps, nfixed_out);
|
||
};
|
||
|
||
// calleecvariadic — true when the callee is a C-style variadic fn: a
|
||
// bare `...` param (str == "...", distinct from the Hare-style `T...`
|
||
// findvariadicparam keys on op == TK_ELLIPSIS). nfixed_out gets the
|
||
// count of fixed params before the `...`. Mirror of cstage's
|
||
// `cu->variadic` gate (cmd/w6c/cgen.c cgcall), which check.c:902 sets
|
||
// ONLY for the bare-`...` form — the bare `...` adds no Tparam, so
|
||
// nfixed is the fixed-param count. Drives the SysV §3.5.7 AL=XMM-count
|
||
// emit and the C-default-promotion of an f32 variadic-tail arg to f64
|
||
// (#14). nfixed_out cannot be nil.
|
||
fn calleecvariadic(c: *cgen, callee: *syntax.node, nfixed_out: *i32) bool = {
|
||
*nfixed_out = 0;
|
||
if (callee == nil) { return false; };
|
||
let ps: *syntax.node = nil;
|
||
if (callee.kind == syntax.nkind.N_IDENT) {
|
||
if (callee.str.len == 0) { return false; };
|
||
ps = fnparamslookup(c, callee.str);
|
||
} else { if (callee.kind == syntax.nkind.N_DOT) {
|
||
if (callee.str.len == 0) { return false; };
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
ps = fnparamslookupmod(c, callee.str, cmod);
|
||
}; };
|
||
let p: *syntax.node = ps;
|
||
for (p != nil) {
|
||
if (p.kind == syntax.nkind.N_PARAM) {
|
||
if (syntax.streq(p.str, "...")) {
|
||
return true;
|
||
};
|
||
*nfixed_out += 1;
|
||
};
|
||
p = p.next;
|
||
};
|
||
return false;
|
||
};
|
||
|
||
// ---- 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.
|
||
//
|
||
// `param` is the corresponding declared parameter for `arg` (N_PARAM
|
||
// node from the callee's signature) or nil. When param's type is a
|
||
// tagged union and `arg`'s surface type is a concrete variant of it,
|
||
// we materialise (tag, value-words, pad) for the parameter slot before
|
||
// pushing — mirrors cmd/w6c/cgen.c's call-arg widening.
|
||
//
|
||
// #38b: cgcall walks the list TWICE — memphase=true first, staging
|
||
// every MEMORY-class (>48B tagged) arg below all register-class
|
||
// words, then memphase=false for the register classes. Each phase
|
||
// skips the other's args; the return value counts only own-phase
|
||
// slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per
|
||
// ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit).
|
||
// argtaggedwidensz — the SysV slot width (16/24/32) a CONCRETE arg is
|
||
// widened to when passed to a tagged-union PARAM, or 0 when no register-class
|
||
// widen happens: param not tagged / not a fixed param, an already-matching-
|
||
// slot tagged source (natural push), the nullable 8B fold (handled by the
|
||
// scalar path), or a >48B memory-class slot (staged below the register words).
|
||
// This is the SSoT the cgcall DRAIN consults for its widen-first POP count;
|
||
// it MUST agree word-for-word with pushargsrev's widen-PUSH count below
|
||
// (same param-tagged + !aistagged gates, same taggedcastpeel) or the drain
|
||
// desyncs — the #30/#48 root was a drain with no widen branch: the widened
|
||
// box's words were under-drained (#30: a following float read the leftover
|
||
// payload word) or the float-source box was misclassified as a float arg
|
||
// (#48: MOVSD ate the tag word into X0). Mirrors cstage's precomputed
|
||
// widen[i]/widen_sz[i] (cmd/w6c/cgen.c cgcall).
|
||
fn argtaggedwidensz(c: *cgen, arg0: *syntax.node, param: *syntax.node) i32 = {
|
||
if (param == nil) { return 0; };
|
||
if (param.kind != syntax.nkind.N_PARAM) { return 0; };
|
||
if (param.op == syntax.tkind.TK_ELLIPSIS) { return 0; };
|
||
let ptype: *syntax.node = param.lhs;
|
||
if (ptype == nil) { return 0; };
|
||
if (!istaggedtype(c, ptype)) { return 0; };
|
||
// >48B slot is memory-class: pushargsrev stages it below the register
|
||
// words and the drain skips it (dmemsz) — never a register widen.
|
||
if (taggedmemargsize(ptype.type_: *syntax.tinfo) > 0) { return 0; };
|
||
let arg: *syntax.node = taggedcastpeel(c, arg0);
|
||
let pslot: i32 = slotsize(c, ptype);
|
||
// Already a matching-slot tagged source → natural push, no widen
|
||
// (mirrors pushargsrev's aistagged gates: ident/call/index/dot/deref).
|
||
if (arg.kind == syntax.nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, arg.str);
|
||
if (lc != nil) {
|
||
if (istaggedtype(c, lc.tnode)) {
|
||
if (slotsize(c, lc.tnode) == pslot) { return 0; };
|
||
};
|
||
};
|
||
};
|
||
if (taggedcallslot(c, arg) == pslot) { return 0; };
|
||
if (arg.kind == syntax.nkind.N_INDEX) {
|
||
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
|
||
};
|
||
if (arg.kind == syntax.nkind.N_DOT) {
|
||
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
|
||
};
|
||
if (arg.kind == syntax.nkind.N_UN && arg.op == syntax.tkind.TK_STAR) {
|
||
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
|
||
};
|
||
// The 8B nullable fold pushes one pointer word the scalar drain path
|
||
// already pops correctly; only the multi-word tagged widen needs the
|
||
// drain's dedicated branch.
|
||
if (pslot < 16) { return 0; };
|
||
return pslot;
|
||
};
|
||
|
||
// argidx is this arg's 0-based position in the call's arg list;
|
||
// cvarnfixed is the fixed-param count of a C-variadic callee (-1 when
|
||
// the callee is not C-variadic). An f32 arg in the variadic tail
|
||
// (argidx >= cvarnfixed) is promoted to f64 here (#14), so its 8B stack
|
||
// slot holds a real double for the pop side and the callee's
|
||
// va_arg(double).
|
||
fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool, argidx: i32, cvarnfixed: i32) i32 = {
|
||
if (arg == nil) { return 0; };
|
||
let nextparam: *syntax.node = nil;
|
||
if (param != nil) { nextparam = param.next; };
|
||
let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase, argidx + 1, cvarnfixed);
|
||
// Family C (#35): peel tagged→tagged casts FIRST so every gate
|
||
// below keys on the operand — an identity cast reduces to the
|
||
// ident fast path, a widening cast trips the widen branch with
|
||
// the operand as source. cgexpr on the cast node collapses to
|
||
// one word (silent word0 push pre-#35). Mirrors cstage's
|
||
// args[i] = cg_tagged_castpeel(args[i]) pre-pass; the cgcall
|
||
// pop side counts via pushargsrev's return, so the drain stays
|
||
// balanced.
|
||
arg = taggedcastpeel(c, arg);
|
||
// #38b MEMORY-class detection: keyed off the declared param's
|
||
// type (so widening into a >48B slot is caught), else the arg's
|
||
// own stamped type (fn-ptr callee carries no param nodes).
|
||
let memsz: i32 = 0;
|
||
let memptype: *syntax.node = nil;
|
||
if (param != nil) { if (param.kind == syntax.nkind.N_PARAM) {
|
||
if (param.op != syntax.tkind.TK_ELLIPSIS) {
|
||
memptype = param.lhs;
|
||
if (memptype != nil) {
|
||
memsz = taggedmemargsize(memptype.type_: *syntax.tinfo);
|
||
};
|
||
};
|
||
}; };
|
||
if (memsz == 0) {
|
||
memsz = taggedmemargsize(arg.type_: *syntax.tinfo);
|
||
};
|
||
if (memphase != (memsz > 0)) { return rest; };
|
||
if (memsz > 0) {
|
||
// same-type check — mirror cstage's `(pu == au) ||
|
||
// type_eq(p->type, at)` widen detection.
|
||
let same: bool = false;
|
||
let at: *syntax.tinfo = arg.type_: *syntax.tinfo;
|
||
if (memptype != nil) {
|
||
let pt: *syntax.tinfo = memptype.type_: *syntax.tinfo;
|
||
let pu: *syntax.tinfo = pt;
|
||
pu = tichase(pu);
|
||
let au: *syntax.tinfo = at;
|
||
au = tichase(au);
|
||
if (pu != nil && pu == au) { same = true; };
|
||
if (!same && pt != nil && at != nil) {
|
||
if (syntax.typeeq(pt, at)) { same = true; };
|
||
};
|
||
} else {
|
||
same = true;
|
||
};
|
||
if (!same) {
|
||
// Widen via the @tagscr scratch for EVERY source
|
||
// shape — the direct-push fast arms below stage
|
||
// exactly 4 words, short of the memsz/8 the drain
|
||
// accounts for (mirrors cstage cg_widen_tagged_push
|
||
// dst_is_mem routing).
|
||
let scroff: i32 = tagscradd(c, memsz);
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let zz: i32 = 0;
|
||
for (zz < memsz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((scroff + zz): i64);
|
||
emitline("(BP)\n");
|
||
zz += 8;
|
||
};
|
||
cgwidentaggedstore(c, memptype.type_: *syntax.tinfo, arg,
|
||
"BP", scroff, memsz);
|
||
let pp: i32 = memsz - 8;
|
||
for (pp >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((scroff + pp): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
pp -= 8;
|
||
};
|
||
return rest + memsz / 8;
|
||
};
|
||
// Exact type: raw slot words high→low from the value's
|
||
// address (local slot, or any aggargsrcaddr-addressable
|
||
// source: global let, N_DOT chain, array index, deref).
|
||
// An exact-type CALL source is sret-class (>32B tagged
|
||
// return) — its result is in memory behind a dest pointer,
|
||
// not a register cursor; receive-then-push is the
|
||
// #40-family follow-up.
|
||
if (arg.kind == syntax.nkind.N_CALL) {
|
||
let mc: str = "#38b: sret-class tagged call result as a >48B by-value arg unwired (#40-family follow-up)\n";
|
||
os.write(2, mc.ptr, mc.len: u64);
|
||
os.exit(1);
|
||
};
|
||
if (arg.kind == syntax.nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, arg.str);
|
||
if (lc != nil) {
|
||
let w: i32 = memsz / 8 - 1;
|
||
for (w >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((lc.off + w*8): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
w -= 1;
|
||
};
|
||
return rest + memsz / 8;
|
||
};
|
||
};
|
||
if (aggargsrcaddr(c, arg, "SI")) {
|
||
let w: i32 = memsz / 8 - 1;
|
||
for (w >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((w*8): i64);
|
||
emitline("(SI), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
w -= 1;
|
||
};
|
||
return rest + memsz / 8;
|
||
};
|
||
// #40/FB3: a place the enumerated arms miss — slice
|
||
// element, deref-spine element — resolves through the F6
|
||
// resolver. AFTER aggargsrcaddr so every pre-#40 shape
|
||
// keeps its asm; the resolver balances its own pushes, so
|
||
// the words already staged below stay put.
|
||
if (cgplaceaddr(c, arg, "SI")) {
|
||
let w: i32 = memsz / 8 - 1;
|
||
for (w >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((w*8): i64);
|
||
emitline("(SI), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
w -= 1;
|
||
};
|
||
return rest + memsz / 8;
|
||
};
|
||
let mu: str = "#38b: >48B tagged arg from unsupported source kind (rvalue and unresolvable-place sources unwired)\n";
|
||
os.write(2, mu.ptr, mu.len: u64);
|
||
os.exit(1);
|
||
};
|
||
// Implicit widening from a concrete variant to a tagged-union
|
||
// parameter slot. Skips when the arg is already a tagged local
|
||
// (line 121's slice-or-tagged shortcut handles that).
|
||
let widensz: i32 = 0;
|
||
let widentag: i32 = 0;
|
||
if (param != nil) {
|
||
if (param.kind == syntax.nkind.N_PARAM) {
|
||
// Hare-style variadic `T...`: effective param type is
|
||
// []T (slice). The arg here is the synthesised slice
|
||
// descriptor (or a forwarded `xs...` slice), not a
|
||
// value of T being widened into a tagged slot — skip
|
||
// the widening detection so the slice-ident fast path
|
||
// at the bottom of pushargsrev gets the push.
|
||
if (param.op == syntax.tkind.TK_ELLIPSIS) {
|
||
widensz = 0;
|
||
} else {
|
||
let ptype: *syntax.node = param.lhs;
|
||
if (istaggedtype(c, ptype)) {
|
||
let aistagged: bool = false;
|
||
if (arg.kind == syntax.nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, arg.str);
|
||
if (lc != nil) {
|
||
// #55: only treat a tagged ident as
|
||
// "already tagged" (natural push, no remap)
|
||
// when its slot MATCHES the param. On slot-
|
||
// DIFFER the source is a NARROWER union widened
|
||
// into a wider one — fall to the widen scratch
|
||
// + tag-remap below (the slot-gated INDEX/DOT/
|
||
// STAR arms' twin). Pre-#55 the ungated TRUE
|
||
// natural-pushed the narrower box's words with
|
||
// no remap (aligned-tag LUCK, misaligned-tag
|
||
// wrong). cstage routes every tagged source
|
||
// through cg_widen_tagged_store (cmd/w6c/
|
||
// cgen.c:2982 src_is_tagged).
|
||
if (istaggedtype(c, lc.tnode)) {
|
||
if (slotsize(c, lc.tnode) == slotsize(c, ptype)) {
|
||
aistagged = true;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
// #21: a CALL returning a tagged-union must
|
||
// skip widening — cgexpr leaves AX=tag,
|
||
// DX=word0, CX=word1, R8=word2 per the
|
||
// tagged-return ABI; the widening branch would
|
||
// treat AX as a concrete payload and silently
|
||
// drop DX/CX/R8. Restrict to the matching-slot
|
||
// case (mirrors cstage type_eq at
|
||
// cmd/w6c/cgen.c:4216-4221); tagged-source
|
||
// widening into a wider slot is out of scope.
|
||
if (taggedcallslot(c, arg) == slotsize(c, ptype)) {
|
||
aistagged = true;
|
||
};
|
||
// #12: N_INDEX of a sum-typed slice element —
|
||
// cgindex emits the same AX/DX/CX/R8 tagged ABI.
|
||
// Without this gate the widening scalar branch
|
||
// hardcodes the param's first-variant tag and
|
||
// the callee reads a fixed arm on garbage.
|
||
// #60: arg.type_ is the checker-stamped element
|
||
// tinfo (check.ww indexresult); istaggedtype/
|
||
// slotsize read .type_, so feed the N_INDEX node
|
||
// directly. cstage reads the element via
|
||
// base->type->sub (cmd/w6c/cgen.c:3518).
|
||
if (arg.kind == syntax.nkind.N_INDEX) {
|
||
if (istaggedtype(c, arg)) {
|
||
if (slotsize(c, arg) == slotsize(c, ptype)) {
|
||
aistagged = true;
|
||
};
|
||
};
|
||
};
|
||
// #22a: t.N tuple-element read leaves the
|
||
// same AX/DX/CX/R8 box cursor (this arc's
|
||
// t.N box load) — without this gate the
|
||
// widening scalar branch clamps the
|
||
// unresolvable tag to 0 and the callee
|
||
// reads variant 0. Stamped-carrier (#67)
|
||
// twin of the N_INDEX arm above; cstage
|
||
// needs no kind gate (its widen[i] `same`
|
||
// check is type-keyed on args[i]->type).
|
||
if (arg.kind == syntax.nkind.N_DOT) {
|
||
if (istaggedtype(c, arg)) {
|
||
if (slotsize(c, arg) == slotsize(c, ptype)) {
|
||
aistagged = true;
|
||
};
|
||
};
|
||
};
|
||
// Family C (#35): a DEREF source is a
|
||
// tagged box too (mem-based, any size)
|
||
// — without this gate the widening
|
||
// scalar branch boxed the box. Same
|
||
// slotsize key as the N_INDEX/N_DOT
|
||
// stamped-carrier arms above.
|
||
if (arg.kind == syntax.nkind.N_UN && arg.op == syntax.tkind.TK_STAR) {
|
||
if (istaggedtype(c, arg)) {
|
||
if (slotsize(c, arg) == slotsize(c, ptype)) {
|
||
aistagged = true;
|
||
};
|
||
};
|
||
};
|
||
if (!aistagged) {
|
||
widensz = slotsize(c, ptype);
|
||
let tagged: *syntax.node = resolvetagged(c, ptype);
|
||
let t: i32 = taggedvariantindex(c, tagged, arg);
|
||
if (t < 0) { t = 0; };
|
||
widentag = t;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
if (widensz == 8) {
|
||
// Nullable fold: pointer value IS the discriminator. No
|
||
// separate tag word.
|
||
cgexpr(c, arg);
|
||
emitline("\tPUSHQ\tAX\n");
|
||
return rest + 1;
|
||
};
|
||
if (widensz > 0) {
|
||
// Struct-payload widening into a tagged-union param uses
|
||
// @tagscr (zero + cgwidentaggedstore writes fields + tag,
|
||
// then push slot words high → low). Scalar / str go via
|
||
// the direct push fast path below — keeps wwstage's asm
|
||
// byte-identical to cstage for selfhost source.
|
||
// #66: a tuple-typed source has no direct-push shape — the
|
||
// scalar fast arm would push word 0 only (payload slot 1+
|
||
// dropped) and coerce an unresolved tag to 0. Route through
|
||
// the scratch store, whose #242/#66 tuple arm handles the
|
||
// literal/cast forms and loud-stops the rest (#72). Mirrors
|
||
// cstage cg_widen_tagged_push src_is_tuple.
|
||
let argtup: *syntax.tinfo = arg.type_: *syntax.tinfo;
|
||
argtup = tichase(argtup);
|
||
let argistuple: bool = false;
|
||
// #55: a tagged SOURCE widened into a wider/reordered tagged param
|
||
// (slot-DIFFER — the ident/deref/index/dot legs that fell past the
|
||
// slot-gated aistagged arms above) has no direct-push shape: the
|
||
// scalar fast arm below would box word0 with a tag clamped to 0,
|
||
// dropping the real tag + high words and skipping the variant
|
||
// remap. Route through the @tagscr store, whose ident/memread/
|
||
// cursor arms copy the box + tag-remap. Mirrors cstage
|
||
// cg_widen_tagged_push's unconditional src_is_tagged scratch
|
||
// routing (cmd/w6c/cgen.c:2982).
|
||
let argistagged: bool = false;
|
||
if (argtup != nil) {
|
||
if (argtup.kind == syntax.tykind.TY_TUPLE) {
|
||
argistuple = true;
|
||
};
|
||
if (argtup.kind == syntax.tykind.TY_TAGGED) {
|
||
argistagged = true;
|
||
};
|
||
};
|
||
let pname: str = rhsstructpayload(c, arg);
|
||
if (pname.len > 0 || argistuple || argistagged) {
|
||
let ptype: *syntax.node = param.lhs;
|
||
let scroff: i32 = tagscradd(c, widensz);
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let zz: i32 = 0;
|
||
for (zz < widensz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((scroff + zz): i64);
|
||
emitline("(BP)\n");
|
||
zz += 8;
|
||
};
|
||
cgwidentaggedstore(c, ptype.type_: *syntax.tinfo, arg, "BP", scroff, widensz);
|
||
let pp: i32 = widensz - 8;
|
||
for (pp >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((scroff + pp): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
pp -= 8;
|
||
};
|
||
return rest + widensz / 8;
|
||
};
|
||
cgexpr(c, arg);
|
||
if (nodeisslice(c, arg)) {
|
||
// Slice payload (24B): cgexpr leaves (AX=ptr, BX=len,
|
||
// CX=cap). Slot layout: [+0]=tag, [+8]=ptr, [+16]=len,
|
||
// [+24]=cap. Push high→low so pop drains tag first.
|
||
// Requires widensz >= 32; a smaller slot would mean the
|
||
// destination union doesn't list slice as a variant
|
||
// (caller should have flagged a type error).
|
||
emitline("\tPUSHQ\tCX\n");
|
||
emitline("\tPUSHQ\tBX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
emitline("\tMOVQ\t$");
|
||
emitint(widentag: i64);
|
||
emitline(", AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
} else { if (nodeisstr(c, arg)) {
|
||
// str IS []u8: slot 32 [+0]=tag,[+8]=ptr,[+16]=len,
|
||
// [+24]=cap — same shape as the slice arm above. Push
|
||
// cap, len, ptr, tag high→low so pop drains tag first
|
||
// into arg-reg[0] (#1/Phase 3).
|
||
emitline("\tPUSHQ\tCX\n");
|
||
emitline("\tPUSHQ\tBX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
emitline("\tMOVQ\t$");
|
||
emitint(widentag: i64);
|
||
emitline(", AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
} else {
|
||
// Scalar variant: single value word at +8. Pad a zero
|
||
// high word when slot is 24B (some other variant of
|
||
// the union is 16B-shaped).
|
||
let pp: i32 = widensz - 8;
|
||
for (pp > 8) {
|
||
emitline("\tXORQ\tDX, DX\n");
|
||
emitline("\tPUSHQ\tDX\n");
|
||
pp -= 8;
|
||
};
|
||
// #49: an f64/f32 payload sits in X0 (cgexpr left it
|
||
// there), not AX — spill it through the stack so the
|
||
// callee reads the real bits. A plain PUSHQ AX pushed
|
||
// whatever AX last held (stale for a runtime float
|
||
// producer; only a const folder leaves the bits in AX
|
||
// — why #48 with a no-payload-read arm passed but #49
|
||
// reading `d == 2.5` did not). Both stages (#263);
|
||
// cstage cg_widen_tagged_push twin.
|
||
if (isfloattype(c, arg)) {
|
||
emitline("\tSUBQ\t$8, SP\n");
|
||
let fmov: str = "MOVSD";
|
||
if (isf32type(c, arg)) { fmov = "MOVSS"; };
|
||
emitline("\t");
|
||
emitline(fmov);
|
||
emitline("\tX0, (SP)\n");
|
||
} else {
|
||
emitline("\tPUSHQ\tAX\n");
|
||
};
|
||
emitline("\tMOVQ\t$");
|
||
emitint(widentag: i64);
|
||
emitline(", AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
};};
|
||
return rest + widensz / 8;
|
||
};
|
||
// 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 == syntax.nkind.N_SLICE) {
|
||
let base: *syntax.node = arg.lhs;
|
||
let lo: *syntax.node = arg.rhs;
|
||
let hi: *syntax.node = arg.cond;
|
||
let baselocal: *local = nil;
|
||
let globaltn: *syntax.node = nil;
|
||
let globalname: str;
|
||
globalname.ptr = nil; globalname.len = 0;
|
||
if (base != nil) {
|
||
if (base.kind == syntax.nkind.N_IDENT) {
|
||
let bn: str = base.str;
|
||
baselocal = localfindnode(c, bn);
|
||
if (baselocal == nil) {
|
||
let gt: *syntax.node = letvartnode(c, bn);
|
||
if (gt != nil) {
|
||
globaltn = gt;
|
||
globalname = bn;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
// #257: an N_DOT `[N]T`-field base (`x.o[lo:hi]` as a call
|
||
// arg) carries no tnode — resolve esz / base-address from the
|
||
// checker-stamped element tinfo on base.type_ instead. Cstage
|
||
// twin reads base->type (cgen.c pushargs N_SLICE esz). Mirror
|
||
// of the cgslice #252 site.
|
||
let dotbu: *syntax.tinfo = nil;
|
||
if (base != nil) { if (base.kind == syntax.nkind.N_DOT) {
|
||
dotbu = base.type_: *syntax.tinfo;
|
||
dotbu = tichase(dotbu);
|
||
};};
|
||
// #60 (alias arc #5): alias-NAMED N_IDENT base — the tnode
|
||
// reads below see only the N_TNAME leaf (esz 1-sentinel,
|
||
// MOVQ base). Re-key off the chased stamped tinfo, the
|
||
// pusharg twin of the cgslice fix (cstage pushargs N_SLICE
|
||
// reads bu = type_chase_named(base->type) uniformly).
|
||
let basealias: bool = false;
|
||
let bu60: *syntax.tinfo = nil;
|
||
if (base != nil) { if (base.kind == syntax.nkind.N_IDENT) {
|
||
let bt60: *syntax.tinfo = base.type_: *syntax.tinfo;
|
||
if (bt60 != nil) { if (bt60.kind == syntax.tykind.TY_NAMED) {
|
||
basealias = true;
|
||
bu60 = tichase(bt60);
|
||
};};
|
||
};};
|
||
// esz from the type table for an N_IDENT base (#76; mirrors
|
||
// the cgindex idiom) or an N_DOT array/slice-field base (#257:
|
||
// scale by the field's element width, not esz=1 -> silently
|
||
// wrong for non-u8). Other non-ident bases stay esz=1.
|
||
// (#31: a bare N_ARRLIT arg never reaches here — it loud-rejects
|
||
// at the checker, supported only at a `let`; #33.)
|
||
let esz: i32 = 1;
|
||
if (baselocal != nil) {
|
||
esz = elemsizeofc(c, baselocal.tnode);
|
||
} else { if (globaltn != nil) {
|
||
esz = elemsizeofc(c, globaltn);
|
||
} else { if (dotbu != nil && dotbu.sub != nil) {
|
||
esz = dotbu.sub.size: i32;
|
||
};};};
|
||
if (bu60 != nil) {
|
||
let es60: *syntax.tinfo = tichase(bu60.sub);
|
||
if (es60 != nil) { esz = es60.size: i32; };
|
||
};
|
||
// base address → push
|
||
if (baselocal != nil) {
|
||
let tn: *syntax.node = baselocal.tnode;
|
||
let isarr60: bool = false;
|
||
if (tn != nil) {
|
||
if (tn.kind == syntax.nkind.N_TARRAY) { isarr60 = true; };
|
||
};
|
||
// #60: alias-NAMED base — chased kind (see cgindex twin).
|
||
if (bu60 != nil) { isarr60 = bu60.kind == syntax.tykind.TY_ARRAY; };
|
||
if (isarr60) {
|
||
emitline("\tLEAQ\t");
|
||
emitoff(baselocal.off: i64);
|
||
emitline("(BP), AX\n");
|
||
} else {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(baselocal.off: i64);
|
||
emitline("(BP), AX\n");
|
||
};
|
||
} else { if (globaltn != nil) {
|
||
let gisarr60: bool = globaltn.kind == syntax.nkind.N_TARRAY;
|
||
// #60: alias-NAMED base — chased kind; runtime-
|
||
// unreachable until #77/#78 global DATA.
|
||
if (bu60 != nil) { gisarr60 = bu60.kind == syntax.tykind.TY_ARRAY; };
|
||
if (gisarr60) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, globalname);
|
||
emitline("(SB), AX\n");
|
||
} else {
|
||
emitline("\tMOVQ\t");
|
||
emitsymname(c, globalname);
|
||
emitline("(SB), AX\n");
|
||
};
|
||
} else { if (dotbaseaddr(c, base, "AX")) {
|
||
// #257: N_DOT `[N]T`-field base as a call arg → field
|
||
// ADDRESS (LEAQ), not the auto-deref VALUE load cgexpr
|
||
// emits. Same choke-point as the cgslice #252 site;
|
||
// `[]T`/str/`*T` fields fall through to cgexpr.
|
||
} 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: *syntax.node = baselocal.tnode;
|
||
if (tn != nil) {
|
||
if (tn.kind == syntax.nkind.N_TARRAY) {
|
||
let lenn: *syntax.node = tn.rhs;
|
||
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
|
||
emitline("\tMOVQ\t$");
|
||
emituint(lenn.uval);
|
||
emitline(", AX\n");
|
||
} else {
|
||
// #56: def/const dim — resolve from the stamped
|
||
// array tinfo (rule-13). The #60 bu60 arm below
|
||
// covers only NAMED-alias bases (tn.kind ==
|
||
// N_TNAME); a plain `[MAX]u8` base reaches here
|
||
// with a non-N_INTLIT dim and dropped the
|
||
// default-hi entirely. cstage reads bu->alen.
|
||
let abt: *syntax.tinfo = tichase(tn.type_: *syntax.tinfo);
|
||
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
|
||
emitline("\tMOVQ\t$");
|
||
emitint(abt.alen: i64);
|
||
emitline(", AX\n");
|
||
};
|
||
};
|
||
} else { if (tn.kind == syntax.nkind.N_TSLICE) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((baselocal.off + 8): i64);
|
||
emitline("(BP), AX\n");
|
||
} else { if (tn.kind == syntax.nkind.N_TNAME) {
|
||
if (syntax.streq(tn.str, "str")) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((baselocal.off + 8): i64);
|
||
emitline("(BP), AX\n");
|
||
};
|
||
};};};
|
||
};
|
||
// #60: alias-NAMED base default-hi — chased kind
|
||
// (see the cgslice twin).
|
||
if (bu60 != nil) {
|
||
if (bu60.kind == syntax.tykind.TY_ARRAY) {
|
||
emitline("\tMOVQ\t$");
|
||
emitint(bu60.alen: i64);
|
||
emitline(", AX\n");
|
||
} else { if (bu60.kind == syntax.tykind.TY_SLICE
|
||
|| bu60.kind == syntax.tykind.TY_STR) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((baselocal.off + 8): i64);
|
||
emitline("(BP), AX\n");
|
||
};};
|
||
};
|
||
} else { if (globaltn != nil) {
|
||
if (globaltn.kind == syntax.nkind.N_TARRAY) {
|
||
let lenn: *syntax.node = globaltn.rhs;
|
||
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
|
||
emitline("\tMOVQ\t$");
|
||
emituint(lenn.uval);
|
||
emitline(", AX\n");
|
||
} else {
|
||
// #56: def/const dim on a global array base —
|
||
// resolve from the stamped array tinfo (rule-13),
|
||
// twin of the local arg-push arm above.
|
||
let abt: *syntax.tinfo = tichase(globaltn.type_: *syntax.tinfo);
|
||
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
|
||
emitline("\tMOVQ\t$");
|
||
emitint(abt.alen: i64);
|
||
emitline(", AX\n");
|
||
};
|
||
};
|
||
} else { if (globaltn.kind == syntax.nkind.N_TSLICE) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, globalname);
|
||
emitline("(SB), CX\n");
|
||
emitline("\tMOVQ\t8(CX), AX\n");
|
||
} else { if (globaltn.kind == syntax.nkind.N_TNAME) {
|
||
if (syntax.streq(globaltn.str, "str")) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, globalname);
|
||
emitline("(SB), CX\n");
|
||
emitline("\tMOVQ\t8(CX), AX\n");
|
||
};
|
||
};};};
|
||
// #60: alias-NAMED global base default-hi — chased
|
||
// kind; runtime-unreachable until #77/#78 global DATA.
|
||
if (bu60 != nil) {
|
||
if (bu60.kind == syntax.tykind.TY_ARRAY) {
|
||
emitline("\tMOVQ\t$");
|
||
emitint(bu60.alen: i64);
|
||
emitline(", AX\n");
|
||
} else { if (bu60.kind == syntax.tykind.TY_SLICE
|
||
|| bu60.kind == syntax.tykind.TY_STR) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, globalname);
|
||
emitline("(SB), CX\n");
|
||
emitline("\tMOVQ\t8(CX), 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
|
||
// ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit).
|
||
// BX=lo*esz; AX=lo PRESERVED for cap. BX (dead hi) reloaded
|
||
// by cgbasecap below.
|
||
if (esz > 1) {
|
||
emitline("\tMOVQ\t$");
|
||
emitint(esz: i64);
|
||
emitline(", BX\n");
|
||
emitline("\tIMULQ\tAX, BX\n");
|
||
emitline("\tADDQ\tBX, CX\n");
|
||
} else {
|
||
emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr
|
||
};
|
||
// cap = base_cap - lo (#20); AX=lo, BX free.
|
||
if (cgbasecap(c, base, "BX")) {
|
||
emitline("\tSUBQ\tAX, BX\n");
|
||
emitline("\tPUSHQ\tBX\n"); // cap
|
||
} else {
|
||
emitline("\tPUSHQ\tDX\n"); // cap = len
|
||
};
|
||
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).
|
||
// For tagged ident with a >24B slot (slice-payload variant),
|
||
// push a fourth word from off+24.
|
||
if (arg.kind == syntax.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(c, lc.tnode)) {
|
||
let nwords: i32 = 3;
|
||
if (istaggedtype(c, lc.tnode)) {
|
||
let ssz: i32 = slotsize(c, lc.tnode);
|
||
nwords = ssz / 8;
|
||
};
|
||
let w: i32 = nwords - 1;
|
||
for (w >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((off + w*8): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
w -= 1;
|
||
};
|
||
return rest + nwords;
|
||
};
|
||
// By-value struct ident: load qword(s) from the slot
|
||
// and push high → low so left-to-right pop on the
|
||
// callee side lands word 0 / word 1 into the SysV arg
|
||
// register pair. Mirrors cstage cgen.c §4240 (call
|
||
// site) so the wwstage prologue's new struct spill arm
|
||
// (cgendecl.ww structparamsize branch) sees the same
|
||
// reg layout. Pre-#11 the call-site fell through to
|
||
// `cgexpr(c, arg)` + scalar PUSHQ AX — only the first
|
||
// 8B word made it across, and the callee's second-arg
|
||
// slots picked up the wrong neighbour's value.
|
||
let stsz: i32 = structparamsize(c, lc.tnode);
|
||
if (stsz > 0) {
|
||
if (stsz > 8) {
|
||
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");
|
||
let nw: i32 = 1;
|
||
if (stsz > 8) { nw = 2; };
|
||
return rest + nw;
|
||
};
|
||
};
|
||
// #150: a module-global by-value struct arg. localfindnode
|
||
// above misses it (a global is not a frame slot), and the #271
|
||
// arm below excludes a ≤16B struct ident (structident=true), so
|
||
// pre-fix it fell through to the scalar single-PUSHQ default and
|
||
// silently dropped word1. cstage's mirror-opposite bug read the
|
||
// FRAME (localfind→0 off==0 footgun); both stages converge here
|
||
// on the global-base load: LEAQ name(SB) into BX, then copy ALL
|
||
// eightbytes high→low (the #256/#129-A.2 struct-global shape; the
|
||
// isletvar||deflookup gate is the let_islet||def_isstructdef twin
|
||
// from dotchainaddr). The slot-word count is the stamped tinfo
|
||
// size (the type table, byte-id with cstage struct_arg_size).
|
||
if (lc == nil) {
|
||
let gst: *syntax.tinfo = arg.type_: *syntax.tinfo;
|
||
gst = tichase(gst);
|
||
if (gst != nil) { if (gst.kind == syntax.tykind.TY_STRUCT) {
|
||
let gsz: i32 = gst.size: i32;
|
||
if (gsz > 0 && gsz <= 16
|
||
&& (isletvar(c, nm) || deflookup(c, nm))) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, nm);
|
||
emitline("(SB), BX\n");
|
||
if (gsz > 8) {
|
||
emitline("\tMOVQ\t8(BX), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
};
|
||
emitline("\tMOVQ\t(BX), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
let gnw: i32 = 1;
|
||
if (gsz > 8) { gnw = 2; };
|
||
return rest + gnw;
|
||
};
|
||
}; };
|
||
// #151: a module-global by-value slice/str arg — the
|
||
// slice/str twin of the #150 struct arm above. wwstage's
|
||
// nodeisslice/nodeisstr (below) are LOCAL-keyed
|
||
// (localfindnode→nil for a global) so a global slice/str
|
||
// ident returned false there and fell to the scalar
|
||
// single-PUSHQ default, dropping len+cap. cstage is
|
||
// type-keyed (node_isslice/node_isstr on n->type) so it
|
||
// pushed all 3 header words. cstage emits a DIFFERENT
|
||
// per-type sequence — mirror EACH for byte-id: a slice via
|
||
// its dedicated push arm (BX-base per-word, cgen.c:9124); a
|
||
// str falls to cgexpr's cgslicehdr (CX-base AX/BX/CX,
|
||
// cgen.c:1866) then the node_isstr triple push.
|
||
// LET-only (NOT deflookup, unlike the #150 struct arm):
|
||
// cstage's slice arm gates let_islet (cgen.c:1869) and a
|
||
// def-str is const-folded by cgexpr (LEAQ _S_0, MOVQ
|
||
// $len) — it has no name(SB) holder. A deflookup here
|
||
// would LEAQ an undefined main.<def>(SB) (w6l fails); a
|
||
// def must fall through to the const-fold path instead.
|
||
if (gst != nil && isletvar(c, nm)) {
|
||
if (gst.kind == syntax.tykind.TY_SLICE) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, nm);
|
||
emitline("(SB), BX\n");
|
||
emitline("\tMOVQ\t16(BX), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
emitline("\tMOVQ\t8(BX), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
emitline("\tMOVQ\t(BX), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
return rest + 3;
|
||
};
|
||
if (gst.kind == syntax.tykind.TY_STR) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, nm);
|
||
emitline("(SB), CX\n");
|
||
emitline("\tMOVQ\t(CX), AX\n");
|
||
emitline("\tMOVQ\t8(CX), BX\n");
|
||
emitline("\tMOVQ\t16(CX), CX\n");
|
||
emitline("\tPUSHQ\tCX\n");
|
||
emitline("\tPUSHQ\tBX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
return rest + 3;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
// #271: aggregate (struct/array) arg from any source the ≤16B
|
||
// struct-IDENT fast path above doesn't cover — a 16B struct from a
|
||
// non-ident source, OR any array, OR a struct > 16B. The arg twin
|
||
// of the #265/#268 let-init copy (mirror of cstage cgen.c #271 push
|
||
// arm): materialise the source ADDRESS in SI and push its ceil(sz/8)
|
||
// words high→low (the pop drains word0 into the first arg reg). A
|
||
// CALL source receives first — ≤24B in AX/DX/CX pushed straight,
|
||
// >24B sret'd into @aggargscr then pushed from there. Pre-fix every
|
||
// such source fell to the scalar default (one PUSHQ for a multi-word
|
||
// aggregate) and stack-imbalanced against the type-based drain.
|
||
let aggsz: i32 = aggargsizetn(arg.type_: *syntax.tinfo);
|
||
if (aggsz > 0) {
|
||
// Exclude a ≤16B-struct IDENT — it owns the structparamsize
|
||
// fast path above (or, when a cross-module same-leaf collision
|
||
// makes the name-keyed structparamsize miss it, the scalar
|
||
// default below, byte-id with cstage's 1-word struct push;
|
||
// #784/#223). The exclusion is TYPE-keyed via the stamped
|
||
// tinfo, mirroring cstage node_isstructarg (struct_arg_size on
|
||
// args[i]->type) — a name-keyed gate here re-opens the #211/#13
|
||
// name-keyed divergence the cstage type gate doesn't have.
|
||
let structident: bool = false;
|
||
if (arg.kind == syntax.nkind.N_IDENT) {
|
||
let st: *syntax.tinfo = arg.type_: *syntax.tinfo;
|
||
st = tichase(st);
|
||
if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) {
|
||
if (st.size: i32 <= 16) { structident = true; };
|
||
}; };
|
||
};
|
||
if (!structident) {
|
||
if (aggargfloatstop(arg)) {
|
||
let msg: str = "#271/#165: float-bearing struct arg from a non-ident source needs SSE eightbyte transport (out of scope)\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let nwords: i32 = (aggsz + 7) / 8;
|
||
if (arg.kind == syntax.nkind.N_CALL) {
|
||
if (callsretsize(c, arg) > 0) {
|
||
let scr: i32 = localadd(c, "@aggargscr",
|
||
aggsz, nil);
|
||
c.sretdestoff = scr;
|
||
cgexpr(c, arg);
|
||
c.sretdestoff = 0;
|
||
let k: i32 = nwords - 1;
|
||
for (k >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((scr + k*8): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
k -= 1;
|
||
};
|
||
} else {
|
||
// ≤24B: producer left AX=word0,
|
||
// DX=word1, CX=word2. Push high→low so
|
||
// the pop drains word0 first.
|
||
cgexpr(c, arg);
|
||
let k: i32 = nwords - 1;
|
||
for (k >= 0) {
|
||
if (k == 2) {
|
||
emitline("\tPUSHQ\tCX\n");
|
||
} else { if (k == 1) {
|
||
emitline("\tPUSHQ\tDX\n");
|
||
} else {
|
||
emitline("\tPUSHQ\tAX\n");
|
||
}; };
|
||
k -= 1;
|
||
};
|
||
};
|
||
return rest + nwords;
|
||
};
|
||
if (!aggargsrcaddr(c, arg, "SI")) {
|
||
let msg: str = "#271: aggregate arg from unsupported source kind\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let k: i32 = nwords - 1;
|
||
for (k >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((k*8): i64);
|
||
emitline("(SI), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
k -= 1;
|
||
};
|
||
return rest + nwords;
|
||
};
|
||
};
|
||
// 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 = 0;
|
||
if (arg != nil) {
|
||
let at: *syntax.tinfo = arg.type_: *syntax.tinfo;
|
||
if (syntax.typeisf32(at)) { fk = 1; }
|
||
else { if (syntax.typeisfloat(at)) { fk = 2; }; };
|
||
};
|
||
if (fk != 0) {
|
||
cgexpr(c, arg);
|
||
let mov: str = "MOVSD";
|
||
if (fk == 1) { mov = "MOVSS"; };
|
||
// #14: an f32 in the C-variadic tail widens to f64 (C default
|
||
// argument promotion) — CVTSS2SD in X0, then MOVSD spills a
|
||
// full 8B double; the pop reloads MOVSD and counts it as one
|
||
// SSE reg. Mirror cstage cmd/w6c/cgen.c promote_f32 push.
|
||
if (cvarnfixed >= 0 && argidx >= cvarnfixed && fk == 1) {
|
||
emitline("\tCVTSS2SD\tX0, X0\n");
|
||
mov = "MOVSD";
|
||
};
|
||
emitline("\tSUBQ\t$8, SP\n");
|
||
emitline("\t");
|
||
emitline(mov);
|
||
emitline("\tX0, (SP)\n");
|
||
return rest + 1;
|
||
};
|
||
// #68: a tuple-LITERAL arg derives its DECLARED tuple type from the
|
||
// callee PARAM type node (the #57 decl wire, extended to call-arg
|
||
// send), so a declared-tagged element's concrete rvalue widens into
|
||
// the box cursor; the restage + push then key on the param tuple type
|
||
// node (declared element widths), not the literal's element-
|
||
// constructed types. Mirrors cstage cgcall paramtup.
|
||
let paramtt: *syntax.node = nil;
|
||
if (arg.kind == syntax.nkind.N_TUPLE) { if (param != nil) {
|
||
if (param.kind == syntax.nkind.N_PARAM && param.lhs != nil) {
|
||
let ptn: *syntax.node = param.lhs;
|
||
for (ptn != nil && ptn.kind == syntax.nkind.N_TNAME) {
|
||
ptn = aliaslookup(c, ptn.str);
|
||
};
|
||
if (ptn != nil) { if (ptn.kind == syntax.nkind.N_TTUPLE) { paramtt = ptn; }; };
|
||
};
|
||
}; };
|
||
if (paramtt != nil) { cgtuplelittocursor(c, arg, paramtt); }
|
||
else { cgexpr(c, arg); };
|
||
// #163/#32 (C-t2): tuple ARG (param twin of #164's return). cgexpr /
|
||
// the decl-aware fill left the tuple in the return-ABI cursor (AX/DX/
|
||
// CX/R8 + X0/X1) for every nodetuplearg producer — call (return ABI),
|
||
// ident (cgtupleslottocursor), literal (cgtuplelittocursor), `?`/`!`
|
||
// unwrap (payload shift); restage it into @tupargscr by SysV class
|
||
// (tupstore, the #164 helper) and push the slot words high->low so
|
||
// the pop drains slot+0 first into the SysV ARG cursor. The frame
|
||
// slot decouples the return-class regs from the overlapping
|
||
// arg-class regs. When the PARAM tuple type is known (#68), walk its
|
||
// declared element type nodes (p.lhs); else an N_TUPLE literal's
|
||
// elements are VALUE exprs classified the way cgtuplelittocursor does.
|
||
let tuparg: *syntax.node = paramtt;
|
||
if (tuparg == nil) { tuparg = nodetuplearg(c, arg); };
|
||
if (tuparg != nil) {
|
||
let tuplit: bool = false;
|
||
if (paramtt == nil) { if (tuparg.kind == syntax.nkind.N_TUPLE) { tuplit = true; }; };
|
||
let gptot: i32 = 0;
|
||
let sstot: i32 = 0;
|
||
let tsz: i32 = 0;
|
||
let p: *syntax.node = tuparg.list;
|
||
for (p != nil) {
|
||
let et: *syntax.node = p.lhs;
|
||
if (tuplit) { et = p; };
|
||
// C-t2 (ken demand 1, rule 7): a COMPOSITE element
|
||
// (nested tuple/struct/array/tagged) occupies more
|
||
// than the one GP word this walk counts — the checker
|
||
// accepts the shape but the cursor transport cannot
|
||
// carry it; pre-guard it ran WRONG (inner words
|
||
// skewed). The stamped tinfo classifies both type
|
||
// nodes and literal value exprs. Mirrors the cstage
|
||
// restage guard.
|
||
let eti: *syntax.tinfo = et.type_: *syntax.tinfo;
|
||
eti = tichase(eti);
|
||
if (eti != nil) {
|
||
let bad: bool = eti.kind == syntax.tykind.TY_TUPLE
|
||
|| eti.kind == syntax.tykind.TY_STRUCT
|
||
|| eti.kind == syntax.tykind.TY_ARRAY;
|
||
// #68: a declared-tagged element graduates to a real
|
||
// widen — the decl-aware send (cgtuplelittocursor over
|
||
// the param tuple type) left the box words in the
|
||
// cursor, so tupstore below carries them. A tagged
|
||
// element with no param decl stays rule-7 loud (the
|
||
// cursor was filled stamped-keyed).
|
||
if (eti.kind == syntax.tykind.TY_TAGGED && paramtt == nil) { bad = true; };
|
||
if (bad) {
|
||
let mne: str = "#32: tuple arg element kind unsupported (nested tuple/struct/array/tagged; rule 7)\n";
|
||
os.write(2, mne.ptr, mne.len: u64);
|
||
os.exit(1);
|
||
};
|
||
};
|
||
// eslot — the full slot stride (str/slice header, tagged
|
||
// box, else 8); on the declared (non-tuplit) path tupeslotn
|
||
// reads the element TYPE node directly (#68 box-aware,
|
||
// mirrors cstage tuple_eslot). A literal element rides the
|
||
// wide-vs-scalar split off its VALUE node.
|
||
let eslot: i32 = 8;
|
||
if (tuplit) {
|
||
let wide: bool = nodeisstr(c, et) || nodeisslice(c, et);
|
||
if (wide) { eslot = tyslicesize(): i32; };
|
||
} else {
|
||
eslot = tupeslotn(et);
|
||
};
|
||
if (isfloattype(c, et)) { sstot += 1; }
|
||
else { gptot += eslot / 8; };
|
||
tsz += eslot;
|
||
p = p.next;
|
||
};
|
||
// The producing cursor fill already satisfied #164's caps;
|
||
// guard anyway (tupstore indexes [AX,DX,CX,R8] / [X0,X1]).
|
||
if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP) {
|
||
let msg: str = "tuple arg exceeds return-cursor ABI capacity; see #163/#164\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let scr: i32 = localadd(c, "@tupargscr", tsz, nil);
|
||
let gpcur: i32 = 0;
|
||
let ssecur: i32 = 0;
|
||
let eoff: i32 = 0;
|
||
p = tuparg.list;
|
||
for (p != nil) {
|
||
let et: *syntax.node = p.lhs;
|
||
if (tuplit) { et = p; };
|
||
let eslot: i32 = 8;
|
||
if (tuplit) {
|
||
let wide: bool = nodeisstr(c, et) || nodeisslice(c, et);
|
||
if (wide) { eslot = tyslicesize(): i32; };
|
||
} else {
|
||
eslot = tupeslotn(et);
|
||
};
|
||
tupstore(c, gpcur, ssecur, scr + eoff, eslot, et);
|
||
if (isfloattype(c, et)) { ssecur += 1; }
|
||
else { gpcur += eslot / 8; };
|
||
eoff += eslot;
|
||
p = p.next;
|
||
};
|
||
let w: i32 = tsz - 8;
|
||
for (w >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((scr + w): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
w -= 8;
|
||
};
|
||
return rest + tsz / 8;
|
||
};
|
||
// #32 (C-t2, rule 7): a tuple-typed arg from a source shape whose
|
||
// cgexpr does NOT fill the return cursor (chain reads, match exprs,
|
||
// ...) must die loud here — pre-fix it fell to the scalar
|
||
// single-PUSHQ default and silently skewed every later arg
|
||
// register. Mirrors the cstage cgcall guard.
|
||
{
|
||
let ati: *syntax.tinfo = arg.type_: *syntax.tinfo;
|
||
ati = tichase(ati);
|
||
if (ati != nil) {
|
||
if (ati.kind == syntax.tykind.TY_TUPLE) {
|
||
let m32: str = "#32: tuple arg from unsupported source shape (call/ident/literal/unwrap only; rule 7)\n";
|
||
os.write(2, m32.ptr, m32.len: u64);
|
||
os.exit(1);
|
||
};
|
||
};
|
||
};
|
||
if (nodeisslice(c, arg)) {
|
||
emitline("\tPUSHQ\tCX\n");
|
||
emitline("\tPUSHQ\tBX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
return rest + 3;
|
||
};
|
||
if (nodeisstr(c, arg)) {
|
||
// str IS []u8: cgexpr left (AX=ptr, BX=len, CX=cap). Push
|
||
// the triple, same as the slice arm above (#1/Phase 3).
|
||
emitline("\tPUSHQ\tCX\n");
|
||
emitline("\tPUSHQ\tBX\n");
|
||
emitline("\tPUSHQ\tAX\n");
|
||
return rest + 3;
|
||
};
|
||
// #21: CALL returning a tagged-union — the aistagged guard
|
||
// above kept us out of the widening path. Push the tagged-
|
||
// return ABI registers (AX=tag, DX=word0, CX=word1, R8=word2)
|
||
// high → low so the left-to-right POPQ into argregs drains the
|
||
// tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387.
|
||
let tcs: i32 = taggedcallslot(c, arg);
|
||
if (tcs > 0) {
|
||
// #38b residual (rule 7): an sret-class call result is in
|
||
// memory, not the cursor — the @aggargscr-style receive-then-
|
||
// push is the #40-family follow-up. Mirrors cstage cgen.c
|
||
// cgcall tagged arg-push gate.
|
||
if (callsretsize(c, arg) > 0) {
|
||
let m38r: str = "#38b: >32B tagged call result as a call argument unwired (#40-family follow-up)\n";
|
||
os.write(2, m38r.ptr, m38r.len: u64);
|
||
os.exit(1);
|
||
};
|
||
if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); };
|
||
if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); };
|
||
if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); };
|
||
emitline("\tPUSHQ\tAX\n");
|
||
return rest + tcs / 8;
|
||
};
|
||
// #12: N_INDEX of a sum-typed slice element. cgindex above left
|
||
// the tagged-CALL ABI in AX/DX/CX/R8; the bare PUSHQ AX below
|
||
// would only carry the tag word and drop the payload. #60:
|
||
// arg.type_ is the element tinfo (istaggedtype/slotsize read
|
||
// .type_) — feed the N_INDEX node directly, dropping the
|
||
// indexvaluetnode walk.
|
||
// #22a: N_DOT rides the same arm — the t.N tuple-element box load
|
||
// (this arc) fills the identical AX/DX/CX/R8 cursor. cstage's twin
|
||
// is the generic node_istaggedarg push (cgen.c cgcall); wwstage
|
||
// keeps the stamped-carrier kind gate (#67 pattern) — the
|
||
// remaining kinds (deref/cast/unwrap) are word0-only reads today,
|
||
// filed residual.
|
||
if (arg.kind == syntax.nkind.N_INDEX || arg.kind == syntax.nkind.N_DOT
|
||
|| (arg.kind == syntax.nkind.N_UN && arg.op == syntax.tkind.TK_STAR)) {
|
||
if (istaggedtype(c, arg)) {
|
||
let isz: i32 = slotsize(c, arg);
|
||
// #35 (Family C): a mem-based read left the box
|
||
// ADDRESS in AX — push the words from memory
|
||
// high→low, the mem twin of the cursor push below.
|
||
// Covers the any-size deref source and the 33-48B
|
||
// INDEX/DOT reads that loud-stopped here pre-#35.
|
||
// Mirrors cstage.
|
||
if (taggedmemread(c, arg)) {
|
||
let mk35: i32 = isz - 8;
|
||
for (mk35 >= 0) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg(mk35: i64, "AX");
|
||
emitline(", DX\n");
|
||
emitline("\tPUSHQ\tDX\n");
|
||
mk35 -= 8;
|
||
};
|
||
return rest + isz / 8;
|
||
};
|
||
if (isz > 24) { emitline("\tPUSHQ\tR8\n"); };
|
||
if (isz > 16) { emitline("\tPUSHQ\tCX\n"); };
|
||
if (isz > 8) { emitline("\tPUSHQ\tDX\n"); };
|
||
emitline("\tPUSHQ\tAX\n");
|
||
return rest + isz / 8;
|
||
};
|
||
};
|
||
emitline("\tPUSHQ\tAX\n");
|
||
return rest + 1;
|
||
};
|
||
|
||
// taggedcallslot — if `n` is an N_CALL whose callee returns a tagged
|
||
// type, returns the slot size in bytes; else 0. Used by pushargsrev's
|
||
// aistagged guard and natural-push arm, and by cgcall's pop sizer, to
|
||
// route a tagged-return call result through the AX/DX/CX/R8 high→low
|
||
// push convention rather than the concrete-variant widening path
|
||
// (which drops DX/CX/R8). See task #21.
|
||
export fn taggedcallslot(c: *cgen, n: *syntax.node) i32 = {
|
||
if (n == nil) { return 0; };
|
||
if (n.kind != syntax.nkind.N_CALL) { return 0; };
|
||
let callee: *syntax.node = n.lhs;
|
||
if (callee == nil) { return 0; };
|
||
if (callee.kind != syntax.nkind.N_IDENT) { return 0; };
|
||
let rtyp: *syntax.node = fnretlookup(c, callee.str);
|
||
if (!istaggedtype(c, rtyp)) { return 0; };
|
||
return slotsize(c, rtyp);
|
||
};
|
||
|
||
fn nodeisslice(c: *cgen, n: *syntax.node) bool = {
|
||
if (n == nil) { return false; };
|
||
let k: syntax.nkind = n.kind;
|
||
if (k == syntax.nkind.N_IDENT) {
|
||
let nm: str = n.str;
|
||
let lc: *local = localfindnode(c, nm);
|
||
// F7-c1: the local read collapses onto the checker-stamped
|
||
// n.type_, the same shape cstage's node_isslice uses
|
||
// (cmd/w6c/cgen.c:182 type_isslice(n->type)). lc.tnode.type_
|
||
// (what isslicetype read) and n.type_ are the same tinfo for a
|
||
// local ident (exprtype N_IDENT stamps n.type_ off the same
|
||
// decl localfindnode keys on); the localfindnode gate stays to
|
||
// keep the global-var-not-def case routing to false (out of F7
|
||
// scope), so this is a zero-delta mechanic validation.
|
||
if (lc != nil) { return syntax.typeisslice(n.type_: *syntax.tinfo); };
|
||
// #21: a module-level `def g: []T` ident is not a frame
|
||
// slot (localfindnode→nil), so the local arm above misses
|
||
// it and it would fall to the scalar single-PUSHQ default,
|
||
// dropping len/cap. cstage's node_isslice is type-keyed
|
||
// (cmd/w6c/cgen.c:226-229 type_isslice(n->type)); align
|
||
// wwstage UP by reading the checker-stamped .type_ (the def
|
||
// decl's str/slice type node, check.ww exprtype N_IDENT
|
||
// arm). A def has no DATA symbol — it rides cgident's
|
||
// const-fold, so the push site and cgcall's pop sizer flip
|
||
// together on this shared recognizer (load-bearing).
|
||
if (deflookup(c, nm)) {
|
||
return syntax.typeisslice(n.type_: *syntax.tinfo);
|
||
};
|
||
return false;
|
||
};
|
||
if (k == syntax.nkind.N_SLICE) { return true; };
|
||
if (k == syntax.nkind.N_CAST) { return isslicetype(c, n.rhs); };
|
||
// #24: N_CALL returning a slice — cgexpr leaves (AX=ptr,
|
||
// BX=len, CX=cap); pushargsrev's slice arm pushes CX/BX/AX
|
||
// and cgcall pops 3 words. Without this arm the natural-push
|
||
// fallthrough emits one PUSHQ AX (loses .len/.cap) and the pop
|
||
// side under-drains by 2 words, leaving R8/R9 unset for the
|
||
// receiver. Mirrors nodeisstr's N_CALL arm just below.
|
||
// N_DOT (cross-module callee, #34): route through fnretlookupmod
|
||
// so a same-leaf caller-module fn with diverging return shape
|
||
// doesn't shadow the explicit `mod.f()` qualifier — surfaced by
|
||
// strings.slice returning `frombytes(utf8.slice(...))`
|
||
// where strings.slice itself returns str.
|
||
if (k == syntax.nkind.N_CALL) {
|
||
let callee: *syntax.node = n.lhs;
|
||
if (callee != nil) {
|
||
if (callee.kind == syntax.nkind.N_IDENT) {
|
||
let rtyp: *syntax.node = fnretlookupmod(c, callee.str, c.curmod);
|
||
return isslicetype(c, rtyp);
|
||
};
|
||
if (callee.kind == syntax.nkind.N_DOT) {
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
let rtyp: *syntax.node = fnretlookupmod(c, callee.str, cmod);
|
||
return isslicetype(c, rtyp);
|
||
};
|
||
};
|
||
return false;
|
||
};
|
||
// N_DOT: read the checker-stamped n.type_. Struct field, nested
|
||
// dot, value-struct hops, and pseudo-fields (.ptr/.len/.cap) all
|
||
// resolve to the right tinfo via check.ww:1947-1978 (pseudo-field
|
||
// + struct-field stamps). Cstage cgen.c:182-184 node_isslice =
|
||
// type_isslice(n->type) — same shape. Collapsed per A.6.3h (#56).
|
||
if (k == syntax.nkind.N_DOT) {
|
||
return syntax.typeisslice(n.type_: *syntax.tinfo);
|
||
};
|
||
// #45 (F7-c2): `arr[i]` whose element is a slice. cgindex leaves
|
||
// (AX=ptr, BX=len, CX=cap) for a 24B element, but with no N_INDEX
|
||
// arm here pushargsrev fell to the scalar default — one PUSHQ AX —
|
||
// and the cgcall pop under-drained by 2 words (take(rows[1]) pushed
|
||
// 1 word, callee read garbage len). Read the checker-stamped element
|
||
// type (exprtype N_INDEX stamps n.type_, check.ww:2777), mirroring
|
||
// cstage node_isslice = type_isslice(n->type) and the N_INDEX arms of
|
||
// nodeisstr (below) + nodeisunsigned (:1798).
|
||
if (k == syntax.nkind.N_INDEX) {
|
||
return syntax.typeisslice(n.type_: *syntax.tinfo);
|
||
};
|
||
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).
|
||
//
|
||
// The value-bearing arms (N_IDENT local, N_INDEX, N_DOT) read the
|
||
// checker-stamped n.type_ — the typed-AST check the prior TODO(#11)
|
||
// wanted, mirroring cstage node_isstr = type_isstr(n->type). The
|
||
// remaining N_kind arms (N_STRLIT, N_CALL, N_CAST) carry their own
|
||
// recognizer because the stamp is on a sub-node (callee return / cast
|
||
// target), not on `n` itself. Covered: N_STRLIT, N_IDENT (local stamp /
|
||
// def stamp), N_CALL (return type), N_INDEX (element stamp — #46 F7-c2),
|
||
// N_DOT (n.type_ — #56 A.6.3h), N_CAST.
|
||
// Not covered (separate bugs / out of scope):
|
||
// - N_UN(TK_STAR) of `*str` — cgun itself emits only `MOVQ (AX), AX`
|
||
// and never loads .len into BX; fixing the recognizer alone won't
|
||
// help. Tracked alongside the broader cgun-load-shape gap.
|
||
fn nodeisstr(c: *cgen, n: *syntax.node) bool = {
|
||
if (n == nil) { return false; };
|
||
let k: syntax.nkind = n.kind;
|
||
if (k == syntax.nkind.N_STRLIT) { return true; };
|
||
if (k == syntax.nkind.N_IDENT) {
|
||
let nm: str = n.str;
|
||
let lc: *local = localfindnode(c, nm);
|
||
// F7-c1: the local read collapses onto the checker-stamped
|
||
// n.type_, the same shape cstage's node_isstr uses (cmd/w6c/
|
||
// cgen.c:213 type_isstr(n->type)). typeisstr chases TY_NAMED so
|
||
// `!str` aliases (parserr = !str) and `type foo = str;` chains
|
||
// resolve through exactly as isstrtype(lc.tnode) did — n.type_
|
||
// and lc.tnode.type_ are the same tinfo for a local ident
|
||
// (exprtype N_IDENT stamps n.type_ off the same decl
|
||
// localfindnode keys on). The localfindnode gate stays to keep
|
||
// the global-var-not-def case routing to false (out of F7
|
||
// scope), so this is a zero-delta mechanic validation.
|
||
if (lc != nil) { return syntax.typeisstr(n.type_: *syntax.tinfo); };
|
||
// #21: a module-level `def s: str` ident is not a frame slot
|
||
// (localfindnode→nil), so the local arm above misses it and
|
||
// it would fall to the scalar single-PUSHQ default, dropping
|
||
// len/cap. cstage's node_isstr is type-keyed (cmd/w6c/
|
||
// cgen.c:213-216 type_isstr(n->type)); align wwstage UP by
|
||
// reading the checker-stamped .type_ (the def decl's str type
|
||
// node, check.ww exprtype N_IDENT arm). A def has no DATA
|
||
// symbol — it rides cgident's const-fold (LEAQ _S_n, MOVQ
|
||
// $len), so the push site and cgcall's pop sizer flip together
|
||
// on this shared recognizer (load-bearing).
|
||
if (deflookup(c, nm)) {
|
||
return syntax.typeisstr(n.type_: *syntax.tinfo);
|
||
};
|
||
return false;
|
||
};
|
||
if (k == syntax.nkind.N_CALL) {
|
||
let callee: *syntax.node = n.lhs;
|
||
if (callee != nil) {
|
||
if (callee.kind == syntax.nkind.N_IDENT) {
|
||
let rtyp: *syntax.node = fnretlookupmod(c, callee.str, c.curmod);
|
||
return isstrtype(c, rtyp);
|
||
};
|
||
// #34: cross-module N_DOT — route through fnretlookupmod
|
||
// so a same-leaf caller-module fn (different return shape)
|
||
// doesn't shadow the explicit qualifier.
|
||
if (callee.kind == syntax.nkind.N_DOT) {
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
let rtyp: *syntax.node = fnretlookupmod(c, callee.str, cmod);
|
||
return isstrtype(c, rtyp);
|
||
};
|
||
};
|
||
return false;
|
||
};
|
||
// #46 (F7-c2): `arr[i]` whose element is a str. The prior structural
|
||
// walk only recognised N_IDENT and N_DOT bases (idxelemtn off the
|
||
// base's tnode), so a chained / call / slice base (take(m[1][1]))
|
||
// fell through to `return false` → 1-word push, callee read garbage
|
||
// .len. Read the checker-stamped element type instead (exprtype
|
||
// N_INDEX stamps n.type_, check.ww:2777), mirroring cstage node_isstr
|
||
// = type_isstr(n->type) and nodeisunsigned's N_INDEX arm (:1798). The
|
||
// base-kind whitelist is gone — every base shape routes through the
|
||
// one stamp read.
|
||
if (k == syntax.nkind.N_INDEX) {
|
||
return syntax.typeisstr(n.type_: *syntax.tinfo);
|
||
};
|
||
// N_DOT: read the checker-stamped n.type_. Struct field, nested
|
||
// dot, value-struct hops, and pseudo-fields (.ptr/.len/.cap) all
|
||
// resolve to the right tinfo via check.ww:1947-1978. Cstage
|
||
// cgen.c:168-170 node_isstr = type_isstr(n->type) — same shape.
|
||
// Collapsed per A.6.3h (#56).
|
||
if (k == syntax.nkind.N_DOT) {
|
||
return syntax.typeisstr(n.type_: *syntax.tinfo);
|
||
};
|
||
if (k == syntax.nkind.N_CAST) {
|
||
return isstrtype(c, n.rhs);
|
||
};
|
||
return false;
|
||
};
|
||
|
||
// typeis8byteprimitive — does this type take exactly one 8-byte
|
||
// slot rather than a wider aggregate? One-liner via typeis8byteprim
|
||
// (cstage N_LET sz==8 ladder SSoT). t.type_ is stamped at check.ww
|
||
// L426-436 for every type-AST kind callers reach. Collapsed onto the
|
||
// tinfo helper per A.6.3b (#46).
|
||
fn typeis8byteprimitive(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
return syntax.typeis8byteprim(t.type_: *syntax.tinfo);
|
||
};
|
||
|
||
// elemissignedc — given an indexable type-AST (`*T`, `[]T`, `[N]T`),
|
||
// is its element a signed narrow primitive? Used by cgindex to pick
|
||
// MOVSXD vs MOVL at esz=4 (and MOVSBQ/MOVSWQ at esz=1/2). Mirrors
|
||
// cstage's `signed_elem` (cmd/w6c/cgen.c idx_eff path). Reads through
|
||
// the stamped tinfo so alias/enum recursion lives in lib/ww/typ.ww.
|
||
fn elemissignedc(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
// #65 Phase-N step-2 cleanup: peel TY_NAMED before the .sub read.
|
||
// #64 now flows per-decl NAMED wrappers, so a NAMED-of-(`*T`/`[]T`/
|
||
// `[N]T`) reaching here would read NAMED.sub (nil) instead of the
|
||
// element. Mirrors cstage idx_eff's type_unwrap (cmd/w6c/cgen.c:790)
|
||
// before the eff->sub read (:3518-3520). Byte-id-neutral: every
|
||
// aliased indexable in-tree has a u8 element (typeissigned=false
|
||
// either way). Transitive peel matches the #63 idiom.
|
||
// idxeffti additionally drills `*[N]T` to the pointee array (#61)
|
||
// so a signed-narrow element behind a pointer-to-array still
|
||
// sign-extends on load.
|
||
let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
|
||
if (ti == nil) { return false; };
|
||
return syntax.typeissigned(ti.sub);
|
||
};
|
||
|
||
// elemisfloatc — given an indexable type-AST (`*T`, `[]T`, `[N]T`), is
|
||
// its element an f32/f64? Used by cgindex to route the element load to
|
||
// MOVSS/MOVSD into X0 instead of the integer loadopsz into AX (#119 —
|
||
// the array-element twin of the scalar-float global load at cgen.c:
|
||
// 2014). Reads through the stamped tinfo, peeling TY_NAMED before the
|
||
// .sub read exactly as elemissignedc does (#64/#65). Float-ness comes
|
||
// from the SAME tinfo the esz already reads — never a fresh node-stamp
|
||
// (the unstamped-base trap that broke the exprfloatkind collapse, #121).
|
||
fn elemisfloatc(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
// idxeffti = TY_NAMED peel + the `*[N]T` drill (#61).
|
||
let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
|
||
if (ti == nil) { return false; };
|
||
return syntax.typeisfloat(ti.sub);
|
||
};
|
||
|
||
// elemisf32c — narrower elemisfloatc: true only when the element is f32,
|
||
// so cgindex picks MOVSS over MOVSD at the #119 element load.
|
||
fn elemisf32c(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
// idxeffti = TY_NAMED peel + the `*[N]T` drill (#61).
|
||
let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
|
||
if (ti == nil) { return false; };
|
||
return syntax.typeisf32(ti.sub);
|
||
};
|
||
|
||
// elemisarrayc — given an indexable type-AST (`*T` / `[]T` / `[N]T`), is
|
||
// its element itself an array (`[N][M]T` → element `[M]T`)? cgindex then
|
||
// leaves the sub-array's ADDRESS in the result reg rather than
|
||
// dereferencing — a nested index adds its offset and only the final
|
||
// scalar element dereferences (#156, sister of #135 N_DOT-base-on-
|
||
// array-field). Node-based with the `*[N]T` drill-through, mirroring
|
||
// elemsizeof (:920-948) so elem-is-array aligns with the esz this same
|
||
// tnode feeds. cstage twin: idx_eff(bt)->sub unwrapped == TY_ARRAY
|
||
// (cmd/w6c/cgen.c). `c` kept for signature symmetry with elemisfloatc.
|
||
fn elemisarrayc(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
let k: syntax.nkind = t.kind;
|
||
let elem: *syntax.node = nil;
|
||
if (k == syntax.nkind.N_TPTR) { elem = t.lhs; };
|
||
if (k == syntax.nkind.N_TSLICE) { elem = t.lhs; };
|
||
if (k == syntax.nkind.N_TARRAY) { elem = t.lhs; };
|
||
if (elem == nil) { return false; };
|
||
if (k == syntax.nkind.N_TPTR) {
|
||
if (elem.kind == syntax.nkind.N_TARRAY) {
|
||
if (elem.lhs != nil) { elem = elem.lhs; };
|
||
};
|
||
};
|
||
return elem.kind == syntax.nkind.N_TARRAY;
|
||
};
|
||
|
||
// tichase — transitive TY_NAMED peel, nil-passthrough. Exact wwstage
|
||
// twin of cstage type_chase_named (cmd/wcc/type.c:160-162, alias arc
|
||
// #5): chain-of-aliases stacks TY_NAMED layers, so any single peel
|
||
// leaves a kind-gated consumer staring at TY_NAMED and falling to a
|
||
// scalar shape (#60's esz=1/pointer-base SEGV family). One chased
|
||
// accessor is the only spelled way to dealias; raw `.under` reads
|
||
// outside it are the lint target (rob F2 ruling).
|
||
fn tichase(t0: *syntax.tinfo) *syntax.tinfo = {
|
||
let t: *syntax.tinfo = t0;
|
||
// peel-ok: chase body
|
||
for (t != nil && t.kind == syntax.tykind.TY_NAMED) { t = t.under; };
|
||
return t;
|
||
};
|
||
|
||
// tinfoisarray — TY_ARRAY (NAMED-aware), the tinfo-keyed companion to
|
||
// elemisarrayc for cgindex's N_DOT/N_INDEX base branches, where the
|
||
// element type comes from n.type_ (stamped tinfo) not a tnode. Same
|
||
// role as typeisslice/typeisstr in lib/ww/typ.ww; kept cgen-local to
|
||
// avoid widening the frontend surface for one #156 read-half check.
|
||
fn tinfoisarray(t: *syntax.tinfo) bool = {
|
||
let u: *syntax.tinfo = t;
|
||
u = tichase(u);
|
||
if (u == nil) { return false; };
|
||
return u.kind == syntax.tykind.TY_ARRAY;
|
||
};
|
||
|
||
// fieldissignedc — does this field/element type-AST need sign-
|
||
// extension on a sub-word load? One-liner via typeissigned (cstage
|
||
// cgen.c:240 `fld_issigned` SSoT). t.type_ is stamped at check.ww
|
||
// L426-436 for every type-AST kind we see here (TNAME / TPTR /
|
||
// TBANG / TENUM / TARRAY / TSLICE — see resolvewalk).
|
||
fn fieldissignedc(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
return syntax.typeissigned(t.type_: *syntax.tinfo);
|
||
};
|
||
|
||
// fieldloadop — pick the load instruction for a non-str struct
|
||
// field by its declared size + signedness. Mirrors cstage's
|
||
// fldloadop: MOVZBQ/MOVSBQ for 1B, MOVZWQ/MOVSWQ for 2B,
|
||
// MOVL/MOVSXD for 4B, MOVQ for 8B. f might be nil for fields
|
||
// outside our struct registry.
|
||
fn fieldloadop(c: *cgen, f: *fieldinfo) str = {
|
||
if (f == nil) { return "MOVQ"; };
|
||
let sz: i32 = f.fsz;
|
||
let sigd: bool = fieldissignedc(c, f.tnode);
|
||
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
|
||
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
|
||
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
|
||
return "MOVQ";
|
||
};
|
||
|
||
// fieldstoreop — pick the store instruction for a non-str struct
|
||
// field by its declared size. MOVB for 1, MOVW for 2, MOVL for 4,
|
||
// MOVQ for 8. c kept in the signature for symmetry with fieldloadop.
|
||
fn fieldstoreop(c: *cgen, f: *fieldinfo) str = {
|
||
if (f == nil) { return "MOVQ"; };
|
||
let sz: i32 = f.fsz;
|
||
if (sz == 1) { return "MOVB"; };
|
||
if (sz == 2) { return "MOVW"; };
|
||
if (sz == 4) { return "MOVL"; };
|
||
return "MOVQ";
|
||
};
|
||
|
||
// tnodeloadop / tnodestoreop — same dispatch as fieldloadop /
|
||
// fieldstoreop but keyed on a raw type-AST node (tuple element type,
|
||
// pointer-target, slice-element, etc.) rather than a struct fieldinfo.
|
||
// Used at the index / tuple / pointer-deref sites where there's no
|
||
// fieldinfo entry but the type-node + size are both known.
|
||
fn tnodeloadop(c: *cgen, t: *syntax.node, sz: i32) str = {
|
||
let sigd: bool = fieldissignedc(c, t);
|
||
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
|
||
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
|
||
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
|
||
return "MOVQ";
|
||
};
|
||
|
||
fn tnodestoreop(c: *cgen, t: *syntax.node, sz: i32) str = {
|
||
if (sz == 1) { return "MOVB"; };
|
||
if (sz == 2) { return "MOVW"; };
|
||
if (sz == 4) { return "MOVL"; };
|
||
return "MOVQ";
|
||
};
|
||
|
||
// loadopsz — load op when the (size, signedness) pair has already
|
||
// been resolved upstream and the type-node isn't carried through.
|
||
// cgindex precomputes `signed_elem` via elemissignedc; cgforrange
|
||
// precomputes `bind_signed[b]` via paramissigned. Same dispatch as
|
||
// tnodeloadop's tail; only the keying differs.
|
||
fn loadopsz(sigd: bool, sz: i32) str = {
|
||
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
|
||
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
|
||
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
|
||
return "MOVQ";
|
||
};
|
||
|
||
// localloadop — read instruction for a scalar local/let load. Same
|
||
// dispatch as fieldloadop, but keyed on the value's own tnode.type_.
|
||
// Lets the caller emit MOVSXD/MOVSWQ/MOVSBQ on a signed-narrow slot
|
||
// instead of a raw MOVQ, so a slot that was last written by a narrow
|
||
// deref-store (`*p: *i32 = v` lowers to MOVL, only 4B) reads back as
|
||
// a properly-sign-extended i64. The natural N_ASSIGN / N_LET paths
|
||
// store the rhs as a sign-extended 8B word, so MOVQ accidentally
|
||
// works; deref-stores are the only path that touches fewer bytes
|
||
// than MOVQ reads. Mirror of cstage's localloadop in cmd/w6c/cgen.c
|
||
// — tinfo.size carries the same numeric width cstage's `t->size`
|
||
// reports, with TBANG / TENUM / TNAME-alias chains pre-folded by
|
||
// tinfofornode (check.ww:1102-1153 TNAME, 1154-1161 TBANG,
|
||
// 1196-1208 TENUM).
|
||
export fn localloadop(c: *cgen, tnode: *syntax.node) str = {
|
||
if (tnode == nil) { return "MOVQ"; };
|
||
let ti: *syntax.tinfo = tnode.type_: *syntax.tinfo;
|
||
if (ti == nil) { return "MOVQ"; };
|
||
let sz: i32 = ti.size: i32;
|
||
if (sz != 1) { if (sz != 2) { if (sz != 4) { return "MOVQ"; }; }; };
|
||
let sigd: bool = syntax.typeissigned(ti);
|
||
return loadopsz(sigd, sz);
|
||
};
|
||
|
||
// idxeffti — element-effective tinfo for indexing. `*[N]T` auto-derefs
|
||
// at an index base, so its esz/element classification must come from
|
||
// the pointee ARRAY (stride T), not from the pointer (whose .sub is
|
||
// the whole [N]T — the #61 stride bug, N*size(T) off target per index
|
||
// step). One peel choke-point: every tinfo-keyed index-element answer
|
||
// (elemsizeofc / elemissignedc / elemisfloatc / elemisf32c) routes
|
||
// through here. Mirrors cstage idx_eff (cmd/w6c/cgen.c:1163).
|
||
fn idxeffti(t0: *syntax.tinfo) *syntax.tinfo = {
|
||
let t: *syntax.tinfo = t0;
|
||
t = tichase(t);
|
||
if (t != nil && t.kind == syntax.tykind.TY_PTR) {
|
||
let p: *syntax.tinfo = t.sub;
|
||
p = tichase(p);
|
||
if (p != nil && p.kind == syntax.tykind.TY_ARRAY) { return p; };
|
||
};
|
||
return t;
|
||
};
|
||
|
||
// idxelemtn — element type-NODE for an indexable base tnode, the
|
||
// node-keyed companion of idxeffti for the cgen arms that classify
|
||
// the element structurally (istaggedtype / isstrtype / isslicetype /
|
||
// isfloattype / tnodestoreop). Same `*[N]T` drill: the pointee array's
|
||
// OWN element, never the array (#61 — an undrilled elemtn made the
|
||
// store-width chooser believe the element IS `[N]T` and emit an
|
||
// N*8-byte aggregate copy from an 8B source: caller-frame smash).
|
||
// nil for non-indexable kinds (str N_TNAME: callers want nil so
|
||
// tnodestoreop falls to the u8 byte store).
|
||
fn idxelemtn(tn: *syntax.node) *syntax.node = {
|
||
if (tn == nil) { return nil; };
|
||
let k: syntax.nkind = tn.kind;
|
||
if (k != syntax.nkind.N_TARRAY && k != syntax.nkind.N_TSLICE
|
||
&& k != syntax.nkind.N_TPTR) { return nil; };
|
||
let elem: *syntax.node = tn.lhs;
|
||
if (k == syntax.nkind.N_TPTR && elem != nil
|
||
&& elem.kind == syntax.nkind.N_TARRAY) {
|
||
return elem.lhs;
|
||
};
|
||
return elem;
|
||
};
|
||
|
||
// 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).
|
||
// For aliased element types (e.g. `[N]formattable`), callers that
|
||
// need the resolved slot size should use elemsizeofc(c, t) which
|
||
// follows aliases via slotsize.
|
||
fn elemsizeof(t: *syntax.node) i32 = {
|
||
if (t == nil) { return 1; };
|
||
let k: syntax.nkind = t.kind;
|
||
let elem: *syntax.node = nil;
|
||
if (k == syntax.nkind.N_TPTR) { elem = t.lhs; };
|
||
if (k == syntax.nkind.N_TSLICE) { elem = t.lhs; };
|
||
if (k == syntax.nkind.N_TARRAY) { elem = t.lhs; };
|
||
if (k == syntax.nkind.N_TNAME) {
|
||
let nm: str = t.str;
|
||
// str's element is u8 (F1: tystr.sub = tyu8), named directly
|
||
// rather than read off str.sub because elemsizeof gets a raw
|
||
// N_TNAME at the ident-base index path with no checker-stamped
|
||
// tinfo — str.sub lives on .type_.sub, unstamped at this site
|
||
// (cf. cgforrange's `if (sti != nil)` guard). This IS the
|
||
// str.sub-equivalent; the structural collapse onto str.sub is
|
||
// blocked on tinfo-stamping here, not intent (#24). cstage twin
|
||
// reads eff->sub->size (cmd/w6c/cgen.c N_INDEX).
|
||
if (syntax.streq(nm, "str")) { return primtypesize("u8"): i32; };
|
||
// Indexing a primitive name (rare): element size = the prim.
|
||
// primsize-ok (#101/#109): elemsizeof is the STRUCTURAL (non-
|
||
// chasing) sizer by design — its alias-resolving twin elemsizeofc
|
||
// owns the chase (routed through aliasprimsize at the :1579 leg).
|
||
// A bare primsize here is correct, not the #101 bug shape.
|
||
let ps: i32 = primsize(nm);
|
||
if (ps > 0) { return ps; };
|
||
return 1;
|
||
};
|
||
if (elem == nil) { return 1; };
|
||
// `*[N]T`: drill through the pointer into the array's element so
|
||
// indexing scales by T's width, not the whole-array byte size.
|
||
// FOOTGUN (#156): this drill ALSO fires for a bare 2D `[N][M]T`
|
||
// (elem = the inner `[M]T`), so elemsizeof of a 2D array bottoms
|
||
// out at the SCALAR T size, NOT the `[M]T` sub-array stride. 2D
|
||
// double-index (cgindex) needs the sub-array stride — call
|
||
// elemsizeofc, the 2D-correct entry, which recovers slotsize([M]T)
|
||
// when elemsizeof returns 8. Never call elemsizeof for a 2D stride.
|
||
if (elem.kind == syntax.nkind.N_TARRAY) {
|
||
if (elem.lhs != nil) { elem = elem.lhs; };
|
||
};
|
||
// `*[]T`: stride is the slice header (24B). Hare-faithful — a
|
||
// pointer-to-slice is a 1D array of slices, not of T. Mirrors the
|
||
// cstage check.c default `*U → U` path for U=[]T (slice element).
|
||
if (elem.kind == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; };
|
||
if (elem.kind == syntax.nkind.N_TNAME) {
|
||
let nm: str = elem.str;
|
||
// str element is 16B (ptr+len). primsize returns 0 for it.
|
||
if (syntax.streq(nm, "str")) { return primtypesize("str"): i32; };
|
||
// primsize-ok (#101/#109): structural sizer — the chase lives
|
||
// in elemsizeofc (:1579), not here. See the :1475 leg.
|
||
let ps: i32 = primsize(nm);
|
||
if (ps > 0) { return ps; };
|
||
};
|
||
return 8;
|
||
};
|
||
|
||
// elemsizeofc — like elemsizeof but resolves aliased element types
|
||
// (struct / tagged / `type foo = bar;`) via slotsize. Used where
|
||
// cgindex / cgassign need a correct stride for `[N]Alias` arrays
|
||
// whose Alias resolves to a tagged union (e.g. `[N]formattable`).
|
||
fn elemsizeofc(c: *cgen, t: *syntax.node) i32 = {
|
||
if (t == nil) { return 1; };
|
||
// #270-2: a NESTED-array element ([N][M]T) — the OUTER index stride
|
||
// is the WHOLE sub-array [M]T, not the scalar T that elemsizeof
|
||
// drills down to (the documented elemsizeof FOOTGUN). elemsizeof
|
||
// returns the inner prim size (4 for [M]u32), so the `direct != 8`
|
||
// short-circuit below would mis-emit esz=$4 where cstage emits the
|
||
// sub-array stride $12. Mirror cstage esz = idx_eff(bt)->sub->size
|
||
// (cmd/w6c/cgen.c:4923): the element-array tinfo's natural size
|
||
// (sub.size*elen, type.c:121) IS the outer stride.
|
||
// N_TPTR is excluded (#61): a pointee-array is NOT a nested
|
||
// element — `p[i]` on `*[N]T` auto-derefs and strides the array's
|
||
// OWN element (idx_eff peels TY_PTR→TY_ARRAY before the .sub
|
||
// read). Routing it through this whole-sub-array rule scaled
|
||
// every index by N*size(T) — the siphash round() corruption. The
|
||
// `*[N][M]T` outer stride still resolves below via idxeffti
|
||
// (pointee array's .sub = [M]T, its natural size).
|
||
let nk: syntax.nkind = t.kind;
|
||
let nest: *syntax.node = nil;
|
||
if (nk == syntax.nkind.N_TSLICE) { nest = t.lhs; };
|
||
if (nk == syntax.nkind.N_TARRAY) { nest = t.lhs; };
|
||
if (nest != nil && nest.kind == syntax.nkind.N_TARRAY) {
|
||
let eti: *syntax.tinfo = nest.type_: *syntax.tinfo;
|
||
eti = tichase(eti);
|
||
if (eti != nil) { return eti.size: i32; };
|
||
};
|
||
// #83/B3: an alias-NAMED INDEXABLE (`type grid = [3]cell`) arrives
|
||
// as a bare N_TNAME — elemsizeof's name arm knows only str + prims
|
||
// and answers the 1-sentinel, so the `direct != 8` short-circuit
|
||
// below returned 1 and every caller strode by one byte (the #60
|
||
// esz-1 family, outer-array leg). Answer from the chased stamped
|
||
// tinfo via idxeffti (which also drills an alias-of-`*[N]T`),
|
||
// element chased like the #8 leg below. Non-indexable names
|
||
// (struct/prim/str aliases) fall through to the old paths —
|
||
// byte-id preserved there.
|
||
if (nk == syntax.nkind.N_TNAME) {
|
||
let eff: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
|
||
if (eff != nil) {
|
||
if (eff.kind == syntax.tykind.TY_ARRAY
|
||
|| eff.kind == syntax.tykind.TY_SLICE) {
|
||
let aes: *syntax.tinfo = tichase(eff.sub);
|
||
if (aes != nil) { return aes.size: i32; };
|
||
};
|
||
};
|
||
};
|
||
let direct: i32 = elemsizeof(t);
|
||
if (direct != 8) { return direct; };
|
||
// #8: direct==8 is elemsizeof's "unresolved alias/aggregate" sentinel.
|
||
// Read the element width off the checker-stamped tinfo, mirroring the
|
||
// sibling elem*c helpers (elemissignedc :915, elemisfloatc :939, which
|
||
// already read t.type_.sub) and cstage idx_eff(bt)->sub->size
|
||
// (cmd/w6c/cgen.c N_INDEX). elemsizeofc was the odd-one-out among the
|
||
// elem*c family — it derived size purely structurally, so a named-narrow
|
||
// element (`[N]tkind`, tkind = enum i32) slipped through to a raw 8B slot
|
||
// instead of its i32 backing (4), wrong-striding both the cgindex READ
|
||
// and the local-array-init STORE (frame-smash). Peel TY_NAMED on the
|
||
// indexable and on its element, matching the #270-2 nested-array block
|
||
// above. Structural slotsize fallback stays for the t.type_==nil case.
|
||
// idxeffti folds that peel together with the `*[N]T` TY_PTR→
|
||
// TY_ARRAY drill (#61) so .sub is the array's element, never the
|
||
// whole pointee array.
|
||
let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
|
||
if (ti != nil) {
|
||
let esub: *syntax.tinfo = ti.sub;
|
||
esub = tichase(esub);
|
||
if (esub != nil) { return esub.size: i32; };
|
||
};
|
||
let elem: *syntax.node = idxelemtn(t);
|
||
if (elem == nil) { return direct; };
|
||
if (elem.kind == syntax.nkind.N_TNAME) {
|
||
let ps: i32 = aliasprimsize(c, elem.str);
|
||
if (ps > 0) { return ps; };
|
||
};
|
||
return slotsize(c, elem);
|
||
};
|
||
|
||
// nodeisunsigned — best-effort cgen-time inference from the AST. We
|
||
// walk surface nodes (N_DOT now reads n.type_ — #55 A.6.3g):
|
||
// nkind.N_INTLIT — never marked unsigned (no tsuffix plumbing yet)
|
||
// nkind.N_IDENT — look up the local's declared type
|
||
// nkind.N_DOT — read the checker-stamped n.type_ (#55 A.6.3g)
|
||
// 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: *syntax.node) bool = {
|
||
if (n == nil) { return false; };
|
||
let k: syntax.nkind = n.kind;
|
||
// #25 (F7-c5): collapse the whole N_IDENT arm onto the checker-stamped
|
||
// n.type_, dropping the localfindnode special-case. The prior arm read
|
||
// the local's declared tnode and returned `false` (signed) for a
|
||
// module-GLOBAL ident (localfindnode→nil) — so a `u64` global counter
|
||
// fed to / % >> or a relational got signed IDIV/SAR/JG instead of the
|
||
// unsigned DIV/SHR/JA cstage emits (type_isunsigned(n->type), cmd/w6c/
|
||
// cgen.c:2541). The N_DOT/N_CAST/N_INDEX/N_CALL arms below already read
|
||
// n.type_; this aligns the bare-ident arm to the same stamp. CLASS-M:
|
||
// the corpus HAS module-global unsigned counters on the divide/shift
|
||
// path, so the self-compile .s MOVES — every move is toward cstage
|
||
// (IDIV→DIV where the global's stamp is unsigned) and runtime-correct.
|
||
if (k == syntax.nkind.N_IDENT) {
|
||
return syntax.typeisunsigned(n.type_: *syntax.tinfo);
|
||
};
|
||
if (k == syntax.nkind.N_DOT) {
|
||
return syntax.typeisunsigned(n.type_: *syntax.tinfo);
|
||
};
|
||
if (k == syntax.nkind.N_CAST) {
|
||
if (n.rhs == nil) { return false; };
|
||
return syntax.typeisunsigned(n.rhs.type_: *syntax.tinfo);
|
||
};
|
||
if (k == syntax.nkind.N_BIN) {
|
||
if (nodeisunsigned(c, n.lhs)) { return true; };
|
||
return nodeisunsigned(c, n.rhs);
|
||
};
|
||
if (k == syntax.nkind.N_UN) { return nodeisunsigned(c, n.lhs); };
|
||
// nkind.N_INDEX: `p[i]` is unsigned iff its element type is
|
||
// unsigned. Read the checker-stamped result type directly,
|
||
// mirroring the N_DOT arm above and cstage cgen.c:2541
|
||
// (`type_isunsigned(n->lhs->type)` on operand's stamped tinfo).
|
||
// Replaces the prior structural base-walk that only fired for
|
||
// N_IDENT base — fell through to `return false` for N_DOT base
|
||
// (e.g. `d.digits[nd]` where d is a *struct), making
|
||
// `d.digits[nd] >= 5u8` pick signed JGE instead of unsigned JAE.
|
||
// Embodies the #121 principle (collapse structural onto stamp).
|
||
// #134.
|
||
if (k == syntax.nkind.N_INDEX) {
|
||
return syntax.typeisunsigned(n.type_: *syntax.tinfo);
|
||
};
|
||
// nkind.N_CALL: a call returning an unsigned type (e.g. `fn f() u64`)
|
||
// is unsigned. Read the checker-stamped result type directly — the
|
||
// N_CALL twin of the #134 N_INDEX arm above. cstage reads the same
|
||
// stamp via `type_isunsigned(n->lhs->type)`, stamped at check.c N_CALL
|
||
// `n->type = u->ret`; without this arm the wwstage fell through to
|
||
// `return false`, picking signed IDIV/SAR over unsigned DIV/SHR on a
|
||
// call-result div/mod/shift operand. #168.
|
||
if (k == syntax.nkind.N_CALL) {
|
||
return syntax.typeisunsigned(n.type_: *syntax.tinfo);
|
||
};
|
||
return false;
|
||
};
|
||
|
||
// nodeprimwidth — primitive byte width of an expression, or 0 if not
|
||
// statically determinable. Mirrors nodeisunsigned's structural walk.
|
||
// Used by cgun TK_TILDE to clamp narrow unsigned ~ results to type
|
||
// width (NOTQ inverts the full 64-bit register).
|
||
fn nodeprimwidth(c: *cgen, n: *syntax.node) i32 = {
|
||
if (n == nil) { return 0; };
|
||
let k: syntax.nkind = n.kind;
|
||
if (k == syntax.nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, n.str);
|
||
if (lc != nil) {
|
||
let tn: *syntax.node = lc.tnode;
|
||
if (tn != nil) {
|
||
if (tn.kind == syntax.nkind.N_TNAME) { return aliasprimsize(c, tn.str); };
|
||
};
|
||
};
|
||
return 0;
|
||
};
|
||
if (k == syntax.nkind.N_CAST) {
|
||
let tn: *syntax.node = n.rhs;
|
||
if (tn != nil) {
|
||
if (tn.kind == syntax.nkind.N_TNAME) { return aliasprimsize(c, tn.str); };
|
||
};
|
||
return 0;
|
||
};
|
||
if (k == syntax.nkind.N_UN) { return nodeprimwidth(c, n.lhs); };
|
||
return 0;
|
||
};
|
||
|
||
// ---- type-driven slot sizing ----------------------------------------
|
||
|
||
// structnaturalsize — type-natural size of `si`, i.e. max(foff +
|
||
// fsz) across declared fields, UNROUNDED. This is the memory-copy
|
||
// extent: cstage copies exactly these bytes for the >24B sret
|
||
// write-through (cgen.c:8150 `int sz; for fields end=foff+fsz`) and
|
||
// struct-to-struct moves, so a trailing narrow field (bool@32 in a
|
||
// 33B struct padded to 40) keeps its MOVB tail rather than widening
|
||
// to a slot-overrunning MOVQ. #33 fixed cstage to use this; ww
|
||
// mirrors it. The ≤24B register RECV/RETURN ABI wants a DIFFERENT
|
||
// number — see structabisize.
|
||
//
|
||
// NOTE: si.totsize is yet a THIRD metric — the slot-padded size
|
||
// (rounded up to 8 for stack-slot use; see registerstruct's tail
|
||
// `if ((off & 7) != 0) ...`). Frame allocation and [N]foo stride
|
||
// want that slot number.
|
||
fn structnaturalsize(si: *structinfo) i32 = {
|
||
if (si == nil) { return 0; };
|
||
let n: i32 = 0;
|
||
let fi: *fieldinfo = si.fields;
|
||
for (fi != nil) {
|
||
let end: i32 = fi.foff + fi.fsz;
|
||
if (end > n) { n = end; };
|
||
fi = fi.finext;
|
||
};
|
||
return n;
|
||
};
|
||
|
||
// structabisize — the ≤24B register-return ABI size of `si`: the
|
||
// natural extent rounded up to the struct's maxalign. SSoT-equal to
|
||
// cstage's `lu->size` (check.c:760 `(off+maxalign-1)&~(maxalign-1)`).
|
||
// Distinct from structnaturalsize because the register RECV/RETURN
|
||
// ABI packs the value into AX/DX/CX at 8-byte granularity: cstage
|
||
// writes/reads the tail at maxalign width (cgen.c:7720 `sz=lu->size`,
|
||
// :8230 `sz=rt->size`), so a maxalign==8 struct with a sub-8 tail
|
||
// (struct{i64,i32}, natural 12) round-trips as MOVQ+MOVQ (16), not
|
||
// MOVQ+MOVL (12). Used ONLY at those register-ABI sites; memory
|
||
// copies (sret >24B, struct ident-copy) and field-offset math stay
|
||
// on structnaturalsize. #169.
|
||
//
|
||
// maxalign comes from each field's TRUE alignment (tinfo.align), not
|
||
// the slot-padded fsz: a [N]u8 / sub-struct field has slot ≥8 but
|
||
// align 1, so an fsz ladder would over-round. Mirrors cstage's
|
||
// maxalign = max(ft->align) (check.c:708).
|
||
fn structabisize(si: *structinfo) i32 = {
|
||
if (si == nil) { return 0; };
|
||
let n: i32 = 0;
|
||
let maxaln: i32 = 1;
|
||
let fi: *fieldinfo = si.fields;
|
||
for (fi != nil) {
|
||
let end: i32 = fi.foff + fi.fsz;
|
||
if (end > n) { n = end; };
|
||
if (fi.tnode != nil) {
|
||
let ti: *syntax.tinfo = fi.tnode.type_: *syntax.tinfo;
|
||
ti = tichase(ti);
|
||
if (ti != nil) {
|
||
let aln: i32 = ti.align: i32;
|
||
if (aln > maxaln) { maxaln = aln; };
|
||
};
|
||
};
|
||
fi = fi.finext;
|
||
};
|
||
return (n + maxaln - 1) & ~(maxaln - 1);
|
||
};
|
||
|
||
// sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B,
|
||
// return its natural size; else 0. Tagged unions, tuples, str,
|
||
// slices, scalars route through their existing register-return ABIs
|
||
// (AX/DX/CX/[R8]) regardless of size. Task #23 mirrors cstage's
|
||
// cg_sret_retsize predicate. Resolves N_TNAME → struct via structlookup
|
||
// and unwraps one leading N_TBANG so `type box = !big;` still
|
||
// triggers sret on the underlying big.
|
||
//
|
||
// Chain-of-aliases (#22): `type a = struct{...}; type b = a;` registers
|
||
// `b → a` in c.aliases (target node = N_TNAME "a"), not `b → struct`.
|
||
// When structlookup(c, "b") misses, fall through to aliaslookup and
|
||
// recurse on the alias target — mirrors slotsize's N_TNAME arm
|
||
// (cgenutil.ww:1955) and the cstage while-loop in cg_sret_retsize.
|
||
|
||
// structlookupchain — resolve TNAME `tn` to its registered struct,
|
||
// chasing alias-of-alias (#22). Returns nil if the chain doesn't
|
||
// bottom out at a struct. Mirrors cstage's transitive
|
||
// `while (t->kind == TY_NAMED) t = t->under` peel; consumed by
|
||
// cgdot / cgassign at every "field-walk on a struct-typed local"
|
||
// site so a transitively-aliased struct name resolves to its
|
||
// fieldinfo list regardless of chain depth.
|
||
export fn structlookupchain(c: *cgen, tn: *syntax.node) *structinfo = {
|
||
if (tn == nil) { return nil; };
|
||
// #92: a struct LITERAL's type ref parses as N_IDENT (expression
|
||
// position, lib/ww/parse/expr.ww) where type specs parse N_TNAME
|
||
// — both carry the name in .str. Accept both at the ENTRY only:
|
||
// iterations past the first walk aliaslookup results, which are
|
||
// N_TNAME by the loop's own reassignment gate. Consumer census
|
||
// (commit body): no existing caller can pass N_IDENT.
|
||
if (tn.kind != syntax.nkind.N_TNAME && tn.kind != syntax.nkind.N_IDENT) { return nil; };
|
||
let si: *structinfo = structlookup(c, tn.str);
|
||
if (si != nil) { return si; };
|
||
let cur: *syntax.node = tn;
|
||
for (cur != nil
|
||
&& (cur.kind == syntax.nkind.N_TNAME || cur.kind == syntax.nkind.N_IDENT)
|
||
&& si == nil) {
|
||
let aliased: *syntax.node = aliaslookup(c, cur.str);
|
||
if (aliased == nil) { cur = nil; }
|
||
else {
|
||
if (aliased.kind == syntax.nkind.N_TNAME) {
|
||
si = structlookup(c, aliased.str);
|
||
cur = aliased;
|
||
} else { cur = nil; };
|
||
};
|
||
};
|
||
return si;
|
||
};
|
||
|
||
export fn sretretsize(c: *cgen, t: *syntax.node) i32 = {
|
||
if (t == nil) { return 0; };
|
||
let r: *syntax.node = t;
|
||
if (r.kind == syntax.nkind.N_TBANG) {
|
||
r = r.lhs;
|
||
if (r == nil) { return 0; };
|
||
};
|
||
// #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP
|
||
// eightbytes; a wider slot was silently truncated (payload word
|
||
// 4+ died in the callee frame). The ≤cap boundary is load-bearing:
|
||
// (str|nomem)-shaped 32B slots MUST stay register-ABI or every
|
||
// such consumer in the tree flips. Nullable folds to one word.
|
||
// Mirrors cstage cg_sret_retsize TY_TAGGED arm.
|
||
if (istaggedtype(c, r)) {
|
||
if (isnullabletype(r)) { return 0; };
|
||
let tsz38: i32 = slotsize(c, r);
|
||
if (tsz38 <= TUPLE_GPCAP * 8) { return 0; };
|
||
return tsz38;
|
||
};
|
||
if (r.kind == syntax.nkind.N_TTUPLE) {
|
||
// #10: over-cap tuple → sret. Walk the element TYPE nodes
|
||
// (pt.lhs) over the SAME caps the SEND/receive use; a float =
|
||
// 1 SSE eightbyte, a slice/str its 3-word header, a scalar 1
|
||
// GP word. Return the tuple's natural total size (tinfo.size,
|
||
// the type table) so the callee returns via sret. Mirrors
|
||
// cstage cg_sret_retsize TY_TUPLE arm; TUPLE_GPCAP/TUPLE_SSECAP
|
||
// are the shared cap SSoT with the cgreturn SEND emitter.
|
||
let ssecap: i32 = TUPLE_SSECAP;
|
||
let gptotal: i32 = 0;
|
||
let ssecount: i32 = 0;
|
||
let pt: *syntax.node = r.list;
|
||
for (pt != nil) {
|
||
let et: *syntax.node = pt.lhs;
|
||
if (isfloattype(c, et)) {
|
||
ssecount = ssecount + 1;
|
||
} else {
|
||
gptotal = gptotal + tupeslotn(et) / 8;
|
||
};
|
||
pt = pt.next;
|
||
};
|
||
if (gptotal > TUPLE_GPCAP || ssecount > ssecap) {
|
||
let rti: *syntax.tinfo = r.type_: *syntax.tinfo;
|
||
if (rti != nil) { return rti.size: i32; };
|
||
};
|
||
return 0;
|
||
};
|
||
// #267: arrays ride the struct-return ABI — natural size
|
||
// (sub.size*len, the type table) gates ≤24 reg / >24 sret, mirroring
|
||
// cstage cg_sret_retsize TY_ARRAY arm. Pure-int element arrays only;
|
||
// no float-array-return consumer (structfloatclass stays struct-only).
|
||
if (r.kind == syntax.nkind.N_TARRAY) {
|
||
let ati: *syntax.tinfo = r.type_: *syntax.tinfo;
|
||
ati = tichase(ati);
|
||
if (ati == nil) { return 0; };
|
||
let asz: i32 = ati.size: i32;
|
||
if (asz <= 24) { return 0; };
|
||
return asz;
|
||
};
|
||
if (r.kind != syntax.nkind.N_TNAME) { return 0; };
|
||
// Primitives / aliased-to-primitives are never sret.
|
||
if (aliasprimsize(c, r.str) > 0) { return 0; };
|
||
if (syntax.streq(r.str, "str")) { return 0; };
|
||
// #129: same-module alias wins over any-module struct hit. Without
|
||
// this, `type stream = *vtable` (io) loses to memio.stream (56B
|
||
// struct) via structlookup's any-module fallback → spurious sret.
|
||
// Mirrors cstage cg_sret_retsize, which sees TY_PTR, not a name.
|
||
if (c != nil) {
|
||
let al: *syntax.node = aliassamemod(c, r.str);
|
||
if (al != nil) {
|
||
return sretretsize(c, al);
|
||
};
|
||
};
|
||
let si: *structinfo = structlookup(c, r.str);
|
||
if (si == nil) {
|
||
if (c != nil) {
|
||
let aliased: *syntax.node = aliaslookup(c, r.str);
|
||
if (aliased != nil) {
|
||
return sretretsize(c, aliased);
|
||
};
|
||
};
|
||
return 0;
|
||
};
|
||
let n: i32 = structnaturalsize(si);
|
||
if (n <= 24) { return 0; };
|
||
return n;
|
||
};
|
||
|
||
// callsretsize — if N_CALL `n`'s callee returns a plain TY_STRUCT
|
||
// > 24B, return its natural size; else 0. Wraps sretretsize over the
|
||
// callee's resolved return type, used by cglet / cgassign receive
|
||
// sites and cgcall to detect sret at the receive / emit boundaries.
|
||
export fn callsretsize(c: *cgen, n: *syntax.node) i32 = {
|
||
if (n == nil) { return 0; };
|
||
if (n.kind != syntax.nkind.N_CALL) { return 0; };
|
||
let callee: *syntax.node = n.lhs;
|
||
if (callee == nil) { return 0; };
|
||
let cn: str;
|
||
cn.ptr = nil; cn.len = 0;
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.kind == syntax.nkind.N_IDENT) {
|
||
cn = callee.str;
|
||
cmod = c.curmod;
|
||
};
|
||
if (callee.kind == syntax.nkind.N_DOT) {
|
||
cn = callee.str;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
};
|
||
if (cn.len == 0) { return 0; };
|
||
let rtyp: *syntax.node = fnretlookupmod(c, cn, cmod);
|
||
// #129: sretretsize must see the CALLEE's module context so
|
||
// aliassamemod resolves aliases from the callee's module (not the
|
||
// caller's). Mirrors cstage operating on resolved Type* objects
|
||
// (type_chase_named never has this confusion). Swap + restore.
|
||
let savedmod: str = c.curmod;
|
||
if (cmod.len > 0) { c.curmod = cmod; };
|
||
let r: i32 = sretretsize(c, rtyp);
|
||
c.curmod = savedmod;
|
||
return r;
|
||
};
|
||
|
||
fn structlookup(c: *cgen, name: str) *structinfo = {
|
||
// Same-module first, then any. Trio-leaf graduation mirroring
|
||
// aliaslookup (#27), fnret/fnparamslookupmod (#28/#31), and
|
||
// enumlookup (#4a): without the prefer pass a bare-leaf struct
|
||
// name in module M can collapse onto another module's same-leaf
|
||
// struct prepended earlier in c.structs, silently picking the
|
||
// wrong totsize / field offsets.
|
||
let s: *structinfo = c.structs;
|
||
for (s != nil) {
|
||
if (syntax.streq(s.sname, name)) {
|
||
if (syntax.streq(s.smod, c.curmod)) { return s; };
|
||
};
|
||
s = s.sinext;
|
||
};
|
||
s = c.structs;
|
||
for (s != nil) {
|
||
let sn: str = s.sname;
|
||
if (syntax.streq(sn, name)) { return s; };
|
||
s = s.sinext;
|
||
};
|
||
// Module-qualified form embedded in name (`pkg.S`): scope the
|
||
// leaf to its originating module. The `smod == pkg` guard
|
||
// prevents same-leaf structs in two modules from collapsing.
|
||
let i: i32 = name.len - 1;
|
||
for (i >= 0) {
|
||
if (name[i] == '.') {
|
||
let pkg: str;
|
||
pkg.ptr = name.ptr;
|
||
pkg.len = i;
|
||
let leaf: str;
|
||
leaf.ptr = name.ptr + ((i + 1): u64);
|
||
leaf.len = name.len - (i + 1);
|
||
// M1 #22: the embedded qualifier (`utf8`) is the use ALIAS;
|
||
// the struct's smod is now the dotted import PATH
|
||
// (`encoding.utf8`). Map alias→path before comparing.
|
||
let pkgmod: str = usehint(c, pkg);
|
||
let b: *structinfo = c.structs;
|
||
for (b != nil) {
|
||
if (syntax.streq(b.sname, leaf)) {
|
||
if (syntax.streq(b.smod, pkgmod)) {
|
||
return b;
|
||
};
|
||
};
|
||
b = b.sinext;
|
||
};
|
||
return nil;
|
||
};
|
||
i -= 1;
|
||
};
|
||
return nil;
|
||
};
|
||
|
||
// #223: same-module-ONLY struct lookup. structlookup's any-module
|
||
// fallback returns a foreign same-leaf struct; the cgdot alias-peel
|
||
// needs to break ONLY on a struct that THIS module defines (a genuine
|
||
// struct-value receiver), not on a foreign struct that merely shares a
|
||
// leaf with a same-module alias (io.stream alias vs memio.stream
|
||
// struct). Returns the struct only when it lives in c.curmod.
|
||
fn structsamemod(c: *cgen, name: str) *structinfo = {
|
||
let s: *structinfo = c.structs;
|
||
for (s != nil) {
|
||
if (syntax.streq(s.sname, name)) {
|
||
if (syntax.streq(s.smod, c.curmod)) { 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;
|
||
};
|
||
|
||
// primsize-ok (#101/#109): the primitive-width oracle itself — this
|
||
// IS the SSoT table aliasprimsize wraps; there is nothing below it to
|
||
// chase.
|
||
fn primsize(name: str) i32 = {
|
||
if (syntax.streq(name, "u8")) { return 1; };
|
||
if (syntax.streq(name, "i8")) { return 1; };
|
||
if (syntax.streq(name, "bool")) { return 1; };
|
||
if (syntax.streq(name, "u16")) { return 2; };
|
||
if (syntax.streq(name, "i16")) { return 2; };
|
||
if (syntax.streq(name, "u32")) { return 4; };
|
||
if (syntax.streq(name, "i32")) { return 4; };
|
||
if (syntax.streq(name, "f32")) { return 4; };
|
||
if (syntax.streq(name, "u64")) { return 8; };
|
||
if (syntax.streq(name, "i64")) { return 8; };
|
||
if (syntax.streq(name, "uint")) { return 8; };
|
||
if (syntax.streq(name, "int")) { return 8; };
|
||
if (syntax.streq(name, "uintptr")) { return 8; };
|
||
if (syntax.streq(name, "size")) { return 8; };
|
||
if (syntax.streq(name, "f64")) { return 8; };
|
||
if (syntax.streq(name, "rune")) { return 4; };
|
||
if (syntax.streq(name, "void")) { return 0; };
|
||
return 0;
|
||
};
|
||
|
||
// aliasprimsize — resolved primitive byte width for a type NAME: the
|
||
// prim width if `nm` is itself a primitive, else chase the alias chain
|
||
// (aliaslookup) to its bottom and take that prim's width. Returns 0
|
||
// when the name doesn't reduce to a width-known primitive (struct /
|
||
// tagged / `!`/enum-bottom / unresolved). SSoT for the size-use
|
||
// primsize() family: a bare primsize(name) is alias-blind — a narrow
|
||
// alias (`type my32 = u32`) returns 0, defaulting the stride/width to
|
||
// 8 (the #101 struct-fill miscompile: [3]my32 strode 8 not 4, field n
|
||
// collided with arr[2]). cstage chases my32→u32→4 via type_chase_named
|
||
// at the twin sites; this is the ww align-up. The bare-primsize GUARD
|
||
// family (is-primitive dispatch) is the #109 follow-on, NOT routed
|
||
// here. #101.
|
||
// primsize-ok (#101/#109): the SSoT chase body itself — primsize is
|
||
// the leaf-primitive probe this helper wraps, then aliaslookup chases.
|
||
fn aliasprimsize(c: *cgen, nm: str) i32 = {
|
||
let ps: i32 = primsize(nm);
|
||
if (ps > 0) { return ps; };
|
||
let cur: *syntax.node = aliaslookup(c, nm);
|
||
for (cur != nil) {
|
||
if (cur.kind != syntax.nkind.N_TNAME) { return 0; };
|
||
let p: i32 = primsize(cur.str);
|
||
if (p > 0) { return p; };
|
||
cur = aliaslookup(c, cur.str);
|
||
};
|
||
return 0;
|
||
};
|
||
|
||
// typenodeprimresolved — walk N_TBANG / N_TENUM / N_TNAME alias
|
||
// chains to the underlying primitive, returning its byte size and
|
||
// signedness. Sets *sz_out = 0 when the type doesn't reduce to a
|
||
// width-known primitive (composite, unresolved name, default-storage
|
||
// enum, etc.). Mirrors cstage's `type_isint(t) ? t->size : 0` /
|
||
// `type_isunsigned` recursion through TY_NAMED and TY_ENUM. Used by
|
||
// cgcast's identity-width identity-sign clamp-skip predicate (#33).
|
||
export fn typenodeprimresolved(c: *cgen, t: *syntax.node,
|
||
sz_out: *i32, unsigned_out: *bool) void = {
|
||
*sz_out = 0;
|
||
*unsigned_out = false;
|
||
let cur: *syntax.node = t;
|
||
for (cur != nil) {
|
||
let k: syntax.nkind = cur.kind;
|
||
if (k == syntax.nkind.N_TBANG) { cur = cur.lhs; }
|
||
else { if (k == syntax.nkind.N_TENUM) { cur = cur.lhs; }
|
||
else { if (k == syntax.nkind.N_TNAME) {
|
||
let nm: str = cur.str;
|
||
// bool is excluded from the int-prim contract: cstage's
|
||
// `type_isint(TY_BOOL)` is false, so its identity check
|
||
// leaves src_w=0 on a bool source. Match that here so a
|
||
// `let y: i8 = b: i8;` (bool b) doesn't fire identity in
|
||
// wwstage and skip the MOVSBQ that cstage emits. Other
|
||
// call sites (slot sizing, etc.) still want
|
||
// primsize("bool")=1, so the exclusion stays local. The
|
||
// dedicated `is_bool` path in cgcast owns bool→bool's
|
||
// ANDQ $255 on both stages.
|
||
if (syntax.streq(nm, "bool")) { return; };
|
||
// primsize-ok (#101/#109): this fn IS a prim-resolver
|
||
// chaser (#33) — primsize is the leaf-primitive probe;
|
||
// aliaslookup below advances the walk on a miss.
|
||
let ps: i32 = primsize(nm);
|
||
if (ps > 0) {
|
||
*sz_out = ps;
|
||
*unsigned_out = syntax.typeisunsigned(cur.type_: *syntax.tinfo);
|
||
return;
|
||
};
|
||
let al: *syntax.node = aliaslookup(c, nm);
|
||
if (al == nil) { return; };
|
||
cur = al;
|
||
}
|
||
else { return; }; }; };
|
||
};
|
||
};
|
||
|
||
// exprprimresolved — best-effort static (primsize, signedness) for an
|
||
// expression. Used by cgcast (#33) to derive the source-side primitive
|
||
// width and signedness so the identity-width identity-sign clamp-skip
|
||
// predicate fires. Sets *sz_out = 0 when the type can't be derived
|
||
// (untyped literal, call result with no return-type lookup, etc.);
|
||
// caller treats sz=0 as "not identity", which conservatively keeps
|
||
// the clamp. Mirror of cstage's `n->lhs->type` lookup with the same
|
||
// TY_NAMED / TY_ENUM recursion through type_isint / type_isunsigned.
|
||
export fn exprprimresolved(c: *cgen, n: *syntax.node,
|
||
sz_out: *i32, unsigned_out: *bool) void = {
|
||
*sz_out = 0;
|
||
*unsigned_out = false;
|
||
if (n == nil) { return; };
|
||
let k: syntax.nkind = n.kind;
|
||
if (k == syntax.nkind.N_INTLIT) {
|
||
// Typed-int literal: `7u32` has tsuffix = "u32". Mirrors
|
||
// cstage's `cexpr` which assigns `lookup_builtin(tsuffix)`
|
||
// as the node's type — without this, wwstage misses the
|
||
// suffix and emits a defensive clamp where cstage skips,
|
||
// breaking byte-id on rows like `let y: mymode = 7u32:
|
||
// mymode;` (mymode = enum u32).
|
||
let s: str = n.tsuffix;
|
||
if (s.len > 0) {
|
||
// primsize-ok (#101/#109): a typed-int literal suffix
|
||
// (`7u32`) is a builtin primitive name by grammar — no
|
||
// alias can reach here, so there is nothing to chase.
|
||
let ps: i32 = primsize(s);
|
||
if (ps > 0) {
|
||
*sz_out = ps;
|
||
*unsigned_out = syntax.typeisunsigned(n.type_: *syntax.tinfo);
|
||
};
|
||
};
|
||
return;
|
||
};
|
||
if (k == syntax.nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, n.str);
|
||
if (lc != nil) {
|
||
typenodeprimresolved(c, lc.tnode,
|
||
sz_out, unsigned_out);
|
||
};
|
||
return;
|
||
};
|
||
if (k == syntax.nkind.N_CAST) {
|
||
typenodeprimresolved(c, n.rhs, sz_out, unsigned_out);
|
||
return;
|
||
};
|
||
if (k == syntax.nkind.N_UN) {
|
||
exprprimresolved(c, n.lhs, sz_out, unsigned_out);
|
||
return;
|
||
};
|
||
if (k == syntax.nkind.N_DOT) {
|
||
// #59: read the checker-stamped tinfo instead of re-deriving the
|
||
// field type via dotfieldtnode's structinfo walk. Mirrors cstage
|
||
// castsrcprim N_DOT (cmd/w6c/cgen.c:323-344): the base must
|
||
// resolve to a struct (or ptr-to-struct) before the field type
|
||
// counts. That guard excludes pseudo-fields .len/.cap/.ptr (the
|
||
// checker stamps them i32/*T at check.ww:1987-2004) and tuple
|
||
// positionals, keeping them at sz=0 — asymmetry there breaks 995
|
||
// byte-id (cgen.c:286-290). The field's own width/sign is the
|
||
// N_DOT's stamped type_ (check.ww:2012). typeisint ? size : 0;
|
||
// bool falls out because typeisint(bool) is false — the same
|
||
// exclusion the old streq("bool") arm encoded.
|
||
// B2-c3/B1: the base walk CHASES each hop (was a hand-rolled
|
||
// 2-peel that ran out at a 3-level alias base or a ptr-to-
|
||
// 2-level base — clamp kept on a knowable identity cast,
|
||
// runtime-correct but cs!=ww asm). Aligns up to cstage
|
||
// castsrcprim post-F1 (type_chase_named at both hops). The
|
||
// field-u chase is asm-neutral: cs keeps its single peel
|
||
// there, sound through type_isint's NAMED recursion + the
|
||
// NAMED tinfo carrying its underlying's size (probe
|
||
// b1c_fld2lvl byte-id).
|
||
let bu: *syntax.tinfo = nil;
|
||
if (n.lhs != nil) { bu = n.lhs.type_: *syntax.tinfo; };
|
||
bu = tichase(bu);
|
||
if (bu != nil && bu.kind == syntax.tykind.TY_PTR) { bu = tichase(bu.sub); };
|
||
if (bu != nil && bu.kind == syntax.tykind.TY_STRUCT) {
|
||
let u: *syntax.tinfo = n.type_: *syntax.tinfo;
|
||
u = tichase(u);
|
||
if (syntax.typeisint(u)) {
|
||
*sz_out = u.size: i32;
|
||
*unsigned_out = syntax.typeisunsigned(u);
|
||
};
|
||
};
|
||
return;
|
||
};
|
||
};
|
||
|
||
// 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 (syntax.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 (syntax.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 (syntax.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: *syntax.node) *syntax.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: *syntax.node = rhs;
|
||
if (rhs.kind == syntax.nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; };
|
||
if (rhs.kind == syntax.nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; };
|
||
if (call == nil) { return nil; };
|
||
if (call.kind != syntax.nkind.N_CALL) { return nil; };
|
||
let callee: *syntax.node = call.lhs;
|
||
if (callee == nil) { return nil; };
|
||
let cname: str;
|
||
cname.ptr = nil; cname.len = 0;
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.kind == syntax.nkind.N_IDENT) {
|
||
cname = callee.str;
|
||
cmod = c.curmod;
|
||
};
|
||
if (callee.kind == syntax.nkind.N_DOT) {
|
||
cname = callee.str;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
};
|
||
if (cname.len == 0) { return nil; };
|
||
let rtyp: *syntax.node = fnretlookupmod(c, cname, cmod);
|
||
if (rtyp == nil) { return nil; };
|
||
if (unwrap) {
|
||
// Strip error variants — success type is the first
|
||
// variant of the tagged return.
|
||
if (rtyp.kind != syntax.nkind.N_TTAGGED) { return nil; };
|
||
return rtyp.list;
|
||
};
|
||
// Plain call: declared return type is the local's type.
|
||
return rtyp;
|
||
};
|
||
|
||
// 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. Called from cglet at emit time so the frame
|
||
// grows monotonically per first-use (#15).
|
||
//
|
||
// `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: *syntax.node) i32 = {
|
||
// `[_]T = arrlit;` inferred-length arrays no longer need a slot-size
|
||
// intercept here: the checker (inferarraylen, check.ww) stamps the
|
||
// real element count onto the array type's length child before cgen
|
||
// runs, so slotsize reads it like any explicit `[N]T` (#7).
|
||
// 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 tn: *syntax.node = n.lhs;
|
||
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
|
||
if (tn == nil) { return 8; };
|
||
let s: i32 = slotsize(c, tn);
|
||
// #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot
|
||
// to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real
|
||
// type size. slotsize returns 0 for a void/`done`-aliased scalar
|
||
// local; localreserve no longer applies a sub-8 floor (it mirrors
|
||
// cstage's no-floor localslot), so a void slot must be floored here
|
||
// instead, else its zero width collides with the next local. A
|
||
// genuine empty struct or [0]T array (also slotsize 0) keeps its 0.
|
||
if (s == 0) {
|
||
let ti: *syntax.tinfo = tn.type_: *syntax.tinfo;
|
||
if (ti != nil) { ti = tichase(ti); };
|
||
if (ti != nil && ti.kind == syntax.tykind.TY_VOID) { return 8; };
|
||
};
|
||
return s;
|
||
};
|
||
|
||
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
|
||
// `n.type_` on every N_T* kind via tinfofornode, which folds TBANG
|
||
// (inner unchanged, check.ww:1154-1161), TNAME alias chains
|
||
// (resolvealias, check.ww:1128-1153), TARRAY/TPTR/TSLICE/TCHAN/TFN/
|
||
// TENUM/TTUPLE/TSTRUCT/TTAGGED with size + slot-padded slotsize.
|
||
//
|
||
// cstage SSoT is distributed — there is no single slot_size(Type*).
|
||
// Tagged slot follows cstage cmd/wcc/check.c:348 + :998 (tag (8) +
|
||
// max variant payload rounded to 8); the wwstage TTAGGED arm at
|
||
// check.ww:1296-1346 mirrors that layout. cgen.c:4850 / :5193 use
|
||
// the same `(su->kind == TY_TAGGED) ? su->size : 16` pattern for the
|
||
// match-spill slot. Pointer-and-narrow → 8 is the local-frame
|
||
// convention encoded at every cstage `localoff(…, 8, …)` callsite
|
||
// (cmd/w6c/cgen.c throughout); wwstage encodes the same pad-to-8 at
|
||
// this read site so `[N]i32` stride stays 4 (natural) — moving it
|
||
// into ti.slotsize would lift array stride to 8/elem.
|
||
//
|
||
// `c: *cgen` retained unused for callsite stability (localloadop
|
||
// precedent, 68219a1).
|
||
fn slotsize(c: *cgen, typn: *syntax.node) i32 = {
|
||
if (typn == nil) { return 8; };
|
||
let ti: *syntax.tinfo = typn.type_: *syntax.tinfo;
|
||
if (ti == nil) { return 8; };
|
||
// #63 Phase-N step 1: peel TY_NAMED before this structural query.
|
||
// #64 builds per-decl NAMED wrappers (tinfofornode), so the peel
|
||
// now fires on aliased operands; byte-id holds because it collapses
|
||
// NAMED to the alias-invariant underlying this read consumes.
|
||
ti = tichase(ti);
|
||
if (ti == nil) { return 8; };
|
||
let kk: syntax.tykind = ti.kind;
|
||
if (kk == syntax.tykind.TY_VOID) { return 0; };
|
||
if (kk == syntax.tykind.TY_PTR || kk == syntax.tykind.TY_SLICE ||
|
||
kk == syntax.tykind.TY_CHAN || kk == syntax.tykind.TY_FN ||
|
||
kk == syntax.tykind.TY_STR || kk == syntax.tykind.TY_TAGGED) {
|
||
return ti.size: i32;
|
||
};
|
||
// #75: a struct LOCAL's stack slot is the struct's NATURAL size
|
||
// rounded up to the 8B slot grain — NOT ti.slotsize, which sums the
|
||
// slot-padded field widths and over-reserves on sub-8 nested
|
||
// composites (e.g. outer{a:u8, p:inner{x:u8,y:u8}, z:i64} → ww $32 vs
|
||
// cstage $16). cstage reserves the local at f->type->size (cmd/w6c/
|
||
// cgen.c), i.e. the checker's natural r.size (check.ww:2256). Same
|
||
// dual-SSoT leak as #44 (field-offset) / #55, one notion over. The
|
||
// TUPLE arm (8B/elem slot, user ruling #60) and ARRAY arm (element
|
||
// stride, #48 [N]Alias 24B) keep ti.slotsize — deliberate divergences.
|
||
if (kk == syntax.tykind.TY_STRUCT) {
|
||
return ((ti.size + 7u64) & ~7u64): i32;
|
||
};
|
||
if (kk == syntax.tykind.TY_TUPLE || kk == syntax.tykind.TY_ARRAY) {
|
||
return ti.slotsize: i32;
|
||
};
|
||
return 8;
|
||
};
|
||
|
||
// fieldsize — slot-padded byte width of a struct field's type-AST,
|
||
// consumed by registerstruct's alignment + offset math (≥8→8 /
|
||
// ≥4→4 / ≥2→2 ladder at L1875-1877) and by the `*p OP=` deref-
|
||
// compound at cgenexpr.ww:3594. Mirror of check.ww:1053
|
||
// `fieldslotsize` (the same dispatch on the populated tinfo);
|
||
// slotsize-template precedent at a828c03. cstage SSoT is
|
||
// `f->type->size` (cmd/w6c/cgen.c:1386, :1656, :2515, :2535);
|
||
// wwstage routes through ti.slotsize for composites since the
|
||
// stack-slot pad rules live on tinfo (#48 verdict), and through
|
||
// ti.size for the kinds whose natural size already equals their
|
||
// in-struct width.
|
||
//
|
||
// `c: *cgen` retained unused for callsite stability (slotsize /
|
||
// localloadop precedent, a828c03 / 68219a1).
|
||
fn fieldsize(c: *cgen, tnode: *syntax.node) i32 = {
|
||
if (tnode == nil) { return 8; };
|
||
let ti: *syntax.tinfo = tnode.type_: *syntax.tinfo;
|
||
if (ti == nil) { return 8; };
|
||
// #63 Phase-N step 1: peel TY_NAMED before this structural query.
|
||
// #64 builds per-decl NAMED wrappers (tinfofornode), so the peel
|
||
// now fires on aliased operands; byte-id holds because it collapses
|
||
// NAMED to the alias-invariant underlying this read consumes.
|
||
ti = tichase(ti);
|
||
if (ti == nil) { return 8; };
|
||
let k: syntax.tykind = ti.kind;
|
||
if (k == syntax.tykind.TY_STRUCT) { return ti.slotsize: i32; };
|
||
if (k == syntax.tykind.TY_ARRAY) { return ti.slotsize: i32; };
|
||
if (k == syntax.tykind.TY_TAGGED) { return ti.size: i32; };
|
||
if (k == syntax.tykind.TY_SLICE) { return ti.size: i32; };
|
||
if (k == syntax.tykind.TY_PTR || k == syntax.tykind.TY_FN ||
|
||
k == syntax.tykind.TY_CHAN) { return 8; };
|
||
if (k == syntax.tykind.TY_STR) { return ti.size: i32; };
|
||
// Primitives + TY_ENUM keep natural width inside structs
|
||
// (cstage parity: cgen.c reads f->type->size directly). TY_TUPLE
|
||
// flows here too — pre-collapse fallback was 8, the populated
|
||
// ti.size carries the natural sum; ken-thompson 2026-05-23 review:
|
||
// keep the corrected behavior, no fixtures in selfhost exercise
|
||
// a tuple-typed struct field today (995 byte-id is the gate).
|
||
if (ti.size > 0u64) { return ti.size: i32; };
|
||
return 8;
|
||
};
|
||
|
||
// #44/#55: fi.foff is a VIEW of the checker's already-built NATURAL
|
||
// `tfield.offset` (check.ww N_TSTRUCT L2234), NOT a second slot-padded
|
||
// layout recomputed via fieldsize. The two sources diverged iff a struct
|
||
// had a nested sub-8 composite field (slotsize != size) plus a successor:
|
||
// the WRITE path used this slot-padded foff, the READ path
|
||
// (cgplaceaddr/dotbaseaddr) read tfield.offset natural — ww mis-addressed
|
||
// its own fields. cstage has no structinfo and reads tfield directly
|
||
// (self-consistently natural); this unifies ww's second source onto it,
|
||
// preserving cs==ww. Lock-step walk: tstruct.list N_TFIELD AST nodes and
|
||
// ti.fields tfields share one head-first declared order (both skip
|
||
// non-TFIELD identically), so they advance in exact step. si.totsize keeps
|
||
// the slot-padded total (stack-slot allocator's number) from ti.slotsize,
|
||
// already 8-rounded at check.ww:2259-2261.
|
||
fn registerstruct(c: *cgen, name: str, srcmod: str, tstruct: *syntax.node) void = {
|
||
let si: *structinfo = alloc(structinfo{
|
||
sname = name,
|
||
smod = srcmod,
|
||
})!;
|
||
if (tstruct.type_ == nil) {
|
||
let msg: str = "#44/#55: registerstruct: struct node has no stamped tinfo\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let ti: *syntax.tinfo = tichase(tstruct.type_: *syntax.tinfo);
|
||
if (ti == nil) {
|
||
let msg: str = "#44/#55: registerstruct: tichase yielded nil tinfo\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let head: *fieldinfo = nil;
|
||
let tail: *fieldinfo = nil;
|
||
let f: *syntax.node = tstruct.list;
|
||
let tf: *syntax.tfield = ti.fields;
|
||
for (f != nil) {
|
||
if (f.kind == syntax.nkind.N_TFIELD) {
|
||
if (tf == nil) {
|
||
let msg: str = "#44/#55: registerstruct: AST/tfield walk desync (more TFIELDs than tinfo.fields)\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let fi: *fieldinfo = alloc(fieldinfo{
|
||
fname = f.str,
|
||
foff = tf.offset: i32,
|
||
fsz = tf.type_.size: i32,
|
||
tnode = f.lhs,
|
||
})!;
|
||
if (head == nil) { head = fi; tail = fi; }
|
||
else { tail.finext = fi; tail = fi; };
|
||
tf = tf.tnext;
|
||
};
|
||
f = f.next;
|
||
};
|
||
si.fields = head;
|
||
si.totsize = ti.slotsize: i32;
|
||
si.sinext = c.structs;
|
||
c.structs = si;
|
||
};
|
||
|
||
fn collectstructs(c: *cgen, file: *syntax.node) void = {
|
||
c.structs = nil;
|
||
if (file == nil) { return; };
|
||
let d: *syntax.node = file.list;
|
||
for (d != nil) {
|
||
if (d.kind == syntax.nkind.N_TYPEDECL) {
|
||
let body: *syntax.node = d.lhs;
|
||
// #9: an error-struct (`type X = !struct{...}`) carries an
|
||
// N_TBANG-wrapped body; peel it so X registers like any
|
||
// struct. cstage is tinfo-based and needs no table, but
|
||
// wwstage's name-keyed widen dispatch (cgreturn needswiden
|
||
// → cgwidentaggedstore) resolves struct layout through
|
||
// c.structs; an unregistered error-struct made the
|
||
// struct-variant-of-large-union return silently drop its
|
||
// construction (cs!=ww). The strategic fix is #222's sret
|
||
// cutover, which deletes this name-keyed dispatch outright;
|
||
// until then the table must be complete (errors.opaque_ is
|
||
// the first such type). #10 tracks retiring the table.
|
||
if (body != nil && body.kind == syntax.nkind.N_TBANG) {
|
||
body = body.lhs;
|
||
};
|
||
if (body != nil) {
|
||
if (body.kind == syntax.nkind.N_TSTRUCT) {
|
||
registerstruct(c, d.str, d.nmod, body);
|
||
};
|
||
};
|
||
};
|
||
d = d.next;
|
||
};
|
||
};
|
||
|
||
// isstrtype — alias-aware. Reads the stamped tinfo so `str`,
|
||
// `type alias = str`, `!str`, and chained aliases all route to the
|
||
// str-shaped slot. Cite cstage cgen.c:159 `type_isstr` SSoT.
|
||
// Collapsed onto typeisstr per A.6.3b (#46); the prior AST walker is
|
||
// reconstituted by typeisstr's TY_NAMED chase + tinfofornode's
|
||
// TBANG-unwrap (check.ww:1145).
|
||
fn isstrtype(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
return syntax.typeisstr(t.type_: *syntax.tinfo);
|
||
};
|
||
|
||
fn isslicetype(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
return syntax.typeisslice(t.type_: *syntax.tinfo);
|
||
};
|
||
|
||
// resolvetagged — return the underlying N_TTAGGED node for `t`, or nil
|
||
// if `t` doesn't ultimately denote a tagged union. Follows N_TNAME
|
||
// aliases (via resolvetype) and unwraps one leading N_TBANG so
|
||
// `type error = !(invalid | overflow);` resolves to its inner
|
||
// `(invalid | overflow)` node. Use at sites that read variant lists
|
||
// or detect nullable folding off a scrutinee — cgmatch, cgtypetest,
|
||
// cgtypeassert — so aliased `!(A|B)` shapes still dispatch.
|
||
export fn resolvetagged(c: *cgen, t: *syntax.node) *syntax.node = {
|
||
let r: *syntax.node = resolvetype(c, t);
|
||
if (r == nil) { return nil; };
|
||
if (r.kind == syntax.nkind.N_TBANG) {
|
||
let inner: *syntax.node = r.lhs;
|
||
if (inner == nil) { return nil; };
|
||
r = resolvetype(c, inner);
|
||
if (r == nil) { return nil; };
|
||
};
|
||
if (r.kind == syntax.nkind.N_TTAGGED) { return r; };
|
||
return nil;
|
||
};
|
||
|
||
// matchscrutt — resolve a non-ident match scrutinee node to its tagged
|
||
// type (or nil if unresolvable). Used by cgmatch to size the
|
||
// @match_spill slot at first use (#15 first-use+fail-loud convergence).
|
||
// IDENT scrutinees use a different lookup path (read off the local
|
||
// directly, no spill) so this returns nil for them too.
|
||
fn matchscrutt(c: *cgen, scrut: *syntax.node) *syntax.node = {
|
||
if (scrut == nil) { return nil; };
|
||
let k: syntax.nkind = scrut.kind;
|
||
if (k == syntax.nkind.N_IDENT) { return nil; };
|
||
if (k == syntax.nkind.N_CALL) {
|
||
let callee: *syntax.node = scrut.lhs;
|
||
if (callee != nil) {
|
||
let cnm: str;
|
||
cnm.ptr = nil; cnm.len = 0;
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.kind == syntax.nkind.N_IDENT) { cnm = callee.str; };
|
||
if (callee.kind == syntax.nkind.N_DOT) {
|
||
cnm = callee.str;
|
||
// Same-module-first disambiguation: a leaf collision
|
||
// on `next` (utf8.next + caller-side next) otherwise
|
||
// returns the last-declared (caller) rtype and the
|
||
// 4-arm match collapses arms 2+ to tag 0. Task #31.
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
};
|
||
if (cnm.len > 0) {
|
||
let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod);
|
||
if (rtyp != nil) { return resolvetagged(c, rtyp); };
|
||
};
|
||
};
|
||
return nil;
|
||
};
|
||
if (k == syntax.nkind.N_INDEX) {
|
||
let ibase: *syntax.node = scrut.lhs;
|
||
if (ibase == nil) { return nil; };
|
||
if (ibase.kind != syntax.nkind.N_IDENT) {
|
||
// #48: non-ident index base (s.field[i], call()[i],
|
||
// nested). Only idents carry a declared tnode to walk,
|
||
// so resolve from the checker-stamped element tinfo
|
||
// instead — the #45/#67 stamped-carrier pattern; cgmatch
|
||
// gates on istaggedtype and reads scrutt.type_ directly.
|
||
// cstage N_MATCH reads s->type for every scrutinee shape
|
||
// (cmd/w6c/cgen.c:7510). Pre-#48 this returned nil and
|
||
// the variant clamped to 0 + @match_spill mis-sized.
|
||
if (istaggedtype(c, scrut)) { return scrut; };
|
||
return nil;
|
||
};
|
||
let bl: *local = localfindnode(c, ibase.str);
|
||
let btn: *syntax.node = nil;
|
||
if (bl != nil) { btn = bl.tnode; }
|
||
else { btn = letvartnode(c, ibase.str); };
|
||
if (btn == nil) { return nil; };
|
||
// idxelemtn: `*[N]T` drills to the pointee array's element (#61).
|
||
let etn: *syntax.node = idxelemtn(btn);
|
||
if (etn == nil) { return nil; };
|
||
return resolvetagged(c, etn);
|
||
};
|
||
if (k == syntax.nkind.N_DOT) {
|
||
// #67: read the field's stamped tinfo off the N_DOT node
|
||
// (post-#66 N_DOT carries the field type) instead of the
|
||
// dotfieldtnode AST walk. cgmatch's gate reads scrutt.type_
|
||
// via istaggedtype, so the resolved N_TTAGGED node the walk
|
||
// produced is no longer the carrier; the istaggedtype guard
|
||
// preserves the nil-for-non-tagged contract the sibling
|
||
// N_CALL/N_INDEX branches get from resolvetagged.
|
||
if (!istaggedtype(c, scrut)) { return nil; };
|
||
return scrut;
|
||
};
|
||
// Family C (#46): a DEREF scrutinee — stamped-carrier like the
|
||
// non-ident N_INDEX/N_DOT arms (`match (*p)` spill size + variant
|
||
// indices key off scrut.type_; pre-#46 nil here clamped the
|
||
// variant to 0 and mis-sized @match_spill).
|
||
if (k == syntax.nkind.N_UN && scrut.op == syntax.tkind.TK_STAR) {
|
||
if (!istaggedtype(c, scrut)) { return nil; };
|
||
return scrut;
|
||
};
|
||
return nil;
|
||
};
|
||
|
||
// matchspillsz — slot size for the @match_spill scratch a non-ident
|
||
// scrutinee lands in. Mirrors cstage's `slot_size = (su->kind ==
|
||
// TY_TAGGED) ? su->size : 16` (cmd/w6c/cgen.c cgmatch). 16 default
|
||
// when the scrutinee type can't be resolved keeps the historical
|
||
// alloc for non-tagged / unresolved cases. Called by cgmatch at first
|
||
// use; #15 first-use+fail-loud pins this size per fn.
|
||
fn matchspillsz(c: *cgen, scrutt: *syntax.node) i32 = {
|
||
if (scrutt == nil) { return 16; };
|
||
let sz: i32 = slotsize(c, scrutt);
|
||
if (sz <= 0) { return 16; };
|
||
return sz;
|
||
};
|
||
|
||
// structparamsize — bytes occupied by a user-defined by-value struct
|
||
// param if it fits in 1-2 SysV integer eightbytes (cstage cgen.c
|
||
// struct_arg_size mirror; gates on size <= 16). Returns 0 for non-
|
||
// struct types or oversized structs so callers can fall through to
|
||
// other dispatch arms. Pre-#11 the wwstage prologue had no struct
|
||
// branch — user-defined struct params dropped through to the 8B
|
||
// scalar catch-all, the second-half value registers (DX/CX) were
|
||
// never spilled, and field reads from the under-allocated slot
|
||
// trailed into the saved-BP word.
|
||
fn structparamsize(c: *cgen, t: *syntax.node) i32 = {
|
||
if (c == nil) { return 0; };
|
||
let r: *syntax.node = resolvetype(c, t);
|
||
if (r == nil) { return 0; };
|
||
if (r.kind != syntax.nkind.N_TNAME) { return 0; };
|
||
let nm: str = r.str;
|
||
if (syntax.streq(nm, "str")) { return 0; };
|
||
if (aliasprimsize(c, nm) > 0) { return 0; };
|
||
let si: *structinfo = structlookup(c, nm);
|
||
if (si == nil) { return 0; };
|
||
if (si.totsize <= 0) { return 0; };
|
||
if (si.totsize > 16) { return 0; };
|
||
return si.totsize;
|
||
};
|
||
|
||
// aggargsize — byte size of a by-value aggregate (struct OR array) call
|
||
// arg, else 0 (#271, mirror of cstage aggarg_size). The size axis the
|
||
// ≤16B-struct structparamsize carve-out doesn't cover: arrays of any
|
||
// size and structs > 16B. Reads the stamped tinfo size (the type table,
|
||
// byte-id with cstage Type.size).
|
||
fn aggargsizetn(t: *syntax.tinfo) i32 = {
|
||
if (t == nil) { return 0; };
|
||
let u: *syntax.tinfo = t;
|
||
u = tichase(u);
|
||
if (u == nil) { return 0; };
|
||
if (u.kind == syntax.tykind.TY_STRUCT || u.kind == syntax.tykind.TY_ARRAY) {
|
||
return u.size: i32;
|
||
};
|
||
return 0;
|
||
};
|
||
|
||
// taggedmemargsize — #38b: a tagged-union arg past the 6-reg register
|
||
// convention (>48B slot, where the register transport's cap trips) is
|
||
// MEMORY-class: the caller stages the whole slot on the outgoing stack
|
||
// below every register-class word and the callee reads it in place at
|
||
// positive BP offsets. Mirror of cstage tagged_memarg_size; ABI shape
|
||
// per ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). The
|
||
// ≤48B register convention is pinned in-tree (test/926 boundary rows).
|
||
fn taggedmemargsize(t: *syntax.tinfo) i32 = {
|
||
if (t == nil) { return 0; };
|
||
let u: *syntax.tinfo = t;
|
||
u = tichase(u);
|
||
if (u == nil) { return 0; };
|
||
if (u.kind != syntax.tykind.TY_TAGGED) { return 0; };
|
||
if (u.nullable != 0) { return 0; };
|
||
// sizelint-ok: 6 SysV int arg regs (DI..R9) x 8B words — the
|
||
// same register-capacity constant as cstage tagged_arg_size.
|
||
if (u.size: i32 <= 6 * 8) { return 0; };
|
||
return u.size: i32;
|
||
};
|
||
|
||
fn nodeisaggarg(n: *syntax.node) bool = {
|
||
if (n == nil) { return false; };
|
||
return aggargsizetn(n.type_: *syntax.tinfo) > 0;
|
||
};
|
||
|
||
// aggargfloatstop — true iff the arg is a ≤16B struct with any float
|
||
// field (#271/#165). Such a struct from a non-ident source would need
|
||
// the SSE eightbyte transport the GP aggregate push/drain can't model;
|
||
// both stages loud-stop on it. Same predicate as the cstage Tfield
|
||
// fld_isfloat walk (cmd/w6c/cgen.c #271 push arm).
|
||
fn aggargfloatstop(n: *syntax.node) bool = {
|
||
if (n == nil) { return false; };
|
||
let st: *syntax.tinfo = n.type_: *syntax.tinfo;
|
||
st = tichase(st);
|
||
if (st == nil) { return false; };
|
||
if (st.kind != syntax.tykind.TY_STRUCT) { return false; };
|
||
if (st.size: i32 > 16) { return false; };
|
||
let f: *syntax.tfield = st.fields;
|
||
for (f != nil) {
|
||
if (syntax.typeisfloat(f.type_)) { return true; };
|
||
f = f.tnext;
|
||
};
|
||
return false;
|
||
};
|
||
|
||
// structfloatclass — SysV per-eightbyte classification for the #165
|
||
// float-bearing-struct param case (param twin of #171's struct return;
|
||
// classifies per-eightbyte, not #163's per-element). Returns 0 when the
|
||
// struct does NOT qualify — the caller keeps the all-GP transport, which
|
||
// is correct + byte-identical there — for: not a <=16B struct; an all-
|
||
// integer layout (no float to route); an f32 field; >1 float packed in
|
||
// one eightbyte; a float straddling the 8-byte SysV eightbyte boundary;
|
||
// or an aggregate field (SysV would recurse, out of scope). Otherwise a
|
||
// packed result whose low bits hold the eightbyte count nb (1|2) and bit
|
||
// (4+e) marks eightbyte e SSE-class (a lone f64). Qualifies iff every
|
||
// eightbyte is pure-INT or a lone f64 AND at least one is f64. f32 /
|
||
// sub-eightbyte packing deferred (#165b). Mirrors cstage
|
||
// struct_float_class (cmd/w6c/cgen.c).
|
||
fn structfloatclass(c: *cgen, t: *syntax.node) i32 = {
|
||
if (c == nil) { return 0; };
|
||
let r: *syntax.node = resolvetype(c, t);
|
||
if (r == nil) { return 0; };
|
||
if (r.kind != syntax.nkind.N_TNAME) { return 0; };
|
||
let nm: str = r.str;
|
||
if (syntax.streq(nm, "str")) { return 0; };
|
||
if (aliasprimsize(c, nm) > 0) { return 0; };
|
||
let si: *structinfo = structlookup(c, nm);
|
||
if (si == nil) { return 0; };
|
||
if (si.totsize <= 0) { return 0; };
|
||
if (si.totsize > 16) { return 0; };
|
||
// SysV classifies aggregates in 8-byte eightbytes; 8 is the
|
||
// eightbyte stride, not a type footprint.
|
||
let nb: i32 = 1;
|
||
if (si.totsize > 8) { nb = 2; };
|
||
let nflt0: i32 = 0; let nflt1: i32 = 0;
|
||
let nint0: i32 = 0; let nint1: i32 = 0;
|
||
let fi: *fieldinfo = si.fields;
|
||
for (fi != nil) {
|
||
let foff: i32 = fi.foff;
|
||
let fsz: i32 = fi.fsz;
|
||
let e: i32 = foff / 8;
|
||
if (e < 0) { return 0; };
|
||
if (e >= nb) { return 0; };
|
||
if (isfloattype(c, fi.tnode)) {
|
||
if (isf32type(c, fi.tnode)) { return 0; };
|
||
if ((foff & 7) != 0) { return 0; };
|
||
if (fsz != 8) { return 0; };
|
||
if (e == 0) { nflt0 += 1; } else { nflt1 += 1; };
|
||
} else {
|
||
if (isslicetype(c, fi.tnode)) { return 0; };
|
||
if (isstrtype(c, fi.tnode)) { return 0; };
|
||
if (istaggedtype(c, fi.tnode)) { return 0; };
|
||
if (structparamsize(c, fi.tnode) > 0) { return 0; };
|
||
// Alias-aware array/tuple reject, mirroring cstage's
|
||
// NAMED-peeled TY_ARRAY/TY_TUPLE (cgen.c struct_float_class).
|
||
// A direct-AST-kind N_TARRAY test misses an aliased array
|
||
// and every tuple field; the fsz>8 guard below also lets a
|
||
// <=8B one slip, so such a struct would wrongly SSE-route on
|
||
// this stage but stay GP on cstage (a #165b leak).
|
||
let rf: *syntax.node = resolvetype(c, fi.tnode);
|
||
if (rf != nil) {
|
||
if (rf.kind == syntax.nkind.N_TARRAY) { return 0; };
|
||
if (rf.kind == syntax.nkind.N_TTUPLE) { return 0; };
|
||
};
|
||
if (fsz > 8) { return 0; };
|
||
if ((foff + fsz - 1) / 8 != e) { return 0; };
|
||
if (e == 0) { nint0 += 1; } else { nint1 += 1; };
|
||
};
|
||
fi = fi.finext;
|
||
};
|
||
let enc: i32 = nb;
|
||
let hasfloat: bool = false;
|
||
if (nflt0 == 1 && nint0 == 0) { enc += 16; hasfloat = true; }
|
||
else { if (nflt0 != 0) { return 0; }; };
|
||
if (nb == 2) {
|
||
if (nflt1 == 1 && nint1 == 0) { enc += 32; hasfloat = true; }
|
||
else { if (nflt1 != 0) { return 0; }; };
|
||
};
|
||
if (!hasfloat) { return 0; };
|
||
return enc;
|
||
};
|
||
|
||
// istaggedtype — alias-aware. Reads stamped tinfo so `T`,
|
||
// `type alias = (A|B)`, `type error = !(invalid|overflow)` all
|
||
// resolve to TY_TAGGED — tinfofornode handles the N_TBANG unwrap
|
||
// (check.ww:1145) so we don't re-walk it here. Cite cstage cgen.c
|
||
// (`type_chase_named` + TY_TAGGED). Collapsed per A.6.3b (#46).
|
||
fn istaggedtype(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
return syntax.typeistagged(t.type_: *syntax.tinfo);
|
||
};
|
||
|
||
// tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo
|
||
// (the #49 funnel predicate; #209/#211 discipline — never tnode
|
||
// names). Mirror of cstage's chase-then-kind test at the fill /
|
||
// assign aggregate arms.
|
||
fn tnodeisagg(t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
let u: *syntax.tinfo = t.type_: *syntax.tinfo;
|
||
u = tichase(u);
|
||
if (u == nil) { return false; };
|
||
if (u.kind == syntax.tykind.TY_STRUCT) { return true; };
|
||
if (u.kind == syntax.tykind.TY_ARRAY) { return true; };
|
||
return u.kind == syntax.tykind.TY_TUPLE;
|
||
};
|
||
|
||
// isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage
|
||
// cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across
|
||
// cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-
|
||
// prologue. Collapsed per A.6.3b (#46).
|
||
export fn isfloattype(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
return syntax.typeisfloat(t.type_: *syntax.tinfo);
|
||
};
|
||
|
||
// isf32type — narrower: true only for f32 (after alias chase). Cite
|
||
// cstage cgen.c:188 `type_isf32`. Picks MOVSS vs MOVSD and the SS-
|
||
// variant arithmetic / cast opcodes. Collapsed per A.6.3b (#46).
|
||
export fn isf32type(c: *cgen, t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
return syntax.typeisf32(t.type_: *syntax.tinfo);
|
||
};
|
||
|
||
// isnullabletype — `(*T | void)` one-word fold per Hare's
|
||
// `(*T | null)` semantics. Cite cstage cgen.c:396 `type_isnullable`;
|
||
// the .nullable flag lands on tinfo at check.ww:1309-1318 when the
|
||
// two-variant shape matches. Collapsed per A.6.3b (#46).
|
||
export fn isnullabletype(t: *syntax.node) bool = {
|
||
if (t == nil) { return false; };
|
||
return syntax.typeisnullable(t.type_: *syntax.tinfo);
|
||
};
|
||
|
||
// nullableptrtag — 0-based index of the *T variant in a nullable
|
||
// union. Mirror of cstage cgen.c:404-416 `nullable_ptr_tag`: linear
|
||
// scan ti.params, strip TY_NAMED on each variant, return idx of first
|
||
// TY_PTR. Phase 1 (26724fe) populated the chain in tinfofornode's
|
||
// TTAGGED arm so this walk could retire the AST-keyed predecessor.
|
||
export fn nullableptrtag(t: *syntax.node) i32 = {
|
||
if (t == nil) { return 0; };
|
||
let ti: *syntax.tinfo = t.type_: *syntax.tinfo;
|
||
if (ti == nil) { return 0; };
|
||
// #63 Phase-N step 1: peel TY_NAMED before this structural query.
|
||
// #64 builds per-decl NAMED wrappers (tinfofornode), so the peel
|
||
// now fires on aliased operands; byte-id holds because it collapses
|
||
// NAMED to the alias-invariant underlying this read consumes.
|
||
ti = tichase(ti);
|
||
if (ti == nil) { return 0; };
|
||
if (ti.kind != syntax.tykind.TY_TAGGED) { return 0; };
|
||
let p: *syntax.tparam = ti.params;
|
||
let i: i32 = 0;
|
||
for (p != nil) {
|
||
let vt: *syntax.tinfo = p.type_;
|
||
if (vt != nil) {
|
||
// peel-ok: single peel PROBE-CLEARED (batch-2 c3-B2,
|
||
// 018ef66) — constructible variant params never carry
|
||
// 2+-level NAMED at this scan; cs twin nullable_ptr_tag
|
||
// (cmd/w6c/cgen.c:747) keeps the identical single peel.
|
||
if (vt.kind == syntax.tykind.TY_NAMED) { vt = vt.under; };
|
||
if (vt != nil) {
|
||
if (vt.kind == syntax.tykind.TY_PTR) { return i; };
|
||
};
|
||
};
|
||
p = p.tnext;
|
||
i += 1;
|
||
};
|
||
return 0;
|
||
};
|
||
|
||
// voidvariantindex — find the 0-based index of the `void` variant in a
|
||
// tagged-union type, -1 if absent. Used by cgreturn to map bare `return;`
|
||
// in a tagged-union-returning fn to the void variant's tag.
|
||
//
|
||
// #1/#3 (F1 fold): reads the NORMALIZED variant chain (ti.params, which
|
||
// tinfofornode now never-drops + dedups), NOT the raw AST tagged.list. The
|
||
// construct/match tag numbering already rides ti.params, so once F1 dedups
|
||
// it, a deduped union with a void variant (e.g. `(i32|i32|void)`) would
|
||
// desync its bare-`return;` void tag from match's if this stayed AST-keyed.
|
||
// Mirrors cstage cg_tag_for_variant(rt, ty_void) (cmd/w6c/cgen.c:900-911):
|
||
// chase NAMED, scan params, match bare TY_VOID (not `!void`, carried by the
|
||
// tparam iserror flag).
|
||
fn voidvariantindex(tagged: *syntax.node) i32 = {
|
||
if (tagged == nil) { return -1; };
|
||
let ti: *syntax.tinfo = tagged.type_: *syntax.tinfo;
|
||
if (ti == nil) { return -1; };
|
||
ti = tichase(ti);
|
||
if (ti == nil) { return -1; };
|
||
if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; };
|
||
let p: *syntax.tparam = ti.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
let vt: *syntax.tinfo = p.type_;
|
||
if (vt != nil && vt.kind == syntax.tykind.TY_VOID && !p.iserror) {
|
||
return idx;
|
||
};
|
||
p = p.tnext;
|
||
idx += 1;
|
||
};
|
||
return -1;
|
||
};
|
||
|
||
// 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: *syntax.node, rhs: *syntax.node) i32 = {
|
||
if (tagged == nil) { return -1; };
|
||
return taggedvariantindext(c, tagged.type_: *syntax.tinfo, rhs);
|
||
};
|
||
|
||
// taggedvariantindext — tinfo-keyed core of taggedvariantindex. Given
|
||
// the dst tagged tinfo `du` (NAMED-peeled internally, gated TY_TAGGED)
|
||
// and the source value node, returns the 0-based variant index. #68: the
|
||
// tagged-store machinery reads tinfo directly (no type node), so the
|
||
// node-keyed taggedvariantindex delegates here off `tagged.type_`.
|
||
//
|
||
// #66 Phase-N step 3: match the value's stamped type against the variant
|
||
// types by typeeq (flatvariantidxt), replacing the rhstargetname surface-
|
||
// name compare. #33: untyped values now resolve in flatvariantidxt's
|
||
// pass 1 (cgvariantmatch's tyassignableuntyped arm, the cg_variant_match
|
||
// :801 mirror); the str/slice shape scan below covers the remaining
|
||
// LOOSE sources (concrete types that typeeq no variant).
|
||
fn taggedvariantindext(c: *cgen, du: *syntax.tinfo, rhs: *syntax.node) i32 = {
|
||
if (du == nil) { return -1; };
|
||
if (rhs == nil) { return -1; };
|
||
let ti: *syntax.tinfo = du;
|
||
ti = tichase(ti);
|
||
if (ti == nil) { return -1; };
|
||
if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; };
|
||
let r: i32 = flatvariantidxt(ti, rhs.type_: *syntax.tinfo, false);
|
||
if (r >= 0) { return r; };
|
||
// Shape fallback: classify rhs as (str, slice, scalar/other) and
|
||
// pick the first variant of matching shape. Covers LOOSE concrete
|
||
// sources that typeeq no variant (untyped sources resolve above
|
||
// via tyassignableuntyped since #33); the slice axis keeps a
|
||
// (u8 | []u8) widen off the leading scalar variant (task #19).
|
||
// tinfo.params is already spread-flattened (#61a — `...inner`
|
||
// inlined in declaration order), so the old N_TTAGGED.list spread-
|
||
// walk collapses to a flat scan over p.type_.
|
||
let wantstr: bool = nodeisstr(c, rhs);
|
||
let wantslice: bool = nodeisslice(c, rhs);
|
||
let p: *syntax.tparam = ti.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
let vt: *syntax.tinfo = p.type_;
|
||
let visstr: bool = syntax.typeisstr(vt);
|
||
let visslice: bool = syntax.typeisslice(vt);
|
||
if (visstr == wantstr && visslice == wantslice) { return idx; };
|
||
p = p.tnext;
|
||
idx += 1;
|
||
};
|
||
return -1;
|
||
};
|
||
|
||
// flatvariantidx — flat 0-based index of the variant whose type matches
|
||
// the pattern node `pat`, by typeeq on the stamped tinfos. Reads the
|
||
// pre-flattened variant chain off tinfo.params (#61a — `...inner`
|
||
// spreads already inlined in declaration order); peels TY_NAMED then
|
||
// gates TY_TAGGED.
|
||
//
|
||
// #66 Phase-N step 3 (THE FLIP, user-ruled B-full): match by
|
||
// typeeq(p.type_, pat.type_) — nominal identity carried by the per-decl
|
||
// TY_NAMED ptr — instead of the surface-name compare. So `type linerr =
|
||
// !str` ≠ str and a cross-module `a.T` ≠ `b.T` are now distinguished.
|
||
// Mirrors cstage cg_variant_match (cmd/w6c/cgen.c:451): both-NAMED →
|
||
// ptr-id (typeeq, typ.ww:514), one-NAMED → kind mismatch → false. ww has
|
||
// no type_assignable, so the untyped/loose arm (cg_variant_match's first
|
||
// branch) lives in the caller's str/slice shape fallback, not here.
|
||
fn flatvariantidx(c: *cgen, tagged: *syntax.node, pat: *syntax.node) i32 = {
|
||
if (tagged == nil) { return -1; };
|
||
if (pat == nil) { return -1; };
|
||
return flatvariantidxt(tagged.type_: *syntax.tinfo, pat.type_: *syntax.tinfo, false);
|
||
};
|
||
|
||
// flatvariantidxt — tinfo-keyed core of flatvariantidx: flat 0-based
|
||
// index of the variant whose type typeeq's `want`, over the pre-
|
||
// flattened tinfo.params chain (#61a). Peels TY_NAMED then gates
|
||
// TY_TAGGED. #68: the tagged-store machinery reads tinfo directly and
|
||
// has no type node to hand the node-keyed flatvariantidx, so the typeeq
|
||
// core lives here; flatvariantidx + taggedvariantindext + cgwidentagremap
|
||
// all funnel through it. Mirrors cstage cg_tag_for_variant
|
||
// (cmd/w6c/cgen.c:503) + cg_variant_match's both-NAMED ptr-id / typeeq
|
||
// arm (:451).
|
||
// exactonly (#95-c3): the is/as ACCEPTANCE gate (check.ww route) wants
|
||
// only nominal variant membership — pass 1. The chain/structural
|
||
// tag-synthesis arms below and their >=2 ambiguity os.exit belong to the
|
||
// cgen WIDEN consumer; routing the checker through them widened is/as
|
||
// acceptance (cs!=ww) and surfaced a cgen fatal mid-check (#107). Two
|
||
// consumers, two modes — not a wrapper. cstage has no twin: its is/as
|
||
// gate (check.c:2036) never calls cg_tag_for_variant, so cg_tag_for_variant
|
||
// stays full-only there.
|
||
fn flatvariantidxt(tagged: *syntax.tinfo, want: *syntax.tinfo, exactonly: bool) i32 = {
|
||
if (want == nil) { return -1; };
|
||
let ti: *syntax.tinfo = tagged;
|
||
ti = tichase(ti);
|
||
if (ti == nil) { return -1; };
|
||
if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; };
|
||
// Pass 1: exact match (NAMED-vs-NAMED typeeq, tagged-vs-tagged, bare
|
||
// typeeq). Exact matches take precedence and need no guard — distinct
|
||
// variants don't exact-match the same source.
|
||
let p: *syntax.tparam = ti.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
if (cgvariantmatch(p.type_, want)) { return idx; };
|
||
p = p.tnext;
|
||
idx += 1;
|
||
};
|
||
if (exactonly) { return -1; };
|
||
// Pass 1b (#95): NAMED source, no exact variant — chain membership.
|
||
// An alias IS-A every type on its NAMED chain (ali2 is-a ali is-a
|
||
// base), so declaring the variant as `ali` admits any source whose
|
||
// chain shares a node with ali's. Two linear NAMED chains intersect
|
||
// iff they share their chased bottom node (.ai/ken-95-oracle.md §1),
|
||
// so membership reduces to pointer identity of the chased ends —
|
||
// tichase is the blessed chase, no raw hops. Variants are counted
|
||
// UNGATED (bare prims are type-table singletons, so a bare variant
|
||
// node can BE the source's bottom): that keeps the guard ≡ harec's
|
||
// nassign>=2 → NULL (ref/harec/src/types.c:734-738; the P1-exact
|
||
// short-circuit is pass 1 above). >=2 chain hits cannot be
|
||
// disambiguated once the nominal-lossy model collapses the chain —
|
||
// hard-error (drew's ambiguity proviso extended to the chained
|
||
// set). cs twin cg_tag_for_variant fused in this commit.
|
||
if (want.kind == syntax.tykind.TY_NAMED) {
|
||
let sb: *syntax.tinfo = tichase(want);
|
||
if (sb == nil) { return -1; };
|
||
let q: *syntax.tparam = ti.params;
|
||
let qi: i32 = 0;
|
||
let found: i32 = -1;
|
||
let n: i32 = 0;
|
||
for (q != nil) {
|
||
if (q.type_ != nil && tichase(q.type_) == sb) {
|
||
if (found < 0) { found = qi; };
|
||
n += 1;
|
||
};
|
||
q = q.tnext;
|
||
qi += 1;
|
||
};
|
||
if (n >= 2) {
|
||
let msg: str = "flatvariantidxt: source alias chain reaches >=2 variants — ambiguous without nominal layout (#95)\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
if (found >= 0) { return found; };
|
||
// c2 (#95): no chain hit (found/n are still -1/0 here) —
|
||
// structural fallback on the chased ends. harec interns bare
|
||
// composites structurally (type_hash, ref/harec/src/
|
||
// types.c:72-81), so a nominally-unrelated structurally-
|
||
// equal decl DEALIASES TO THE SAME NODE there and the
|
||
// assignability arm accepts via `to == from`
|
||
// (types.c:1000-1002) — acceptance is definitional, not an
|
||
// arm we could misread. Our store does not intern, so the
|
||
// pointer compare of pass 1b misses it; chased typeeq is the
|
||
// non-interned rendering of the same rule. EQUALITY only —
|
||
// no type_is_assignable scalar import. The >=2 hard-error is
|
||
// the nominal-lossy-model rendering of a case harec cannot
|
||
// represent (two structurally-identical variants intern to
|
||
// ONE type — a union cannot contain it twice), not a harec
|
||
// deviation. cs twin cg_tag_for_variant fused in this commit.
|
||
q = ti.params;
|
||
qi = 0;
|
||
for (q != nil) {
|
||
if (q.type_ != nil && syntax.typeeq(tichase(q.type_), sb)) {
|
||
if (found < 0) { found = qi; };
|
||
n += 1;
|
||
};
|
||
q = q.tnext;
|
||
qi += 1;
|
||
};
|
||
if (n >= 2) {
|
||
let msg2: str = "flatvariantidxt: source structurally matches >=2 variants — ambiguous without nominal layout (#95)\n";
|
||
os.write(2, msg2.ptr, msg2.len: u64);
|
||
os.exit(1);
|
||
};
|
||
return found;
|
||
};
|
||
// Pass 2 (#15): no exact variant matched — structurally match a BARE
|
||
// source against a NAMED-alias variant (bare *vtable into the
|
||
// `stream` (= *vtable) variant of `(file | stream)`). The bare side
|
||
// has no nominal identity, so structure is the only discriminator;
|
||
// without this the widen found no variant and defaulted to tag 0,
|
||
// miscompiling emitbytes' io.write(&cgoutstream.vt). Exact-first
|
||
// (pass 1) keeps a bare `i64` into `(i64 | oserror)` binding the
|
||
// exact `i64`. drew's proviso: guard the structural fallback like the
|
||
// #218 nested-widen site — a bare source matching >=2 NAMED variants
|
||
// needs nominal layout to disambiguate, so hard-error.
|
||
if (want.kind != syntax.tykind.TY_NAMED) {
|
||
let q: *syntax.tparam = ti.params;
|
||
let qi: i32 = 0;
|
||
let found: i32 = -1;
|
||
let n: i32 = 0;
|
||
for (q != nil) {
|
||
// Full chase (F2a batch-4 c2): the old one-level
|
||
// unwrap missed a chained ptr-alias variant
|
||
// (type a=*X; type b=a) — every pass fell through
|
||
// and the widen defaulted to tag 0, SILENT. The
|
||
// TY_NAMED gate keeps bare variants in pass-1's
|
||
// exact domain; drew's >=2-candidate hard-error
|
||
// below now guards the CHASED match set. cs twin
|
||
// cg_tag_for_variant fused in this commit (probe:
|
||
// both-wrong-identical pre-fix).
|
||
let pu: *syntax.tinfo = q.type_;
|
||
if (pu != nil && pu.kind == syntax.tykind.TY_NAMED
|
||
&& syntax.typeeq(tichase(pu), want)) {
|
||
if (found < 0) { found = qi; };
|
||
n += 1;
|
||
};
|
||
q = q.tnext;
|
||
qi += 1;
|
||
};
|
||
if (n >= 2) {
|
||
let msg: str = "flatvariantidxt: bare source structurally matches >=2 NAMED variants — ambiguous without nominal layout (#15/#218/#199b/#10)\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
return found;
|
||
};
|
||
return -1;
|
||
};
|
||
|
||
// tyassignableuntyped — can a value slot of type `dst` HOLD an untyped
|
||
// source value? The untyped→typed subset of cstage type_assignable
|
||
// (cmd/wcc/type.c:355-370), plus its concrete→tagged variant drill
|
||
// (type.c:316-324) for a variant that is itself a union. ww's checker
|
||
// isassignable is node-keyed (AST type exprs), so the tinfo-keyed
|
||
// widen/match funnel needs this focused mirror (#33).
|
||
fn tyassignableuntyped(dst: *syntax.tinfo, want: *syntax.tinfo) bool = {
|
||
if (dst == nil) { return false; };
|
||
if (want == nil) { return false; };
|
||
// c4 (rob RE-RULE 2026-06-05): FULL chase, not the one-level unwrap
|
||
// — harec type_is_assignable dealiases the dst transitively when it
|
||
// is not tagged and keeps the ORIGINAL dst for the tagged variant
|
||
// drill, taggedness detected via the full chase (ref/harec/src/
|
||
// types.c:989-996). The one-level unwrap left a 2-level named bool
|
||
// alias variant silently mis-tagged 0 (the str twin was masked by
|
||
// taggedvariantindext's shape fallback). Aligns ww UP to the cs
|
||
// twin cmd/wcc/type.c:369-385, harec-shaped since F1 9bd0d8b.
|
||
let du: *syntax.tinfo = tichase(dst);
|
||
if (du != nil && du.kind == syntax.tykind.TY_TAGGED) {
|
||
// concrete→tagged drill (type.c:316-324): a NESTED tagged
|
||
// variant compares by type_eq there — never equal to an
|
||
// untyped source — so only non-tagged variants recurse, on
|
||
// the variant's ORIGINAL type; the variant's taggedness is
|
||
// detected via the full chase (was one-level: a 2-level
|
||
// alias-tagged variant slipped INTO the recursion, diverging
|
||
// from cs's checker-level #199α reject — see the c4 finding).
|
||
let p: *syntax.tparam = du.params;
|
||
for (p != nil) {
|
||
let pu: *syntax.tinfo = tichase(p.type_);
|
||
let nestedtagged: bool = false;
|
||
if (pu != nil) {
|
||
if (pu.kind == syntax.tykind.TY_TAGGED) { nestedtagged = true; };
|
||
};
|
||
if (!nestedtagged) {
|
||
if (tyassignableuntyped(p.type_, want)) { return true; };
|
||
};
|
||
p = p.tnext;
|
||
};
|
||
return false;
|
||
};
|
||
// type.c:369-385 — INT/FLOAT/RUNE run on the UNPEELED dst exactly
|
||
// like cs (typeisnum/typeisfloat/typeisint self-recurse TY_NAMED);
|
||
// STR/BOOL/NIL read the chased du like cs reads its chased du.
|
||
if (want.kind == syntax.tykind.TY_UNTYPED_INT) { return syntax.typeisnum(dst); };
|
||
if (want.kind == syntax.tykind.TY_UNTYPED_FLOAT) { return syntax.typeisfloat(dst); };
|
||
if (want.kind == syntax.tykind.TY_UNTYPED_STR) {
|
||
if (du == nil) { return false; };
|
||
return du.kind == syntax.tykind.TY_STR;
|
||
};
|
||
if (want.kind == syntax.tykind.TY_UNTYPED_RUNE) {
|
||
if (syntax.typeisint(dst)) { return true; };
|
||
return dst.kind == syntax.tykind.TY_RUNE;
|
||
};
|
||
if (want.kind == syntax.tykind.TY_UNTYPED_BOOL) {
|
||
if (du == nil) { return false; };
|
||
return du.kind == syntax.tykind.TY_BOOL;
|
||
};
|
||
if (want.kind == syntax.tykind.TY_UNTYPED_NIL) {
|
||
if (du == nil) { return false; };
|
||
if (du.kind == syntax.tykind.TY_PTR) { return true; };
|
||
if (du.kind == syntax.tykind.TY_SLICE) { return true; };
|
||
if (du.kind == syntax.tykind.TY_CHAN) { return true; };
|
||
return du.kind == syntax.tykind.TY_FN;
|
||
};
|
||
return false;
|
||
};
|
||
|
||
// cgvariantmatch — does a source value of type `want` tag as variant
|
||
// `vt` in a tagged-union dispatch? Mirrors cstage cg_variant_match
|
||
// (cmd/w6c/cgen.c):
|
||
// - untyped source → first variant that can HOLD it (cgen.c:801 →
|
||
// type_assignable; #33 — see tyassignableuntyped)
|
||
// - both NAMED → nominal ptr-id (typeeq line 545 = same ptr only)
|
||
// - exactly one NAMED → #218 nominal lost: fall back to structural
|
||
// equality of the two unwrapped tagged unions, so an outer widen of
|
||
// a NAMED multi-variant union into an enclosing union computes its
|
||
// tag (project tinfo_lossy_nominal). Sound only while the model is
|
||
// nominal-lossy; the collision guard in cgwidentaggedstorebp enforces
|
||
// the invariant for when #199b/B-full lands true nominal layout.
|
||
// - neither NAMED → structural typeeq
|
||
fn cgvariantmatch(vt: *syntax.tinfo, want: *syntax.tinfo) bool = {
|
||
if (vt == nil) { return false; };
|
||
if (want == nil) { return false; };
|
||
// #33: pre-fix an untyped scalar fell past the typeeq passes to
|
||
// taggedvariantindext's str/slice SHAPE fallback, whose first
|
||
// non-str/slice variant can be `void` — a bare
|
||
// `let e: (void | size) = 5` stored tag 0 while the is/as side
|
||
// resolved `size` to 1 (wwstage-only; cstage resolves here).
|
||
if (syntax.typeisuntyped(want)) { return tyassignableuntyped(vt, want); };
|
||
if (vt.kind == syntax.tykind.TY_NAMED && want.kind == syntax.tykind.TY_NAMED) {
|
||
return syntax.typeeq(vt, want);
|
||
};
|
||
if (vt.kind == syntax.tykind.TY_NAMED || want.kind == syntax.tykind.TY_NAMED) {
|
||
let vu: *syntax.tinfo = vt;
|
||
vu = tichase(vu);
|
||
let wu: *syntax.tinfo = want;
|
||
wu = tichase(wu);
|
||
if (vu != nil && wu != nil
|
||
&& vu.kind == syntax.tykind.TY_TAGGED
|
||
&& wu.kind == syntax.tykind.TY_TAGGED) {
|
||
return syntax.typeeq(vu, wu);
|
||
};
|
||
return false;
|
||
};
|
||
return syntax.typeeq(vt, want);
|
||
};
|
||
|
||
// cgvariantstructmatch — structural equality ignoring nominal identity
|
||
// (peel NAMED, then typeeq). #218 collision guard: counts how many dst
|
||
// variants share the source's *shape*; ≥2 means the structural fallback
|
||
// could not disambiguate them once nominal identity is lost. Mirrors
|
||
// cstage cg_variant_struct_match (cmd/w6c/cgen.c).
|
||
fn cgvariantstructmatch(vt: *syntax.tinfo, want: *syntax.tinfo) bool = {
|
||
let vu: *syntax.tinfo = vt;
|
||
vu = tichase(vu);
|
||
let wu: *syntax.tinfo = want;
|
||
wu = tichase(wu);
|
||
if (vu == nil) { return false; };
|
||
if (wu == nil) { return false; };
|
||
return syntax.typeeq(vu, wu);
|
||
};
|
||
|
||
// flatslicevariantidx — flat 0-based index of a slice-shape variant in
|
||
// `tagged`. Prefers the variant whose element typeeq's the pattern
|
||
// element `elem`; falls back to the first slice-shape slot when no exact
|
||
// element match is found (the untyped/loose arm — ww has no
|
||
// type_assignable). Reads the flattened tinfo.params chain (#61a); peels
|
||
// TY_NAMED then gates TY_TAGGED. The slice axis exists because a scalar-
|
||
// vs-`[]T` distinction has no surface name to key on (task #19).
|
||
//
|
||
// #66 Phase-N step 3: element compare flips from surface-name to
|
||
// typeeq(p.type_.sub, elem.type_). Mirrors cstage cg_tag_for_variant
|
||
// over Type->params. Returns -1 when no slice variant exists.
|
||
fn flatslicevariantidx(c: *cgen, tagged: *syntax.node, elem: *syntax.node) i32 = {
|
||
if (tagged == nil) { return -1; };
|
||
let ti: *syntax.tinfo = tagged.type_: *syntax.tinfo;
|
||
ti = tichase(ti);
|
||
if (ti == nil) { return -1; };
|
||
if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; };
|
||
let want: *syntax.tinfo = nil;
|
||
if (elem != nil) { want = elem.type_: *syntax.tinfo; };
|
||
let fallback: i32 = -1;
|
||
let p: *syntax.tparam = ti.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
let vt: *syntax.tinfo = p.type_;
|
||
if (vt != nil) {
|
||
if (syntax.typeisslice(vt)) {
|
||
if (fallback < 0) { fallback = idx; };
|
||
if (want != nil) {
|
||
let su: *syntax.tinfo = vt;
|
||
su = tichase(su);
|
||
if (su != nil) {
|
||
if (syntax.typeeq(su.sub, want)) { return idx; };
|
||
};
|
||
};
|
||
};
|
||
};
|
||
p = p.tnext;
|
||
idx += 1;
|
||
};
|
||
return fallback;
|
||
};
|
||
|
||
// cgwidentagremap — when widening from one tagged union to a wider one,
|
||
// rewrite the source's variant tag at slot_off+0 to use the destination's
|
||
// variant indices. No-op when src and dst index orders coincide.
|
||
//
|
||
// #68: both `du` (dst) and `su` (src) are now the tagged tinfos — peel
|
||
// TY_NAMED then walk su.params, mapping each source variant to its dst
|
||
// index by typeeq (flatvariantidxt). Mirrors cg_widen_tag_remap
|
||
// (cmd/w6c/cgen.c:1177) over su->params + cg_tag_for_variant (:503).
|
||
fn cgwidentagremap(c: *cgen, du: *syntax.tinfo, su: *syntax.tinfo, slot_off: i32) void = {
|
||
let dt: *syntax.tinfo = du;
|
||
dt = tichase(dt);
|
||
if (dt == nil) { return; };
|
||
if (dt.kind != syntax.tykind.TY_TAGGED) { return; };
|
||
let st: *syntax.tinfo = su;
|
||
st = tichase(st);
|
||
if (st == nil) { return; };
|
||
if (st.kind != syntax.tykind.TY_TAGGED) { return; };
|
||
let identity: bool = true;
|
||
let p: *syntax.tparam = st.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
let di: i32 = flatvariantidxt(dt, p.type_, false);
|
||
if (di < 0) { di = 0; };
|
||
if (di != idx) { identity = false; p = nil; }
|
||
else { p = p.tnext; idx += 1; };
|
||
};
|
||
if (identity) { return; };
|
||
let done: str = mklabel(c, "remap_done");
|
||
emitline("\tMOVQ\t");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP), AX\n");
|
||
p = st.params;
|
||
idx = 0;
|
||
for (p != nil) {
|
||
let next: str = mklabel(c, "remap_next");
|
||
let di: i32 = flatvariantidxt(dt, p.type_, false);
|
||
if (di < 0) { di = 0; };
|
||
emitline("\tCMPQ\t$");
|
||
emitint(idx: i64);
|
||
emitline(", AX\n");
|
||
emitline("\tJNE\t");
|
||
emitline(next);
|
||
emitline("\n");
|
||
emitline("\tMOVQ\t$");
|
||
emitint(di: i64);
|
||
emitline(", AX\n");
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
emitline("\tJMP\t");
|
||
emitline(done);
|
||
emitline("\n");
|
||
emitlabel(next);
|
||
p = p.tnext;
|
||
idx += 1;
|
||
};
|
||
emitlabel(done);
|
||
return;
|
||
};
|
||
|
||
// rhsisstructpayload — is `src` a struct value (literal or local ident
|
||
// of a struct type)? Returns the struct name, or empty str. Only true
|
||
// when the name is registered in c.structs — `!void` / `!i32` aliases
|
||
// share the N_STRUCTLIT / N_TNAME shape but aren't structs, and must
|
||
// fall through to the scalar/str/tagged-source paths instead.
|
||
fn rhsstructpayload(c: *cgen, src: *syntax.node) str = {
|
||
let empty: str;
|
||
empty.ptr = nil; empty.len = 0;
|
||
if (src == nil) { return empty; };
|
||
if (src.kind == syntax.nkind.N_STRUCTLIT) {
|
||
let trefn: *syntax.node = src.lhs;
|
||
if (trefn != nil) {
|
||
// #92: bare structlookup missed an alias-named literal
|
||
// — `ali{...}` into a union fell to the scalar widen
|
||
// arm, word0-only payload (the class #62 L2 closed for
|
||
// the N_IDENT-local arm below). Same funnel: the
|
||
// REGISTERED name keeps every consumer's re-lookup
|
||
// hitting. Base spellings: structlookup hits at the
|
||
// chain entry and si.sname == the literal's own name —
|
||
// same string out, same asm.
|
||
let si: *structinfo = structlookupchain(c, trefn);
|
||
if (si != nil) { return si.sname; };
|
||
};
|
||
return empty;
|
||
};
|
||
if (src.kind == syntax.nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, src.str);
|
||
if (lc != nil) {
|
||
let tn: *syntax.node = lc.tnode;
|
||
if (tn != nil) {
|
||
if (tn.kind == syntax.nkind.N_TNAME) {
|
||
// #62 Layer-2 (F2 ww half): bare structlookup
|
||
// missed an alias name (ali->base), so the
|
||
// struct value fell to the SCALAR widen arm —
|
||
// word0-only box payload, words 1+ zero-filled
|
||
// (both-wrong-identical with cstage pre-F1,
|
||
// gate-blind). structlookupchain is the name-
|
||
// domain twin of cstage's su=type_chase_named
|
||
// (cg_widen_tagged_store, c138605); returning
|
||
// the REGISTERED name keeps every consumer's
|
||
// re-lookup hitting. The variant TAG still
|
||
// keys on the un-chased stamped type — the
|
||
// member's nominal identity is the alias.
|
||
let si: *structinfo = structlookupchain(c, tn);
|
||
if (si != nil) { return si.sname; };
|
||
};
|
||
};
|
||
};
|
||
};
|
||
return empty;
|
||
};
|
||
|
||
// rhstaggedsource — return the tagged-type node for `src` when src is a
|
||
// tagged-typed local ident; nil otherwise. The slot-copy path uses this
|
||
// to walk variants for tag remap.
|
||
fn rhstaggedident(c: *cgen, src: *syntax.node) *syntax.node = {
|
||
if (src == nil) { return nil; };
|
||
if (src.kind != syntax.nkind.N_IDENT) { return nil; };
|
||
let lc: *local = localfindnode(c, src.str);
|
||
if (lc == nil) { return nil; };
|
||
let tn: *syntax.node = lc.tnode;
|
||
if (!istaggedtype(c, tn)) { return nil; };
|
||
return resolvetagged(c, tn);
|
||
};
|
||
|
||
// rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX
|
||
// return ABI? True for N_CALL of a tagged-returning fn, N_INDEX of a
|
||
// tagged-element base, and N_DOT of a tagged-typed struct field (after
|
||
// #28's cgdot fix loads AX/DX/CX/R8 from the field's slot). Used to
|
||
// decide whether cgexpr/spill works for the tagged-source branch of
|
||
// cgwidentaggedstore.
|
||
fn rhstaggedabicall(c: *cgen, src: *syntax.node) bool = {
|
||
if (src == nil) { return false; };
|
||
if (src.kind == syntax.nkind.N_CALL) {
|
||
// #211: a call's result type is the CALLEE's fn-type ret, which
|
||
// the checker stamps onto this N_CALL node (check.ww N_CALL: both
|
||
// the SK_FN path and the fn-VALUE/field path set e.type_ = the
|
||
// result tinfo). Read it directly — a value-receiver fn-ptr FIELD
|
||
// call `s.f(...)` keyed by the field leaf `f` with the receiver
|
||
// VARIABLE name as the "module" otherwise mis-binds a same-named
|
||
// GLOBAL fn of different register shape (silent cs≠ww). cstage's
|
||
// sister reads u->ret off the callee type (cmd/wcc/check.c:1433,
|
||
// :1490); harec selects by interned type id, not name (ref/harec/
|
||
// src/types.c:714). Mirrors the N_DOT branch below.
|
||
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
|
||
return false;
|
||
};
|
||
if (src.kind == syntax.nkind.N_INDEX) {
|
||
// The checker stamps every N_INDEX node's type_ to the element
|
||
// tinfo (check.ww:2334-2337 indexresult) for ANY base shape —
|
||
// N_IDENT, N_DOT (`x.o[i]`), or chained N_INDEX (`m[i][j]`). Read
|
||
// it directly, mirroring cstage cg_widen_tagged_store keying on
|
||
// src->type (cmd/w6c/cgen.c:2020-2023). #261: the prior
|
||
// N_IDENT-base-only structural lookup missed N_DOT/N_INDEX bases,
|
||
// so a tagged element materialized via `x.o[i]` (correct AX/DX
|
||
// slot from cgindex) was then spilled by the scalar-widen arm,
|
||
// dropping the tag/payload-high word — silent cs≠ww.
|
||
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
|
||
return false;
|
||
};
|
||
// N_DOT of a tagged-typed struct field — cgdot loads
|
||
// AX=tag, DX=word0, CX=word1[, R8=word2], so downstream
|
||
// spill matches the call/index shapes. #58 A.6.3i-phase-2:
|
||
// read the checker-stamped n.type_ (check.ww N_DOT struct-field
|
||
// stamp) instead of re-deriving via dotfieldtnode — matches
|
||
// cstage cg_widen_tagged_store reading src->type directly
|
||
// (cmd/w6c/cgen.c:1302-1305). typeistagged(nil) is false.
|
||
if (src.kind == syntax.nkind.N_DOT) {
|
||
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
|
||
};
|
||
// Family C (#35/#46): a DEREF source is mem-based (taggedmemread,
|
||
// any size) — the widen-store's memread arm copies the box from
|
||
// the address cgexpr leaves in AX. An unwrap (`?`/`!`) source
|
||
// fills the cursor after the tagged-success payload shift (the
|
||
// cgtryprop/cgtryunw twin of cstage's kind-blind su-tagged arm).
|
||
if (src.kind == syntax.nkind.N_UN && src.op == syntax.tkind.TK_STAR) {
|
||
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
|
||
};
|
||
if (src.kind == syntax.nkind.N_TRYPROP || src.kind == syntax.nkind.N_TRYUNW) {
|
||
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
|
||
};
|
||
return false;
|
||
};
|
||
|
||
// taggedmemread — #37: does cgexpr leave this tagged expr's box in
|
||
// MEMORY (AX = box address) instead of the AX/DX/CX/R8 cursor? True
|
||
// for an N_INDEX/N_DOT read whose box exceeds the 4-reg cursor — the
|
||
// same mem-based class as an sret-classified call (which the #38b
|
||
// gates key separately on callsretsize). Every cursor-spill consumer
|
||
// must branch on this before reading AX as the tag. Mirrors cstage
|
||
// cg_tagged_memread.
|
||
// Family C (#35/#46): a DEREF source is mem-based at ANY size — the
|
||
// pointer value IS the box address, so cgun skips the scalar load
|
||
// (which carried only the tag) and consumers copy from memory. ≤32B
|
||
// INDEX/DOT keep the cursor byte-for-byte (the #37 no-drift bar);
|
||
// the nullable one-word fold stays a scalar deref.
|
||
fn taggedmemread(c: *cgen, e: *syntax.node) bool = {
|
||
if (e == nil) { return false; };
|
||
if (e.kind == syntax.nkind.N_UN && e.op == syntax.tkind.TK_STAR) {
|
||
let du: *syntax.tinfo = e.type_: *syntax.tinfo;
|
||
du = tichase(du);
|
||
if (du == nil) { return false; };
|
||
if (du.kind != syntax.tykind.TY_TAGGED) { return false; };
|
||
if (du.nullable != 0) { return false; };
|
||
return du.size: i32 > 8;
|
||
};
|
||
if (e.kind != syntax.nkind.N_INDEX && e.kind != syntax.nkind.N_DOT) { return false; };
|
||
let u: *syntax.tinfo = e.type_: *syntax.tinfo;
|
||
u = tichase(u);
|
||
if (u == nil) { return false; };
|
||
if (u.kind != syntax.tykind.TY_TAGGED) { return false; };
|
||
return u.size: i32 > TUPLE_GPCAP * 8;
|
||
};
|
||
|
||
// taggedcastpeel — Family C (#35): a tagged→tagged cast is transport-
|
||
// transparent — the operand's box IS the value; transport consumers
|
||
// (widen-store, arg push) derive the remap from the operand's type.
|
||
// Peeling exposes the ident/deref carrier their source arms key on;
|
||
// cgexpr on the cast node itself collapses to one word. Concrete-
|
||
// variant casts (`7: size`) keep their node for variant-tag lookup;
|
||
// the nullable one-word fold never spills a cursor — excluded.
|
||
// Mirrors cstage cg_tagged_castpeel.
|
||
fn taggedcastpeel(c: *cgen, e: *syntax.node) *syntax.node = {
|
||
for (e != nil && e.kind == syntax.nkind.N_CAST && e.lhs != nil) {
|
||
let cu: *syntax.tinfo = e.type_: *syntax.tinfo;
|
||
cu = tichase(cu);
|
||
if (cu == nil) { return e; };
|
||
if (cu.kind != syntax.tykind.TY_TAGGED) { return e; };
|
||
if (cu.nullable != 0) { return e; };
|
||
let iu: *syntax.tinfo = e.lhs.type_: *syntax.tinfo;
|
||
iu = tichase(iu);
|
||
if (iu == nil) { return e; };
|
||
if (iu.kind != syntax.tykind.TY_TAGGED) { return e; };
|
||
if (iu.nullable != 0) { return e; };
|
||
e = e.lhs;
|
||
};
|
||
return e;
|
||
};
|
||
|
||
// taggedidcastpeel — the IDENTITY-only subset of the peel for
|
||
// consumers that key variant indices on the scrutinee's own type
|
||
// (is/as/match): same-type casts are no-ops there, but a WIDENING
|
||
// cast changes the tag numbering and must NOT be peeled — those die
|
||
// loud at the consumer's cast catch-all instead. Mirrors cstage
|
||
// cg_tagged_idcastpeel.
|
||
fn taggedidcastpeel(c: *cgen, e: *syntax.node) *syntax.node = {
|
||
for (e != nil && e.kind == syntax.nkind.N_CAST && e.lhs != nil) {
|
||
if (!syntax.typeeq(e.type_: *syntax.tinfo, e.lhs.type_: *syntax.tinfo)) { return e; };
|
||
let cu: *syntax.tinfo = e.type_: *syntax.tinfo;
|
||
cu = tichase(cu);
|
||
if (cu == nil) { return e; };
|
||
if (cu.kind != syntax.tykind.TY_TAGGED) { return e; };
|
||
e = e.lhs;
|
||
};
|
||
return e;
|
||
};
|
||
|
||
// cgloadtaggedfield — load a tagged-union slot at `basereg`+foff
|
||
// into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1,
|
||
// R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B
|
||
// = + word2 (slice variant). Mirrors the cstage tagged-field load
|
||
// in cmd/w6c/cgen.c (N_DOT TY_STRUCT/TY_PTR branches).
|
||
//
|
||
// Load order is fixed regardless of basereg: tag, word0, word2,
|
||
// word1. CX (word1 target) goes LAST because basereg may itself
|
||
// be CX — top-level globals address via LEAQ name(SB), CX — and
|
||
// overwriting it earlier would trash the base address for the
|
||
// remaining loads. For BP / BX bases the order is harmless.
|
||
// Callers must guarantee basereg is one of "BP", "BX", "CX"; the
|
||
// only register loaded into that is NOT a target is BX, so AX-
|
||
// or DX-rooted callers must spill first.
|
||
fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32,
|
||
cxlast: bool) void = {
|
||
// #37: >32B box — leave its ADDRESS in AX (taggedmemread, the
|
||
// sret-receive convention); the 4-reg cursor walk below would
|
||
// truncate past payload word 2. Mirrors cstage's N_DOT
|
||
// TY_STRUCT/TY_PTR tagged arms.
|
||
if (slot_sz > TUPLE_GPCAP * 8) {
|
||
emitline("\tLEAQ\t");
|
||
emitdispreg(foff: i64, basereg);
|
||
emitline(", AX\n");
|
||
return;
|
||
};
|
||
// tag → AX
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg(foff: i64, basereg);
|
||
emitline(", AX\n");
|
||
// word0 → DX
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg((foff + 8): i64, basereg);
|
||
emitline(", DX\n");
|
||
// word1 → CX, word2 → R8. cstage's direct *struct-ptr arm (cgen.c
|
||
// N_DOT TY_PTR→TY_STRUCT, ~11926) loads them in strict offset order
|
||
// (CX@+16 then R8@+24, BX is not a cursor target); every other arm —
|
||
// local/global struct (CX may be the base addr) and the chained-
|
||
// *struct twin (~12021) — loads R8 BEFORE CX. cxlast selects: true =
|
||
// R8 then CX, false = offset order.
|
||
if (cxlast) {
|
||
if (slot_sz > 24) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg((foff + 24): i64, basereg);
|
||
emitline(", R8\n");
|
||
};
|
||
if (slot_sz > 16) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg((foff + 16): i64, basereg);
|
||
emitline(", CX\n");
|
||
};
|
||
} else {
|
||
if (slot_sz > 16) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg((foff + 16): i64, basereg);
|
||
emitline(", CX\n");
|
||
};
|
||
if (slot_sz > 24) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg((foff + 24): i64, basereg);
|
||
emitline(", R8\n");
|
||
};
|
||
};
|
||
};
|
||
|
||
// cgwidentaggedstore — write tagged-union slot bytes for `src` into
|
||
// the slot at `basereg`+slot_off, sized to slot_sz. Mirrors
|
||
// cg_widen_tagged_store in cmd/w6c/cgen.c.
|
||
//
|
||
// `basereg` selects the addressing root:
|
||
// - "BP": function-frame slot (let / assign / return / structlit /
|
||
// array-elem scratch). Body writes straight to slot_off(BP).
|
||
// - else (e.g. "BX" for *struct field, top-level struct LEAQ
|
||
// base): pointer-rooted dst. cgexpr inside trashes every GPR,
|
||
// so we route through a fresh BP-rooted scratch slot, spill
|
||
// basereg before the body, reload after, then word-copy
|
||
// scratch → (basereg, slot_off).
|
||
//
|
||
// Branches by source shape:
|
||
// - nullable dst (8B slot): cgexpr → AX → slot+0.
|
||
// - tagged src ident: copy slot words, zero-pad, tag-remap.
|
||
// - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr,
|
||
// spill words; no remap (callee already speaks dst tag order — or
|
||
// it doesn't, in which case the source is the wider one and remap
|
||
// would need a reversed direction we don't currently emit).
|
||
// - struct src (literal or ident): zero slot, write fields at +8+foff,
|
||
// tag last.
|
||
// - str src: tag@+0, ptr@+8, len@+16, cap@+24 (str IS []u8, #1/Phase 3).
|
||
// - scalar src: tag@+0, value@+8.
|
||
fn cgwidentaggedstore(c: *cgen, dst: *syntax.tinfo, src: *syntax.node,
|
||
basereg: str, slot_off: i32, slot_sz: i32) void = {
|
||
if (syntax.streq(basereg, "BP")) {
|
||
cgwidentaggedstorebp(c, dst, src, slot_off, slot_sz);
|
||
return;
|
||
};
|
||
// Pointer-rooted dst: spill basereg (cgexpr will trash it),
|
||
// materialise into a BP-rooted scratch via the BP path, then
|
||
// reload basereg and word-copy scratch → caller's slot.
|
||
let bspill: i32 = localadd(c, "@tagbase", 8, nil);
|
||
emitline("\tMOVQ\t");
|
||
emitline(basereg);
|
||
emitline(", ");
|
||
emitoff(bspill: i64);
|
||
emitline("(BP)\n");
|
||
// Shared per-size scratch sized at first use (#15/#26c, size-keyed
|
||
// by #44): sibling sites (cgreturn, pushargsrev, cgindex) hitting
|
||
// the same slot size reuse the slot; a different size pins its own.
|
||
let scr: i32 = tagscradd(c, slot_sz);
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let z: i32 = 0;
|
||
for (z < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((scr + z): i64);
|
||
emitline("(BP)\n");
|
||
z += 8;
|
||
};
|
||
cgwidentaggedstorebp(c, dst, src, scr, slot_sz);
|
||
emitline("\tMOVQ\t");
|
||
emitoff(bspill: i64);
|
||
emitline("(BP), ");
|
||
emitline(basereg);
|
||
emitline("\n");
|
||
let k: i32 = 0;
|
||
for (k < slot_sz) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((scr + k): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitdispreg((slot_off + k): i64, basereg);
|
||
emitline("\n");
|
||
k += 8;
|
||
};
|
||
};
|
||
|
||
// cgwidentaggedstorebp — BP-rooted body. Called via cgwidentaggedstore
|
||
// for the natural "BP" case and via the wrapper's scratch path for
|
||
// pointer-rooted dst. Direct callers exist only in case of future
|
||
// inlined uses inside this file; new code should call the wrapper.
|
||
fn cgwidentaggedstorebp(c: *cgen, dst: *syntax.tinfo, src: *syntax.node, slot_off: i32, slot_sz: i32) void = {
|
||
// #68: dst is the tagged tinfo. Peel TY_NAMED → du and gate
|
||
// TY_TAGGED, mirroring cstage cg_widen_tagged_store's
|
||
// `du = (dst->kind==TY_NAMED)?dst->under:dst` + TY_TAGGED guard
|
||
// (cmd/w6c/cgen.c:1273). resolvetagged's N_TBANG unwrap is already
|
||
// handled upstream by tinfofornode (check.ww:1203-1210).
|
||
let dt: *syntax.tinfo = dst;
|
||
dt = tichase(dt);
|
||
if (dt == nil) { return; };
|
||
if (dt.kind != syntax.tykind.TY_TAGGED) { return; };
|
||
// Nullable fold: one 8B word holding the pointer (or 0 for void).
|
||
if (dt.nullable != 0) {
|
||
cgexpr(c, src);
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
};
|
||
// Family C (#35): a tagged→tagged cast is transport-transparent
|
||
// — peel it so the ident/deref/memread source arms below see the
|
||
// carrier and the remap keys on the operand's type. Pre-#35 the
|
||
// cast node fell to the scalar arm (`let w: un3 = (v: un3)`
|
||
// stored tag 0 + word0). Mirrors cstage cg_widen_tagged_store.
|
||
src = taggedcastpeel(c, src);
|
||
// `expr: TaggedAlias` where the cast's destination IS the union
|
||
// itself is a widening, not a re-interpret. cgexpr on a CAST
|
||
// produces the inner's register shape (str: AX=ptr, BX=len), not
|
||
// the tagged AX/DX/CX triple — so peel to the inner and route
|
||
// through the matching concrete-variant branch below. A cast to
|
||
// a concrete variant (`7: i32`) is left intact so the existing
|
||
// scalar / str / slice branches pick the right variant tag.
|
||
if (src != nil) {
|
||
if (src.kind == syntax.nkind.N_CAST) {
|
||
if (src.lhs != nil) {
|
||
let inner: *syntax.node = src.lhs;
|
||
let inneristagged: bool = false;
|
||
if (inner.kind == syntax.nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, inner.str);
|
||
if (lc != nil) {
|
||
inneristagged = istaggedtype(c, lc.tnode);
|
||
};
|
||
};
|
||
if (rhstaggedabicall(c, inner)) {
|
||
inneristagged = true;
|
||
};
|
||
// Cast's destination = the dst tagged union
|
||
// itself? The rhs of N_CAST holds the target
|
||
// type. #68: compare on the stamped tinfos —
|
||
// `castu == dt` (peeled-underlying ptr-id) ||
|
||
// (castu tagged && typeeq(castt, dst)) — mirroring
|
||
// cstage cg_widen_tagged_store's
|
||
// `cast_is_widen = (castu==du) || (castu->kind==
|
||
// TY_TAGGED && type_eq(castt, dst))`
|
||
// (cmd/w6c/cgen.c:1295-1296). Replaces the prior
|
||
// surface-name streq; same-alias TNAMEs share one
|
||
// NAMED tinfo (check.ww:1160-1173) so typeeq hits
|
||
// the a==b fast path.
|
||
let castisdst: bool = false;
|
||
let castsubset: bool = false;
|
||
let castrhs: *syntax.node = src.rhs;
|
||
if (castrhs != nil) {
|
||
let castt: *syntax.tinfo = castrhs.type_: *syntax.tinfo;
|
||
let castu: *syntax.tinfo = castt;
|
||
castu = tichase(castu);
|
||
if (castu != nil) {
|
||
if (castu == dt) {
|
||
castisdst = true;
|
||
} else { if (castu.kind == syntax.tykind.TY_TAGGED) {
|
||
if (syntax.typeeq(castt, dst)) {
|
||
castisdst = true;
|
||
} else {
|
||
castsubset = true;
|
||
};
|
||
}; };
|
||
};
|
||
};
|
||
// S1/#35 (rule 7): a widen-SUBSET cast — the cast target
|
||
// is a TAGGED union that is NOT dst (castu tagged &&
|
||
// !typeeq(castt, dst)). The inner-variant tag is never
|
||
// remapped to dst's index, so the scalar arm below would
|
||
// emit tag=0: a SILENT mis-tag on any 2nd-variant value
|
||
// (census S1: `return true: inner`, inner=(int|bool) into
|
||
// (int|bool|str), ran the int arm not the bool arm). Loud-
|
||
// align to cstage cgen.c:2721-2723. The !inneristagged gate
|
||
// matches the src=inner collapse below — a tagged inner is
|
||
// handled by the #218 nested arm. Faithful remap (read inner
|
||
// tag, inner-idx→outer-idx) = the #23/#40 widen-subset
|
||
// feature, deferred post-CSP (needs the nominal variant-
|
||
// remap table).
|
||
if (castsubset && !inneristagged) {
|
||
let m35: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n";
|
||
os.write(2, m35.ptr, m35.len: u64);
|
||
os.exit(1);
|
||
};
|
||
if (castisdst && !inneristagged) {
|
||
src = inner;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
// #62 Layer-2: ONE source-classify chase at entry (post cast peels,
|
||
// where src is final) — the per-arm single reads it replaces all
|
||
// chased the same src.type_. Mirrors cstage cg_widen_tagged_store's
|
||
// `su = type_chase_named(st)` position (c138605). Tag lookups keep
|
||
// the un-chased src.type_ (nominal identity is the alias).
|
||
let su: *syntax.tinfo = src.type_: *syntax.tinfo;
|
||
su = tichase(su);
|
||
// #218: is the source itself a single NESTED variant of dt (its
|
||
// whole tagged type matches one dt variant), rather than a flattened
|
||
// SUBSET whose members spread into dt? If so, the inner tagged value
|
||
// is the payload: store it at slot_off+8 with the outer tag at
|
||
// slot_off+0, mirroring the scalar/struct/str single-variant arms —
|
||
// NOT a copy-to-+0 + sub-variant remap. flatvariantidxt's structural
|
||
// fallback (cgvariantmatch) recovers the index after the nominal-
|
||
// lossy collapse. Gated on a tagged source so scalar/str/struct
|
||
// sources keep their existing arms. Mirrors cstage
|
||
// cg_widen_tagged_store's nested arm (cmd/w6c/cgen.c).
|
||
let srctagged: bool = (rhstaggedident(c, src) != nil)
|
||
|| rhstaggedabicall(c, src);
|
||
// #51 (#263): a module-global tagged ident is also a tagged source
|
||
// (no local slot — handled by the global branch in the nested copy).
|
||
if (!srctagged) { if (src.kind == syntax.nkind.N_IDENT) {
|
||
if (localfindnode(c, src.str) == nil) {
|
||
let gtn51: *syntax.node = letvartnode(c, src.str);
|
||
if (gtn51 != nil) { if (istaggedtype(c, gtn51)) { srctagged = true; }; };
|
||
};
|
||
}; };
|
||
if (srctagged) {
|
||
let nested: i32 = flatvariantidxt(dt, src.type_: *syntax.tinfo, false);
|
||
if (nested >= 0) {
|
||
// drew collision guard: the structural fallback over-
|
||
// matches if ≥2 nominally-distinct dt variants share the
|
||
// source's shape. Unreachable under today's nominal-lossy
|
||
// model, but INVERTS when #199b/B-full lands the nominal
|
||
// layer — hard-error NOW so a future collision STOPS the
|
||
// compiler instead of silently mis-tagging.
|
||
let nmatch: i32 = 0;
|
||
let gp: *syntax.tparam = dt.params;
|
||
for (gp != nil) {
|
||
if (cgvariantstructmatch(gp.type_,
|
||
src.type_: *syntax.tinfo)) {
|
||
nmatch += 1;
|
||
};
|
||
gp = gp.tnext;
|
||
};
|
||
if (nmatch >= 2) {
|
||
let msg: str = "cgwidentaggedstore: structural fallback cannot disambiguate nominally-distinct same-shape variants without nominal layout (#218/#199b/B-full)\n";
|
||
os.write(2, msg.ptr, msg.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let ssz: i32 = su.size: i32;
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let zk: i32 = 0;
|
||
for (zk < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + zk): i64);
|
||
emitline("(BP)\n");
|
||
zk += 8;
|
||
};
|
||
if (src.kind == syntax.nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, src.str);
|
||
if (lc != nil) {
|
||
let soff: i32 = lc.off;
|
||
let ck: i32 = 0;
|
||
for (ck < ssz) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((soff + ck): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8 + ck): i64);
|
||
emitline("(BP)\n");
|
||
ck += 8;
|
||
};
|
||
} else {
|
||
// #51 (#263 ww-runtime-correct): a module-global tagged
|
||
// ident source — no BP slot. Land g(SB) in SI
|
||
// (aggargsrcaddr) and copy the inner box to slot+8.
|
||
// Pre-fix rhstaggedident returned nil for a global, so
|
||
// srctagged was false and the value fell to the scalar
|
||
// word0 arm — gi's TAG landed as the payload. cstage
|
||
// copies frame garbage (cstage half #44).
|
||
if (aggargsrcaddr(c, src, "SI")) {
|
||
let ck2: i32 = 0;
|
||
for (ck2 < ssz) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(ck2: i64);
|
||
emitline("(SI), AX\n");
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8 + ck2): i64);
|
||
emitline("(BP)\n");
|
||
ck2 += 8;
|
||
};
|
||
};
|
||
};
|
||
} else {
|
||
// non-nested SUBSET-widen of a GLOBAL tagged
|
||
// source still mis-copies (both stages) — task #49.
|
||
// #38b: an sret-classified call result is in
|
||
// memory (AX = dest pointer), not the cursor —
|
||
// the spill below would store the pointer as
|
||
// the payload. Mem-to-mem widen is #40.
|
||
if (src.kind == syntax.nkind.N_CALL) {
|
||
if (callsretsize(c, src) > 0) {
|
||
let m40a: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n";
|
||
os.write(2, m40a.ptr, m40a.len: u64);
|
||
os.exit(1);
|
||
};
|
||
};
|
||
if (taggedmemread(c, src)) {
|
||
// #37: >32B box read — ADDRESS in AX;
|
||
// copy the inner box from memory into
|
||
// the payload area. Mirrors cstage.
|
||
cgexpr(c, src);
|
||
let mk: i32 = 0;
|
||
for (mk < ssz) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg(mk: i64, "AX");
|
||
emitline(", DX\n");
|
||
emitline("\tMOVQ\tDX, ");
|
||
emitoff((slot_off + 8 + mk): i64);
|
||
emitline("(BP)\n");
|
||
mk += 8;
|
||
};
|
||
} else {
|
||
// #37 (rule 7): >32B from a non-mem-based
|
||
// kind would spill an unfilled cursor.
|
||
if (ssz > TUPLE_GPCAP * 8) {
|
||
let m37a: str = "#37: >32B tagged payload from a non-mem-based source unwired (rule 7)\n";
|
||
os.write(2, m37a.ptr, m37a.len: u64);
|
||
os.exit(1);
|
||
};
|
||
// Family C catch-all (rule 7): a tagged cast
|
||
// surviving taggedcastpeel (cast to a THIRD
|
||
// union) has no cursor — loud, not word0
|
||
// garbage. Mirrors cstage.
|
||
if (src.kind == syntax.nkind.N_CAST) {
|
||
let m35a: str = "#35: tagged cast source shape unwired at the widen nested arm (rule 7)\n";
|
||
os.write(2, m35a.ptr, m35a.len: u64);
|
||
os.exit(1);
|
||
};
|
||
cgexpr(c, src);
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8): i64);
|
||
emitline("(BP)\n");
|
||
if (ssz > 8) {
|
||
emitline("\tMOVQ\tDX, ");
|
||
emitoff((slot_off + 16): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
if (ssz > 16) {
|
||
emitline("\tMOVQ\tCX, ");
|
||
emitoff((slot_off + 24): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
if (ssz > 24) {
|
||
emitline("\tMOVQ\tR8, ");
|
||
emitoff((slot_off + 32): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
};
|
||
};
|
||
emitline("\tMOVQ\t$");
|
||
emitint(nested: i64);
|
||
emitline(", ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
};
|
||
};
|
||
// Tagged source ident: byte-copy slot words then tag-remap.
|
||
// rhstaggedident gates "src is a tagged-typed local ident"; the
|
||
// remap reads the source tagged tinfo off the local's tnode (#68).
|
||
let st: *syntax.node = rhstaggedident(c, src);
|
||
if (st != nil) {
|
||
let lc: *local = localfindnode(c, src.str);
|
||
let ssz: i32 = slotsize(c, lc.tnode);
|
||
let soff: i32 = lc.off;
|
||
let k: i32 = 0;
|
||
for (k < ssz) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((soff + k): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + k): i64);
|
||
emitline("(BP)\n");
|
||
k += 8;
|
||
};
|
||
if (ssz < slot_sz) {
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let p: i32 = ssz;
|
||
for (p < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + p): i64);
|
||
emitline("(BP)\n");
|
||
p += 8;
|
||
};
|
||
};
|
||
cgwidentagremap(c, dt, lc.tnode.type_: *syntax.tinfo, slot_off);
|
||
return;
|
||
};
|
||
// Tagged source via AX/DX/CX/R8 register ABI (N_CALL, N_INDEX
|
||
// of tagged element). R8 carries the 4th word for slice-payload
|
||
// variants (slot 32B).
|
||
if (rhstaggedabicall(c, src)) {
|
||
// #38b: an sret-classified call result is in memory (AX =
|
||
// dest pointer), not the cursor. #40.
|
||
if (src.kind == syntax.nkind.N_CALL) {
|
||
if (callsretsize(c, src) > 0) {
|
||
let m40b: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n";
|
||
os.write(2, m40b.ptr, m40b.len: u64);
|
||
os.exit(1);
|
||
};
|
||
};
|
||
// #37: >32B box read (insts[pc], t.N, s.f) — cgexpr left
|
||
// its ADDRESS in AX; copy the whole box from memory, pad,
|
||
// tag-remap — the mem-based twin of the ident arm above.
|
||
// Mirrors cstage cg_widen_tagged_store's memread arm.
|
||
if (taggedmemread(c, src)) {
|
||
let ssz37: i32 = su.size: i32;
|
||
cgexpr(c, src);
|
||
let mk37: i32 = 0;
|
||
for (mk37 < ssz37) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg(mk37: i64, "AX");
|
||
emitline(", DX\n");
|
||
emitline("\tMOVQ\tDX, ");
|
||
emitoff((slot_off + mk37): i64);
|
||
emitline("(BP)\n");
|
||
mk37 += 8;
|
||
};
|
||
if (ssz37 < slot_sz) {
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let pp37: i32 = ssz37;
|
||
for (pp37 < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + pp37): i64);
|
||
emitline("(BP)\n");
|
||
pp37 += 8;
|
||
};
|
||
};
|
||
cgwidentagremap(c, dt, src.type_: *syntax.tinfo, slot_off);
|
||
return;
|
||
};
|
||
// #55: spill the AX/DX/CX/R8 cursor by the SOURCE box width
|
||
// (su.size), zero-pad to the dst slot, then tag-remap — the
|
||
// cursor twin of the ident / memread arms above. Pre-#55 it
|
||
// spilled by slot_sz (reading STALE high regs when the source is
|
||
// narrower) and skipped the pad + remap, so an INDEX/DOT tagged
|
||
// element widened into a wider/reordered union pushed a stale word
|
||
// and the un-remapped source tag (silent cs!=ww). Mirrors cstage
|
||
// cg_widen_tagged_store's cursor arm + shared pad/remap tail
|
||
// (cmd/w6c/cgen.c:2695-2714). Same-slot source (ssz==slot_sz)
|
||
// leaves the pad empty + an identity remap, so the emission is
|
||
// byte-identical to the prior arm for the exact-slot callers.
|
||
cgexpr(c, src);
|
||
let ssz: i32 = su.size: i32;
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
if (ssz > 8) {
|
||
emitline("\tMOVQ\tDX, ");
|
||
emitoff((slot_off + 8): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
if (ssz > 16) {
|
||
emitline("\tMOVQ\tCX, ");
|
||
emitoff((slot_off + 16): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
if (ssz > 24) {
|
||
emitline("\tMOVQ\tR8, ");
|
||
emitoff((slot_off + 24): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
if (ssz < slot_sz) {
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let pp: i32 = ssz;
|
||
for (pp < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + pp): i64);
|
||
emitline("(BP)\n");
|
||
pp += 8;
|
||
};
|
||
};
|
||
cgwidentagremap(c, dt, src.type_: *syntax.tinfo, slot_off);
|
||
return;
|
||
};
|
||
// #37 (rule 7): a >32B TAGGED source of a kind the resolver arms
|
||
// above don't carry (deref/cast/unwrap/...) would fall to the
|
||
// scalar word0 arm below and silently truncate — keyed on the
|
||
// stamped src.type_ (kind-blind), the twin of cstage
|
||
// cg_widen_tagged_store's generic-else bound. Surfaced by
|
||
// reviewer-37's `let w = *p` probe on a 56B box: cstage loud,
|
||
// wwstage silent (rule-10 break).
|
||
if (su != nil && su.kind == syntax.tykind.TY_TAGGED
|
||
&& su.size: i32 > TUPLE_GPCAP * 8) {
|
||
let m37f: str = "#37: >32B tagged source of a non-mem-based kind unwired (rule 7)\n";
|
||
os.write(2, m37f.ptr, m37f.len: u64);
|
||
os.exit(1);
|
||
};
|
||
// Family C catch-all (rule 7): a tagged cast surviving
|
||
// taggedcastpeel (cast to a THIRD union) would fall to the
|
||
// scalar arm below and silently truncate — loud. Mirrors
|
||
// cstage's widen subset-arm cast bound.
|
||
if (src.kind == syntax.nkind.N_CAST && su != nil
|
||
&& su.kind == syntax.tykind.TY_TAGGED && su.nullable == 0) {
|
||
let m35b: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n";
|
||
os.write(2, m35b.ptr, m35b.len: u64);
|
||
os.exit(1);
|
||
};
|
||
// #242: tuple payload. Each element rides ONE register-ABI
|
||
// eightbyte — scalar/float a single 8B word, a slice/str its 3-word
|
||
// {ptr,len,cap} header (24B) — matching the tagged-return load
|
||
// (AX=tag, DX=word0, CX=word1, R8=word2) and the cgmlet receive
|
||
// cursor. NOT the packed-by-size t.N field layout (#238). Mirror of
|
||
// cstage cg_widen_tagged_store's TY_TUPLE arm.
|
||
//
|
||
// #66: the cast-wrapped tuple literal `((a, b): range_alias)` is
|
||
// the spelling real code uses (regex.ha:213) — the cast targets the
|
||
// CONCRETE variant, so the widen-cast peel above leaves it intact
|
||
// and pre-#66 it fell to the scalar arm, silently dropping payload
|
||
// slot 1+. Peel to the inner tuple here; src.type_ stays the CAST's
|
||
// type, which resolves the variant tag by exact named match, so the
|
||
// #241 untyped-element un-matchability does not arise for this form.
|
||
let tupsrc: *syntax.node = nil;
|
||
let tupcast: bool = false;
|
||
if (src != nil) {
|
||
if (src.kind == syntax.nkind.N_TUPLE) { tupsrc = src; };
|
||
if (src.kind == syntax.nkind.N_CAST) {
|
||
if (src.lhs != nil) {
|
||
if (src.lhs.kind == syntax.nkind.N_TUPLE) {
|
||
tupsrc = src.lhs;
|
||
tupcast = true;
|
||
};
|
||
};
|
||
};
|
||
// #116: a NON-LITERAL tuple-typed source (ident, index,
|
||
// deref) is no longer loud here — it routes to the
|
||
// addressable block-copy arm just below the literal arm.
|
||
// Mirrors cstage cg_widen_tagged_store.
|
||
};
|
||
if (tupsrc != nil) {
|
||
if (su != nil) { if (su.kind == syntax.tykind.TY_TUPLE) {
|
||
// #242/#241: mirror cstage's loud-stop CONDITION, not its
|
||
// -1 mechanism (rule 10, align the RICHER side DOWN).
|
||
// wwstage types `true`/`false` as bool and a suffix-less `7`
|
||
// as untyped_int, so flatvariantidxt below DOES resolve the
|
||
// variant — but cstage's cg_tag_for_variant can't type a bare
|
||
// literal element (#241), returns -1, and loud-stops. A
|
||
// program cstage rejects, wwstage must also reject. The shape
|
||
// cstage can't type: a bool literal (N_TRUE/N_FALSE) or a
|
||
// suffix-less numeric literal (untyped_int/untyped_float).
|
||
// LIFT BOTH stage guards together when #241 fixes cstage
|
||
// literal typing -> symmetric accept. BARE form only (#66):
|
||
// the cast form resolves its tag from the cast's type on
|
||
// BOTH stages, so bare elements are fine there.
|
||
let bl: *syntax.node = tupsrc.list;
|
||
for (bl != nil && !tupcast) {
|
||
let bare: bool = false;
|
||
if (bl.kind == syntax.nkind.N_TRUE) { bare = true; };
|
||
if (bl.kind == syntax.nkind.N_FALSE) { bare = true; };
|
||
if (bl.kind == syntax.nkind.N_INTLIT && bl.tsuffix.len == 0) {
|
||
bare = true;
|
||
};
|
||
if (bl.kind == syntax.nkind.N_FLOATLIT && bl.tsuffix.len == 0) {
|
||
bare = true;
|
||
};
|
||
if (bare) {
|
||
let ml: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n";
|
||
os.write(2, ml.ptr, ml.len: u64);
|
||
os.exit(1);
|
||
};
|
||
bl = bl.next;
|
||
};
|
||
// #242: resolve the variant tag via the typeeq core
|
||
// (flatvariantidxt) — NOT taggedvariantindext, whose
|
||
// str/slice shape fallback would silently pick tag 0 for an
|
||
// unmatched tuple, diverging from cstage cg_tag_for_variant
|
||
// (which returns -1) and masking the loud-stop below.
|
||
let ttag: i32 = flatvariantidxt(dt, src.type_: *syntax.tinfo, false);
|
||
// #242: an untyped/literal tuple element (`(true,7)`) leaves
|
||
// the src tuple un-matchable, so the variant tag can't
|
||
// resolve — the supported shape is a tuple of TYPED exprs
|
||
// (strconv parseint `(neg, n)`). Loud-stop rather than
|
||
// silently mis-tag (rule 7); #241 literal-init family.
|
||
if (ttag < 0) {
|
||
let m1: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n";
|
||
os.write(2, m1.ptr, m1.len: u64);
|
||
os.exit(1);
|
||
};
|
||
// #242: this 8B-per-eightbyte packing is correct only when
|
||
// no two scalar elements share a SysV eightbyte — e.g.
|
||
// (bool,u64). A (i32,i32,u64) would overflow the union
|
||
// payload the slotted write assumes. Loud-stop (rule 7);
|
||
// SysV eightbyte tuple classification is a deferred
|
||
// follow-up. Symmetric with cstage cg_widen_tagged_store.
|
||
let ttotal: i32 = 0;
|
||
let ce: *syntax.node = tupsrc.list;
|
||
for (ce != nil) {
|
||
// #47 gap-A: a tagged element rides its OWN
|
||
// box (tag + payload, roundup8) per tuple slot
|
||
// — NOT one 8B word. Mirror the checker's
|
||
// N_TTUPLE accumulation (check.ww:1640-1646);
|
||
// the store loop below boxes it recursively
|
||
// (two-level widen). Symmetric with cstage.
|
||
let ceti: *syntax.tinfo = ce.type_: *syntax.tinfo;
|
||
ceti = tichase(ceti);
|
||
if (ceti != nil && ceti.kind == syntax.tykind.TY_TAGGED) {
|
||
ttotal += (ceti.size: i32 + 7) & ~7;
|
||
} else { if (nodeisstr(c, ce) || nodeisslice(c, ce)) {
|
||
ttotal += 24;
|
||
} else { ttotal += 8; }; };
|
||
ce = ce.next;
|
||
};
|
||
if (8 + ttotal > slot_sz) {
|
||
let m2: str = "cgwidentaggedstore: tuple-in-union payload needs SysV eightbyte packing (narrow elements share an eightbyte; see #242 follow-up)\n";
|
||
os.write(2, m2.ptr, m2.len: u64);
|
||
os.exit(1);
|
||
};
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let tzk: i32 = 0;
|
||
for (tzk < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + tzk): i64);
|
||
emitline("(BP)\n");
|
||
tzk += 8;
|
||
};
|
||
let tfoff: i32 = 0;
|
||
let te: *syntax.node = tupsrc.list;
|
||
for (te != nil) {
|
||
// #47 gap-A: a tagged element is its own
|
||
// tag+payload box. Recurse so the inner box
|
||
// (tag@slot+0, payload@slot+8) is built at the
|
||
// element's tuple-payload offset, exactly as a
|
||
// top-level tagged-store does — two-level widen
|
||
// (inner boxes here, outer tuple tag stamped
|
||
// below). slot stride = roundup8(box). Symmetric
|
||
// with cstage cg_widen_tagged_store.
|
||
let teti: *syntax.tinfo = te.type_: *syntax.tinfo;
|
||
teti = tichase(teti);
|
||
if (teti != nil && teti.kind == syntax.tykind.TY_TAGGED) {
|
||
let ebox: i32 = (teti.size: i32 + 7) & ~7;
|
||
cgwidentaggedstore(c, te.type_: *syntax.tinfo, te,
|
||
"BP", slot_off + 8 + tfoff, ebox);
|
||
tfoff += ebox;
|
||
te = te.next;
|
||
continue;
|
||
};
|
||
let isflt: bool = isfloattype(c, te);
|
||
let wide: bool = nodeisstr(c, te) || nodeisslice(c, te);
|
||
let esz: i32 = 8;
|
||
let eti: *syntax.tinfo = te.type_: *syntax.tinfo;
|
||
if (eti != nil) { esz = eti.size: i32; };
|
||
cgexpr(c, te);
|
||
if (isflt) {
|
||
let mov: str = "MOVSD";
|
||
if (isf32type(c, te)) { mov = "MOVSS"; };
|
||
emitline("\t");
|
||
emitline(mov);
|
||
emitline("\tX0, ");
|
||
emitoff((slot_off + 8 + tfoff): i64);
|
||
emitline("(BP)\n");
|
||
} else { if (wide) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8 + tfoff): i64);
|
||
emitline("(BP)\n");
|
||
emitline("\tMOVQ\tBX, ");
|
||
emitoff((slot_off + 8 + tfoff + 8): i64);
|
||
emitline("(BP)\n");
|
||
emitline("\tMOVQ\tCX, ");
|
||
emitoff((slot_off + 8 + tfoff + 16): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
let sop: str = tnodestoreop(c, te, esz);
|
||
emitline("\t");
|
||
emitline(sop);
|
||
emitline("\tAX, ");
|
||
emitoff((slot_off + 8 + tfoff): i64);
|
||
emitline("(BP)\n");
|
||
}; };
|
||
if (wide) { tfoff += 24; } else { tfoff += 8; };
|
||
te = te.next;
|
||
};
|
||
emitline("\tMOVQ\t$");
|
||
emitint(ttag: i64);
|
||
emitline(", ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
}; };
|
||
};
|
||
// #116: a NON-LITERAL but ADDRESSABLE tuple source — a tuple
|
||
// IDENT var, a slice/array INDEX (tbl[i]), or a DEREF (*p). The
|
||
// tuple in memory uses the SAME tupeslot strides as the box
|
||
// payload the literal loop above fills, so the source-in-memory
|
||
// layout already equals the box payload layout — the fill is a
|
||
// flat block-copy of sum(tupeslot) bytes from the source address
|
||
// into slot_off+8, no re-slotting (a tagged element rides over
|
||
// as its already-built box). Kept loud (out of scope): a
|
||
// CALL/sret result (#40-kin) and struct-field / array-literal-
|
||
// element sources (loud EARLIER at construction, #49 / #270-1c).
|
||
// A cast wrapping a concrete-variant tuple (`(tbl[i]: ci)`)
|
||
// survived the widen-cast peel above; its operand is the
|
||
// addressable expr. Mirror of cstage cg_widen_tagged_store.
|
||
if (su != nil) { if (su.kind == syntax.tykind.TY_TUPLE) {
|
||
let addrsrc: *syntax.node = src;
|
||
if (addrsrc.kind == syntax.nkind.N_CAST) {
|
||
if (addrsrc.lhs != nil) { addrsrc = addrsrc.lhs; };
|
||
};
|
||
let okkind: bool = false;
|
||
if (addrsrc.kind == syntax.nkind.N_IDENT) { okkind = true; };
|
||
if (addrsrc.kind == syntax.nkind.N_INDEX) { okkind = true; };
|
||
if (addrsrc.kind == syntax.nkind.N_UN) {
|
||
if (addrsrc.op == syntax.tkind.TK_STAR) { okkind = true; };
|
||
};
|
||
if (!okkind) {
|
||
let mk: str = "cgwidentaggedstore: tuple-typed source shape unwired (only the bare/cast tuple literal and the addressable ident/index/deref trio carry a full payload; see #116)\n";
|
||
os.write(2, mk.ptr, mk.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let ntag: i32 = flatvariantidxt(dt, src.type_: *syntax.tinfo, false);
|
||
if (ntag < 0) {
|
||
let mt: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n";
|
||
os.write(2, mt.ptr, mt.len: u64);
|
||
os.exit(1);
|
||
};
|
||
// The tuple's type-table size IS sum(tupeslot) under the 8B-slot
|
||
// tuple layout (every walk takes its stride from tupeslot; the
|
||
// type's size is their sum), so the payload byte-count routes
|
||
// through the type table (rule 13) without re-walking the
|
||
// elements — and a wwstage tuple tinfo carries no per-element
|
||
// params list anyway. Mirror of cstage cg_widen_tagged_store.
|
||
let ntotal: i32 = su.size: i32;
|
||
if (8 + ntotal > slot_sz) {
|
||
let mp: str = "cgwidentaggedstore: tuple-in-union payload needs SysV eightbyte packing (narrow elements share an eightbyte; see #242 follow-up)\n";
|
||
os.write(2, mp.ptr, mp.len: u64);
|
||
os.exit(1);
|
||
};
|
||
if (!cgplaceaddr(c, addrsrc, "SI")) {
|
||
let ma: str = "cgwidentaggedstore: addressable tuple source address unresolved (see #116)\n";
|
||
os.write(2, ma.ptr, ma.len: u64);
|
||
os.exit(1);
|
||
};
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let nzk: i32 = 0;
|
||
for (nzk < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + nzk): i64);
|
||
emitline("(BP)\n");
|
||
nzk += 8;
|
||
};
|
||
let nck: i32 = 0;
|
||
for (nck < ntotal) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(nck: i64);
|
||
emitline("(SI), AX\n");
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8 + nck): i64);
|
||
emitline("(BP)\n");
|
||
nck += 8;
|
||
};
|
||
emitline("\tMOVQ\t$");
|
||
emitint(ntag: i64);
|
||
emitline(", ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
}; };
|
||
// #50 (#263 ww-runtime-correct): a module-GLOBAL struct ident source.
|
||
// rhsstructpayload below is local/literal-only, so a global struct value
|
||
// fell to the scalar word0 arm — payload truncated. Land g(SB) in SI and
|
||
// byte-copy the struct words into slot+8; cstage zero-fills the payload
|
||
// (cstage half #43). Local/literal sources keep the existing arms (byte-id).
|
||
if (src.kind == syntax.nkind.N_IDENT) {
|
||
if (localfindnode(c, src.str) == nil) {
|
||
let gtn: *syntax.node = letvartnode(c, src.str);
|
||
if (gtn != nil) { if (gtn.kind == syntax.nkind.N_TNAME) {
|
||
let gsi: *structinfo = structlookupchain(c, gtn);
|
||
if (gsi != nil) {
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let gz: i32 = 0;
|
||
for (gz < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + gz): i64);
|
||
emitline("(BP)\n");
|
||
gz += 8;
|
||
};
|
||
let gtag: i32 = taggedvariantindext(c, dt, src);
|
||
if (gtag < 0) { gtag = 0; };
|
||
if (aggargsrcaddr(c, src, "SI")) {
|
||
let gtot: i32 = gsi.totsize;
|
||
let gk: i32 = 0;
|
||
for (gk + 8 <= gtot) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(gk: i64);
|
||
emitline("(SI), AX\n");
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8 + gk): i64);
|
||
emitline("(BP)\n");
|
||
gk += 8;
|
||
};
|
||
if (gk < gtot) {
|
||
let gtail: i32 = gtot - gk;
|
||
let glop: str = "MOVQ";
|
||
if (gtail == 4) { glop = "MOVL"; }
|
||
else { if (gtail == 1) { glop = "MOVB"; }; };
|
||
emitline("\t"); emitline(glop); emitline("\t");
|
||
emitoff(gk: i64); emitline("(SI), AX\n");
|
||
emitline("\t"); emitline(glop); emitline("\tAX, ");
|
||
emitoff((slot_off + 8 + gk): i64); emitline("(BP)\n");
|
||
};
|
||
emitline("\tMOVQ\t$");
|
||
emitint(gtag: i64);
|
||
emitline(", ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
};
|
||
};
|
||
}; };
|
||
};
|
||
};
|
||
// Struct payload (literal or ident).
|
||
let sname: str = rhsstructpayload(c, src);
|
||
if (sname.len > 0) {
|
||
let si: *structinfo = structlookup(c, sname);
|
||
if (si != nil) {
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let zoff: i32 = 0;
|
||
for (zoff < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + zoff): i64);
|
||
emitline("(BP)\n");
|
||
zoff += 8;
|
||
};
|
||
let tag: i32 = taggedvariantindext(c, dt, src);
|
||
if (tag < 0) { tag = 0; };
|
||
if (src.kind == syntax.nkind.N_STRUCTLIT) {
|
||
// #23: delegate to the single fill path. The
|
||
// inline field loop this replaces was a
|
||
// parallel fill that drifted: it lacked the
|
||
// tagged-field widen arm, so a (void|T)-typed
|
||
// field's raw scalar landed in the field's
|
||
// TAG word (silent truncation past the first
|
||
// tagged field, both stages). Delegation also
|
||
// inherits the nested-struct / call / array-
|
||
// lit field arms; float / str / slice /
|
||
// scalar fields emit byte-identically to the
|
||
// old loop EXCEPT fsz==2 scalars, where the
|
||
// old loop's fieldstoreop emitted MOVW
|
||
// against cstage's MOVQ — a latent cs≠ww the
|
||
// fill's #13-pinned dispatch closes. Mirror
|
||
// of cstage cg_widen_tagged_store's
|
||
// N_STRUCTLIT arm.
|
||
cgstructlitfill(c, si, src, 0, 0, "",
|
||
slot_off + 8);
|
||
} else {
|
||
// Struct ident source: byte-copy struct words to slot+8+k.
|
||
let lc: *local = localfindnode(c, src.str);
|
||
let soff: i32 = 0;
|
||
if (lc != nil) { soff = lc.off; };
|
||
let stotal: i32 = si.totsize;
|
||
let ki: i32 = 0;
|
||
for (ki + 8 <= stotal) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((soff + ki): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8 + ki): i64);
|
||
emitline("(BP)\n");
|
||
ki += 8;
|
||
};
|
||
if (ki < stotal) {
|
||
let tail: i32 = stotal - ki;
|
||
let lop: str = "MOVQ";
|
||
if (tail == 4) { lop = "MOVL"; }
|
||
else { if (tail == 1) { lop = "MOVB"; }; };
|
||
emitline("\t");
|
||
emitline(lop);
|
||
emitline("\t");
|
||
emitoff((soff + ki): i64);
|
||
emitline("(BP), AX\n");
|
||
emitline("\t");
|
||
emitline(lop);
|
||
emitline("\tAX, ");
|
||
emitoff((slot_off + 8 + ki): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
};
|
||
emitline("\tMOVQ\t$");
|
||
emitint(tag: i64);
|
||
emitline(", ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
};
|
||
};
|
||
// str IS []u8 — same 32B payload as a slice: cgexpr leaves
|
||
// (AX=ptr, BX=len, CX=cap); slot layout [+0]=tag, [+8]=ptr,
|
||
// [+16]=len, [+24]=cap. str folds onto the slice arm (#1/Phase 3
|
||
// collapse; cite cstage cg_widen_tagged_store).
|
||
if (nodeisslice(c, src) || nodeisstr(c, src)) {
|
||
cgexpr(c, src);
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8): i64);
|
||
emitline("(BP)\n");
|
||
emitline("\tMOVQ\tBX, ");
|
||
emitoff((slot_off + 16): i64);
|
||
emitline("(BP)\n");
|
||
emitline("\tMOVQ\tCX, ");
|
||
emitoff((slot_off + 24): i64);
|
||
emitline("(BP)\n");
|
||
let tag: i32 = taggedvariantindext(c, dt, src);
|
||
if (tag < 0) { tag = 0; };
|
||
emitline("\tMOVQ\t$");
|
||
emitint(tag: i64);
|
||
emitline(", ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
};
|
||
// Float arm: cgexpr on an f64/f32 source leaves the bit pattern in
|
||
// X0 only — the AX-store fallback below would silently write whatever
|
||
// was loaded into AX before the SSE conversion. Literal `1.0` works
|
||
// by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX
|
||
// before MOVSD'ing into X0); every runtime f64 shape (cast, call,
|
||
// unary, ident, struct-field load) needs the explicit MOVSD path.
|
||
// Mirror of cstage cg_widen_tagged_store's float arm. Classify off
|
||
// the checker stamp (src.type_) — the SSoT cstage reads via
|
||
// node_isfloat / type_isf32 — and resolve the variant tag by name
|
||
// directly: rhstargetname has no N_FLOATLIT / N_CALL / N_DOT branch
|
||
// and would fall through to the str-shape fallback that picks tag 0
|
||
// for an `(i64 | f64)` union. The armed asserttyped bail (check.ww)
|
||
// guarantees src carries a non-nil stamp, so the sibling-evidence
|
||
// loud-abort that used to pin "the float arm requires a stamped
|
||
// value" is dead and removed.
|
||
let fkind: i32 = 0;
|
||
if (src != nil) {
|
||
let srct: *syntax.tinfo = src.type_: *syntax.tinfo;
|
||
if (syntax.typeisf32(srct)) { fkind = 1; }
|
||
else { if (syntax.typeisfloat(srct)) { fkind = 2; }; };
|
||
};
|
||
if (fkind != 0) {
|
||
let fmov: str = "MOVSD";
|
||
if (fkind == 1) { fmov = "MOVSS"; };
|
||
cgexpr(c, src);
|
||
emitline("\t");
|
||
emitline(fmov);
|
||
emitline("\tX0, ");
|
||
emitoff((slot_off + 8): i64);
|
||
emitline("(BP)\n");
|
||
// #227: zero pad words (+16..slot_sz) so a >16B union slot
|
||
// carries the full dst payload width, not just the 1-word float
|
||
// value (the BP path never pre-zeroes; a passthrough return or
|
||
// *u8 reinterpret otherwise reads stack garbage at slot+16/+24).
|
||
// Symmetric with cstage cg_widen_tagged_store float arm.
|
||
if (slot_sz > 16) {
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let zp: i32 = 16;
|
||
for (zp < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + zp): i64);
|
||
emitline("(BP)\n");
|
||
zp += 8;
|
||
};
|
||
};
|
||
// #66 Phase-N step 3: the float arm has no pattern node to ride
|
||
// the typeeq flatvariantidx path, so pick the variant by float
|
||
// kind (f32 vs f64) over tinfo.params — a shape classification
|
||
// like the slice axis, not nominal identity.
|
||
let wantf32: bool = (fkind == 1);
|
||
let ftag: i32 = -1;
|
||
let fti: *syntax.tinfo = dt;
|
||
fti = tichase(fti);
|
||
if (fti != nil) { if (fti.kind == syntax.tykind.TY_TAGGED) {
|
||
let fp: *syntax.tparam = fti.params;
|
||
let fidx: i32 = 0;
|
||
for (fp != nil) {
|
||
let fvt: *syntax.tinfo = fp.type_;
|
||
fvt = tichase(fvt);
|
||
if (fvt != nil) {
|
||
if (syntax.typeisfloat(fvt)) {
|
||
if (syntax.typeisf32(fvt) == wantf32) { ftag = fidx; break; };
|
||
};
|
||
};
|
||
fp = fp.tnext;
|
||
fidx += 1;
|
||
};
|
||
}; };
|
||
if (ftag < 0) { ftag = 0; };
|
||
emitline("\tMOVQ\t$");
|
||
emitint(ftag: i64);
|
||
emitline(", ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
};
|
||
// Scalar payload. #227: zero pad words (+16..slot_sz) — see float
|
||
// arm above. The BP path never pre-zeroes, so a passthrough return /
|
||
// *u8 reinterpret of the narrow-tagged value otherwise reads stack
|
||
// garbage in slot+16/+24. Symmetric with cstage scalar arm.
|
||
cgexpr(c, src);
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + 8): i64);
|
||
emitline("(BP)\n");
|
||
if (slot_sz > 16) {
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
let zp: i32 = 16;
|
||
for (zp < slot_sz) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((slot_off + zp): i64);
|
||
emitline("(BP)\n");
|
||
zp += 8;
|
||
};
|
||
};
|
||
let tag: i32 = taggedvariantindext(c, dt, src);
|
||
if (tag < 0) { tag = 0; };
|
||
emitline("\tMOVQ\t$");
|
||
emitint(tag: i64);
|
||
emitline(", ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
return;
|
||
};
|
||
|
||
// Spine-walk a chained N_DOT (n) inward to a root ident, summing field
|
||
// offsets through value-struct intermediates. Optional slice/str leaf
|
||
// pseudo-field (.ptr / .len / .cap) on the last segment is folded into
|
||
// *outslicedelta (0/8/16); otherwise *outleaftype is the leaf *tinfo
|
||
// and *outslicedelta stays -1. Returns true on success; on false the
|
||
// caller falls through to other branches.
|
||
//
|
||
// Mirrors cmd/w6c/cgen.c's N_DOT chained walker; both stages must agree
|
||
// on the same shapes so the bootstrap fixed-point holds. The chain
|
||
// depth is capped at 16 — deeper chains are vanishingly rare and fall
|
||
// through.
|
||
//
|
||
// On success the caller emits one load/store at root_base + *outtotaloff
|
||
// (+ slicedelta for pseudo leaf). Root resolves as: local frame slot
|
||
// (*outisglobal false, base = *outrootoff(BP)) or top-level let
|
||
// (*outisglobal true, base reached via LEAQ *outrootname(SB), CX).
|
||
//
|
||
// Numeric out-params are i32 — offsets fit naturally and the post-#19
|
||
// localloadop sign-extends i32 deref-stored slots on read, so negative
|
||
// frame offsets round-trip intact.
|
||
// #71 (A.6.3j): the spine offset-sum + leaf-type now read off
|
||
// tinfo.fields (natural layout) instead of the structinfo/fieldinfo
|
||
// walk. Mirror of cstage cmd/w6c/cgen.c:3156-3216 which walks
|
||
// `cur->lhs->type` fields. Root resolution (the local/ptr/global split
|
||
// and the rootoff/ptrroot/isglobal flags) stays on localfindnode/
|
||
// letvarstructinfo unchanged, so the firing set + addressing mode are
|
||
// byte-identical to the structinfo era; only the layout SOURCE moves.
|
||
// The slot-padded foff and the natural tfield.offset coincide for every
|
||
// shape the byte-id gate exercises (cstage already reads the natural
|
||
// offset), so this is offset-preserving. Leaf out-param is the field's
|
||
// stamped *tinfo (was *fieldinfo); the str/slice pseudo-leaf leaves it
|
||
// nil and the callers gate on slicedelta>=0 first.
|
||
export fn dotchainresolve(c: *cgen, n: *syntax.node,
|
||
outrootname: *str, outrootoff: *i32, outtotaloff: *i32,
|
||
outleaftype: **syntax.tinfo, outslicedelta: *i32,
|
||
outisglobal: *bool, outptrroot: *bool) bool = {
|
||
*outrootname = "";
|
||
*outrootoff = 0;
|
||
*outisglobal = false;
|
||
*outptrroot = false;
|
||
*outtotaloff = 0;
|
||
*outleaftype = nil;
|
||
*outslicedelta = -1;
|
||
if (n == nil) { return false; };
|
||
if (n.kind != syntax.nkind.N_DOT) { return false; };
|
||
let stk: [16]*syntax.node;
|
||
let nsteps: i32 = 0;
|
||
let cur: *syntax.node = n;
|
||
for (cur != nil) {
|
||
if (cur.kind != syntax.nkind.N_DOT) { break; };
|
||
if (nsteps >= 16) { return false; };
|
||
stk[nsteps] = cur;
|
||
nsteps += 1;
|
||
cur = cur.lhs;
|
||
};
|
||
if (nsteps < 2) { return false; };
|
||
if (cur == nil) { return false; };
|
||
if (cur.kind != syntax.nkind.N_IDENT) { return false; };
|
||
*outrootname = cur.str;
|
||
let resolved: bool = false;
|
||
let lc: *local = localfindnode(c, cur.str);
|
||
if (lc != nil) {
|
||
if (lc.tnode != nil) {
|
||
if (lc.tnode.kind == syntax.nkind.N_TNAME) {
|
||
*outrootoff = lc.off;
|
||
resolved = true;
|
||
};
|
||
// `*T` root (param/local): dereference at emit time;
|
||
// pointee struct supplies the field layout. Callers
|
||
// that opt in via *outptrroot emit a MOVQ load of the
|
||
// slot before indexing.
|
||
if (lc.tnode.kind == syntax.nkind.N_TPTR) {
|
||
let pe: *syntax.node = lc.tnode.lhs;
|
||
if (pe != nil) {
|
||
if (pe.kind == syntax.nkind.N_TNAME) {
|
||
*outrootoff = lc.off;
|
||
*outptrroot = true;
|
||
resolved = true;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
// Global `*struct` root (`let gp: *S`): localfindnode is nil and
|
||
// letvarstructinfo below only matches a VALUE-struct global, so a
|
||
// `*struct` global root needs its own arm. Gate on N_TPTR→N_TNAME
|
||
// exactly as the local `*T` arm above; the post-resolution loop peels
|
||
// the TY_PTR off cur.type_ and validates the pointee is a struct.
|
||
// Mirrors cstage cgen.c's ptr_root + let_islet base resolution
|
||
// (LEAQ name(SB),CX; MOVQ (CX),CX). (#16, chained twin of #15.)
|
||
if (!resolved && lc == nil) {
|
||
let gtn: *syntax.node = letvartnode(c, cur.str);
|
||
if (gtn != nil) {
|
||
if (gtn.kind == syntax.nkind.N_TPTR) {
|
||
let pe: *syntax.node = gtn.lhs;
|
||
if (pe != nil) {
|
||
if (pe.kind == syntax.nkind.N_TNAME) {
|
||
*outisglobal = true;
|
||
*outptrroot = true;
|
||
resolved = true;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
if (!resolved) {
|
||
let gsi: *structinfo = letvarstructinfo(c, cur.str);
|
||
if (gsi != nil) {
|
||
*outisglobal = true;
|
||
resolved = true;
|
||
};
|
||
};
|
||
if (!resolved) { return false; };
|
||
// Root struct layout = the stamped root-ident type_, peeled NAMED
|
||
// (plus one TY_PTR hop for a `*struct` root). tfield.type_ then
|
||
// supplies each nested struct directly, so no name re-lookup.
|
||
let curstruct: *syntax.tinfo = cur.type_: *syntax.tinfo;
|
||
curstruct = tichase(curstruct);
|
||
if (*outptrroot) {
|
||
if (curstruct == nil) { return false; };
|
||
if (curstruct.kind != syntax.tykind.TY_PTR) { return false; };
|
||
curstruct = curstruct.sub;
|
||
curstruct = tichase(curstruct);
|
||
};
|
||
let i: i32 = nsteps - 1;
|
||
for (i >= 0) {
|
||
if (curstruct == nil) { return false; };
|
||
if (curstruct.kind != syntax.tykind.TY_STRUCT) { return false; };
|
||
if (stk[i] == nil) { return false; };
|
||
let stepnm: str = stk[i].str;
|
||
let tf: *syntax.tfield = curstruct.fields;
|
||
let found: *syntax.tfield = nil;
|
||
for (tf != nil) {
|
||
if (syntax.streq(tf.name, stepnm)) { found = tf; break; };
|
||
tf = tf.tnext;
|
||
};
|
||
if (found == nil) { return false; };
|
||
let foff: i32 = found.offset: i32;
|
||
if (i == 0) {
|
||
*outtotaloff = *outtotaloff + foff;
|
||
*outleaftype = found.type_;
|
||
return true;
|
||
};
|
||
let ft: *syntax.tinfo = found.type_;
|
||
ft = tichase(ft);
|
||
if (ft == nil) { return false; };
|
||
if (ft.kind == syntax.tykind.TY_STR) {
|
||
// str IS []u8: .cap is the third header word, same as
|
||
// the TY_SLICE leaf below — cstage treats str≡slice for
|
||
// .ptr/.len/.cap (cmd/w6c/cgen.c:2478) (#1/Phase 3, #11).
|
||
if (i != 1) { return false; };
|
||
let pseudo: str = stk[0].str;
|
||
let delta: i32 = -1;
|
||
if (syntax.streq(pseudo, "ptr")) { delta = 0; }
|
||
else { if (syntax.streq(pseudo, "len")) { delta = 8; }
|
||
else { if (syntax.streq(pseudo, "cap")) { delta = 16; }; }; };
|
||
if (delta < 0) { return false; };
|
||
*outtotaloff = *outtotaloff + foff;
|
||
*outslicedelta = delta;
|
||
return true;
|
||
};
|
||
if (ft.kind == syntax.tykind.TY_SLICE) {
|
||
if (i != 1) { return false; };
|
||
let pseudo: str = stk[0].str;
|
||
let delta: i32 = -1;
|
||
if (syntax.streq(pseudo, "ptr")) { delta = 0; }
|
||
else { if (syntax.streq(pseudo, "len")) { delta = 8; }
|
||
else { if (syntax.streq(pseudo, "cap")) { delta = 16; }; }; };
|
||
if (delta < 0) { return false; };
|
||
*outtotaloff = *outtotaloff + foff;
|
||
*outslicedelta = delta;
|
||
return true;
|
||
};
|
||
if (ft.kind != syntax.tykind.TY_STRUCT) { return false; };
|
||
*outtotaloff = *outtotaloff + foff;
|
||
curstruct = ft;
|
||
i -= 1;
|
||
};
|
||
return false;
|
||
};
|
||
|
||
// cgstructlitfill — fill a struct-typed slot from an N_STRUCTLIT
|
||
// value into one of three destination flavors. Mirror of cstage
|
||
// cgen.c's cg_structlit_fill. Used by cglet, cgreturn N_STRUCTLIT,
|
||
// cgassign N_IDENT-lhs N_STRUCTLIT (BP-rel) AND cgassign N_DOT-lhs
|
||
// N_STRUCTLIT (BP-rel / via *struct local / via struct global) at
|
||
// single-dot and chained-dot sites.
|
||
//
|
||
// Destination modes:
|
||
// 0 = DST_BP — base = BP, no reload. Stores at disp+i(BP).
|
||
// srcoff/srcname unused.
|
||
// 1 = DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before
|
||
// the ELLIPSIS zero-fill loop and before EVERY
|
||
// field store (cgexpr clobbers BX between
|
||
// fields). Stores at disp+i(BX). srcname
|
||
// unused.
|
||
// 2 = DST_GLOBAL — base = BX, reloaded via `LEAQ srcname(SB),
|
||
// BX` with the same cadence as DST_PTR_LOCAL.
|
||
// srcoff unused.
|
||
//
|
||
// Param semantics (locked in here so the recursion contract is
|
||
// clear):
|
||
// - `disp` is the per-recursion accumulator — grows by `fi.foff`
|
||
// as we descend into a nested struct-typed structlit field.
|
||
// - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are
|
||
// *constant* across the whole call tree — they identify the
|
||
// root dst, which doesn't change with depth.
|
||
// - the ELLIPSIS zero-fill extent is read internally as
|
||
// structabisize(si) — cstage's cg_structlit_fill computes
|
||
// `sz = lu->size` (cgen.c:2085), the maxalign-rounded ABI size
|
||
// (check.c:760 lu->size = (off+maxalign-1)&~(maxalign-1)). The
|
||
// pre-#169 callers passed two different sizes (natural at DOT
|
||
// sites, slot-padded at BP-rel sites); neither matched cstage
|
||
// for maxalign<8 structs (the zero-fill ran MOVQ where cstage
|
||
// ran MOVL — value-correct, asm-divergent).
|
||
//
|
||
// Why a helper? The inline field-walk previously did
|
||
// `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose
|
||
// value is itself a nested N_STRUCTLIT, cgexpr has no whole-struct-
|
||
// in-register convention — it lands AX = first qword and the
|
||
// trailing bytes silently stay zero. #17 fixed the BP-rel sites;
|
||
// #18 extends the same recursion to the four cgassign N_DOT-lhs
|
||
// structlit walks (single-dot via_ptr/global/local + chained
|
||
// depth>=2).
|
||
//
|
||
// The non-BP modes emit a redundant BX reload at the start of each
|
||
// recursive nested zero-fill / each recursive scalar store — this is
|
||
// correctness-by-construction (BX is always freshly loaded right
|
||
// before use), and the redundancy only fires on the nested-STRUCTLIT
|
||
// shapes that didn't compile before. Byte-identity for the no-
|
||
// nested case (the only shape selfhost source uses today) is
|
||
// preserved because the existing inline code's reload-before-each-
|
||
// store pattern matches the helper's per-store reload exactly.
|
||
//
|
||
// Graduation note (task #13): the scalar store currently uses the
|
||
// explicit {1→MOVB, 4→MOVL, else MOVQ} dispatch to match cstage
|
||
// byte-identically — cstage hasn't yet learned MOVW for fsz==2. Once
|
||
// #13 aligns both stages, the dispatch can switch to fieldstoreop
|
||
// which already returns MOVW where appropriate.
|
||
fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *syntax.node,
|
||
mode: i32, srcoff: i32, srcname: str,
|
||
disp: i32) void = {
|
||
if (si == nil) { return; };
|
||
let basereg: str = "BP";
|
||
if (mode != 0) { basereg = "BX"; };
|
||
let totsize: i32 = structabisize(si);
|
||
if (lit.op == syntax.tkind.TK_ELLIPSIS) {
|
||
// `..., ...` autofill — zero the entire slot first so
|
||
// unmentioned fields read as 0. Sized stores: 8/4/1. For
|
||
// non-BP modes, reload BX once before the loop (cgexpr-free
|
||
// region between iterations, so one reload is enough).
|
||
emitline("\tXORQ\tAX, AX\n");
|
||
if (mode == 1) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(srcoff: i64);
|
||
emitline("(BP), BX\n");
|
||
};
|
||
if (mode == 2) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, srcname);
|
||
emitline("(SB), BX\n");
|
||
};
|
||
let zi: i32 = 0;
|
||
for (zi + 8 <= totsize) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
if (mode == 0) {
|
||
emitoff((disp + zi): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg((disp + zi): i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
zi += 8;
|
||
};
|
||
for (zi + 4 <= totsize) {
|
||
emitline("\tMOVL\tAX, ");
|
||
if (mode == 0) {
|
||
emitoff((disp + zi): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg((disp + zi): i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
zi += 4;
|
||
};
|
||
for (zi < totsize) {
|
||
emitline("\tMOVB\tAX, ");
|
||
if (mode == 0) {
|
||
emitoff((disp + zi): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg((disp + zi): i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
zi += 1;
|
||
};
|
||
};
|
||
let fieldnode: *syntax.node = lit.list;
|
||
for (fieldnode != nil) {
|
||
if (fieldnode.kind == syntax.nkind.N_FIELD) {
|
||
let fname: str = fieldnode.str;
|
||
let fi: *fieldinfo = si.fields;
|
||
for (fi != nil) {
|
||
let fn_: str = fi.fname;
|
||
if (syntax.streq(fn_, fname)) {
|
||
// Tagged-union field: delegate to the shared
|
||
// widening writer (handles str/scalar/struct
|
||
// literal/ident payload + tagged-subset tag
|
||
// remap). For non-BP modes, reload BX first so
|
||
// the widener sees a valid base reg.
|
||
if (istaggedtype(c, fi.tnode)) {
|
||
if (mode == 1) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(srcoff: i64);
|
||
emitline("(BP), BX\n");
|
||
};
|
||
if (mode == 2) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, srcname);
|
||
emitline("(SB), BX\n");
|
||
};
|
||
cgwidentaggedstore(c, fi.tnode.type_: *syntax.tinfo,
|
||
fieldnode.lhs, basereg,
|
||
disp + fi.foff, fi.fsz);
|
||
fi = nil;
|
||
} else {
|
||
// Nested struct-typed structlit value: look up
|
||
// the inner struct's metadata and recurse at the
|
||
// field's offset. Pre-#17/#18 the cgexpr-then-
|
||
// store below would land AX = first qword and
|
||
// the rest silently stayed zero.
|
||
let nested: bool = false;
|
||
if (fieldnode.lhs != nil) {
|
||
if (fieldnode.lhs.kind == syntax.nkind.N_STRUCTLIT) {
|
||
if (fi.tnode != nil) {
|
||
if (fi.tnode.kind == syntax.nkind.N_TNAME) {
|
||
if (aliasprimsize(c, fi.tnode.str) == 0) {
|
||
let isi: *structinfo = structlookup(c, fi.tnode.str);
|
||
if (isi != nil) {
|
||
cgstructlitfill(c, isi,
|
||
fieldnode.lhs,
|
||
mode, srcoff, srcname,
|
||
disp + fi.foff);
|
||
nested = true;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
// Nested struct-typed CALL value (#20). cgexpr
|
||
// leaves AX=bytes[0..7], DX=bytes[8..15], CX=
|
||
// bytes[16..23] per #4's cgreturn ABI. Pre-#20
|
||
// the cgexpr-then-AX-store fallthrough below
|
||
// silently dropped past the first qword for any
|
||
// fsz > 8 (only AX got stored).
|
||
//
|
||
// Sized stores: MOVQ for full 8B chunks plus a
|
||
// sized tail (MOVL/MOVW/MOVB) by `tail = fsz%8`.
|
||
// Mirror of cstage cg_structlit_fill's #20 branch.
|
||
// MOVW-for-tail==2 only fires on shapes that
|
||
// didn't compile before, so no #13 byte-identity
|
||
// concern.
|
||
//
|
||
// Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches
|
||
// #4's cgreturn ABI: >24B falls through (sret
|
||
// deferred); fsz%8 ∈ {3,5,6,7} would need shift-
|
||
// store and is also unsupported by #4 — falls
|
||
// through to the existing AX-only wrongness
|
||
// (consistent, tracked as follow-up).
|
||
//
|
||
// INVARIANT: between cgexpr(N_CALL) and the
|
||
// AX/DX/CX stores below, NO instruction may touch
|
||
// AX/DX/CX. The BX reload is safe; any other
|
||
// emission added here will silently corrupt the
|
||
// return value.
|
||
let callwhole: bool = false;
|
||
if (!nested) {
|
||
if (fieldnode.lhs != nil) {
|
||
if (fieldnode.lhs.kind == syntax.nkind.N_CALL) {
|
||
if (fi.tnode != nil) {
|
||
if (fi.tnode.kind == syntax.nkind.N_TNAME) {
|
||
if (aliasprimsize(c, fi.tnode.str) == 0) {
|
||
let csi: *structinfo = structlookup(c, fi.tnode.str);
|
||
if (csi != nil) {
|
||
// Inner struct's ABI size
|
||
// (maxalign-rounded) — cstage
|
||
// reads fl->type->size at the
|
||
// nested-call branch
|
||
// (cgen.c:2121); check.c:760
|
||
// sets that to the
|
||
// maxalign-rounded ABI extent
|
||
// (NOT natural). fi.fsz is
|
||
// wwstage's slot-padded
|
||
// totsize (round-to-8); the
|
||
// pre-#169 structnaturalsize
|
||
// shorts struct{i64,i32}
|
||
// (natural 12, ABI 16) to
|
||
// MOVQ+MOVL where cstage
|
||
// writes MOVQ+MOVQ.
|
||
let cfsz: i32 = structabisize(csi);
|
||
let crem: i32 = cfsz - (cfsz / 8) * 8;
|
||
if (cfsz <= 24) {
|
||
if (crem == 0 || crem == 1
|
||
|| crem == 2 || crem == 4) {
|
||
cgexpr(c, fieldnode.lhs);
|
||
if (mode == 1) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(srcoff: i64);
|
||
emitline("(BP), BX\n");
|
||
};
|
||
if (mode == 2) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, srcname);
|
||
emitline("(SB), BX\n");
|
||
};
|
||
let full: i32 = cfsz / 8;
|
||
let ci: i32 = 0;
|
||
for (ci < full) {
|
||
let r: str = "AX";
|
||
if (ci == 1) { r = "DX"; };
|
||
if (ci == 2) { r = "CX"; };
|
||
emitline("\tMOVQ\t");
|
||
emitline(r);
|
||
emitline(", ");
|
||
if (mode == 0) {
|
||
emitoff((disp + fi.foff + ci * 8): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg((disp + fi.foff + ci * 8): i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
ci += 1;
|
||
};
|
||
if (crem > 0) {
|
||
let top: str = "MOVB";
|
||
if (crem == 4) { top = "MOVL"; };
|
||
if (crem == 2) { top = "MOVW"; };
|
||
let tr: str = "AX";
|
||
if (full == 1) { tr = "DX"; };
|
||
if (full == 2) { tr = "CX"; };
|
||
emitline("\t");
|
||
emitline(top);
|
||
emitline("\t");
|
||
emitline(tr);
|
||
emitline(", ");
|
||
if (mode == 0) {
|
||
emitoff((disp + fi.foff + full * 8): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg((disp + fi.foff + full * 8): i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
};
|
||
callwhole = true;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
if (nested) {
|
||
fi = nil;
|
||
} else if (callwhole) {
|
||
fi = nil;
|
||
} else if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) {
|
||
// str IS []u8: 3-word field (ptr,len,cap).
|
||
// cgexpr leaves AX/BX/CX; for non-BP modes
|
||
// the dst base goes in DX to dodge BX=len /
|
||
// CX=cap (the generic store reloads BX, which
|
||
// would clobber len) (#1/Phase 3). A slice is
|
||
// the same 24B {ptr,len,cap} shape, so it rides
|
||
// this arm; without it the generic scalar tail
|
||
// stored only the ptr word (#24).
|
||
cgexpr(c, fieldnode.lhs);
|
||
if (mode == 0) {
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff((disp + fi.foff): i64);
|
||
emitline("(BP)\n");
|
||
emitline("\tMOVQ\tBX, ");
|
||
emitoff((disp + fi.foff + 8): i64);
|
||
emitline("(BP)\n");
|
||
emitline("\tMOVQ\tCX, ");
|
||
emitoff((disp + fi.foff + 16): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
if (mode == 1) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(srcoff: i64);
|
||
emitline("(BP), DX\n");
|
||
} else {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, srcname);
|
||
emitline("(SB), DX\n");
|
||
};
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitdispreg((disp + fi.foff): i64, "DX");
|
||
emitline("\n");
|
||
emitline("\tMOVQ\tBX, ");
|
||
emitdispreg((disp + fi.foff + 8): i64, "DX");
|
||
emitline("\n");
|
||
emitline("\tMOVQ\tCX, ");
|
||
emitdispreg((disp + fi.foff + 16): i64, "DX");
|
||
emitline("\n");
|
||
};
|
||
fi = nil;
|
||
} else if (fi.tnode != nil
|
||
&& fi.tnode.kind == syntax.nkind.N_TARRAY
|
||
&& fieldnode.lhs != nil
|
||
&& fieldnode.lhs.kind == syntax.nkind.N_ARRLIT) {
|
||
// #249: array field from an N_ARRLIT. Without this
|
||
// the generic tail below cgexprs the N_ARRLIT (→ AX)
|
||
// and stores one sized word, silently DROPPING every
|
||
// element. Store element-wise at disp+foff+i*esz,
|
||
// mirroring the N_LET array-init path
|
||
// (cgenstmt.ww:1393). int/float elements only;
|
||
// str/slice/struct/tagged elements are the N_LET
|
||
// multi-word gap — loud rule-7 error (cstage
|
||
// cg_structlit_fill twin).
|
||
let elemn: *syntax.node = fi.tnode.lhs;
|
||
let isstrel: bool = isstrtype(c, elemn);
|
||
let istagel: bool = istaggedtype(c, elemn);
|
||
let issliceel: bool = isslicetype(c, elemn);
|
||
// #100: key the loud gate off the CHASED stamped
|
||
// tinfo (tichase, the tnodeisagg discipline), not
|
||
// the raw tnode — a bare N_TSLICE kind test /
|
||
// structlookup leaf-name probe is alias-blind, so
|
||
// `type el = el0;` bypassed the gate and fell to
|
||
// the scalar tail (silent wrong, ken kb5_fill2).
|
||
// Twin of cstage cg_structlit_fill's
|
||
// type_chase_named key (cgen.c:3179, B5-c1).
|
||
let isstructel: bool = false;
|
||
if (elemn != nil) {
|
||
let eu: *syntax.tinfo = tichase(elemn.type_: *syntax.tinfo);
|
||
if (eu != nil) {
|
||
if (eu.kind == syntax.tykind.TY_STRUCT) {
|
||
isstructel = true;
|
||
};
|
||
};
|
||
};
|
||
if (isstrel || issliceel || isstructel || istagel) {
|
||
let e1: str = "ww: struct-literal array field '";
|
||
os.write(2, e1.ptr, e1.len: u64);
|
||
os.write(2, fname.ptr, fname.len: u64);
|
||
let e2: str = "' has a str/slice/struct/tagged element — multi-word element store out of #249 scope (N_LET array-init gap)\n";
|
||
os.write(2, e2.ptr, e2.len: u64);
|
||
os.exit(1);
|
||
};
|
||
let esz: i32 = 8;
|
||
if (elemn != nil) {
|
||
if (elemn.kind == syntax.nkind.N_TNAME) {
|
||
let ps: i32 = aliasprimsize(c, elemn.str);
|
||
if (ps > 0) { esz = ps; };
|
||
};
|
||
};
|
||
let mop: str = tnodestoreop(c, elemn, esz);
|
||
let isfloatel: bool = isfloattype(c, elemn);
|
||
let fmov: str = "MOVSD";
|
||
if (isf32type(c, elemn)) { fmov = "MOVSS"; };
|
||
let idx: i32 = 0;
|
||
let repeat: bool = false;
|
||
let e: *syntax.node = fieldnode.lhs.list;
|
||
for (e != nil) {
|
||
let isellip: bool = false;
|
||
if (e.kind == syntax.nkind.N_FIELD) {
|
||
if (syntax.streq(e.str, "...")) {
|
||
repeat = true;
|
||
isellip = true;
|
||
};
|
||
};
|
||
if (isellip) {
|
||
e = nil;
|
||
} else {
|
||
cgexpr(c, e);
|
||
if (mode == 1) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(srcoff: i64);
|
||
emitline("(BP), BX\n");
|
||
};
|
||
if (mode == 2) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, srcname);
|
||
emitline("(SB), BX\n");
|
||
};
|
||
let eoff: i32 = disp + fi.foff + idx * esz;
|
||
if (isfloatel) {
|
||
emitline("\t");
|
||
emitline(fmov);
|
||
emitline("\tX0, ");
|
||
} else {
|
||
emitline("\t");
|
||
emitline(mop);
|
||
emitline("\tAX, ");
|
||
};
|
||
if (mode == 0) {
|
||
emitoff(eoff: i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg(eoff: i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
idx += 1;
|
||
e = e.next;
|
||
};
|
||
};
|
||
if (repeat) {
|
||
let total: i32 = idx;
|
||
if (fi.tnode.rhs != nil) {
|
||
if (fi.tnode.rhs.kind == syntax.nkind.N_INTLIT) {
|
||
total = fi.tnode.rhs.uval: i32;
|
||
};
|
||
};
|
||
for (idx < total) {
|
||
if (mode == 1) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(srcoff: i64);
|
||
emitline("(BP), BX\n");
|
||
};
|
||
if (mode == 2) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, srcname);
|
||
emitline("(SB), BX\n");
|
||
};
|
||
let eoff: i32 = disp + fi.foff + idx * esz;
|
||
if (isfloatel) {
|
||
emitline("\t");
|
||
emitline(fmov);
|
||
emitline("\tX0, ");
|
||
} else {
|
||
emitline("\t");
|
||
emitline(mop);
|
||
emitline("\tAX, ");
|
||
};
|
||
if (mode == 0) {
|
||
emitoff(eoff: i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg(eoff: i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
idx += 1;
|
||
};
|
||
};
|
||
fi = nil;
|
||
} else if (tnodeisagg(fi.tnode)) {
|
||
// #49 (f38b/x5f-h): an aggregate (struct/array/
|
||
// tuple) field from an ADDRESSABLE source expr —
|
||
// `outer{.., r = r}` — fell to the scalar tail
|
||
// below and stored word0 only. Funnel: source
|
||
// address via aggargsrcaddr (SI), field address
|
||
// via LEAQ/ADDQ (BX — loaded AFTER the source
|
||
// walk, which clobbers BX/AX), then aggcopy.
|
||
// Width = the checker-STAMPED tinfo size (the
|
||
// cstage fl->type->size SSoT; fi.fsz is the
|
||
// slot-padded extent and skews on maxalign<8).
|
||
// Non-addressable aggregate sources (tuple-lit,
|
||
// >24B/odd-tail call) die loud — pre-#49 the
|
||
// same silent word0 (rule 7). Mirror of cstage
|
||
// cg_structlit_fill #49 arm.
|
||
if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) {
|
||
let m49g: str = "structlit fill: aggregate field '";
|
||
os.write(2, m49g.ptr, m49g.len: u64);
|
||
os.write(2, fname.ptr, fname.len: u64);
|
||
let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n";
|
||
os.write(2, m49h.ptr, m49h.len: u64);
|
||
os.exit(1);
|
||
};
|
||
if (mode == 0) {
|
||
emitline("\tLEAQ\t");
|
||
emitoff((disp + fi.foff): i64);
|
||
emitline("(BP), BX\n");
|
||
} else {
|
||
if (mode == 1) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(srcoff: i64);
|
||
emitline("(BP), BX\n");
|
||
} else {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, srcname);
|
||
emitline("(SB), BX\n");
|
||
};
|
||
if (disp + fi.foff != 0) {
|
||
emitline("\tADDQ\t$");
|
||
emitint((disp + fi.foff): i64);
|
||
emitline(", BX\n");
|
||
};
|
||
};
|
||
let agsz49: i32 = 0;
|
||
if (fi.tnode != nil) {
|
||
let agti49: *syntax.tinfo = fi.tnode.type_: *syntax.tinfo;
|
||
agti49 = tichase(agti49);
|
||
if (agti49 != nil) { agsz49 = agti49.size: i32; };
|
||
};
|
||
aggcopy(c, agsz49);
|
||
fi = nil;
|
||
} else {
|
||
cgexpr(c, fieldnode.lhs);
|
||
// For non-BP modes, cgexpr just clobbered
|
||
// BX; reload it before the store.
|
||
if (mode == 1) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff(srcoff: i64);
|
||
emitline("(BP), BX\n");
|
||
};
|
||
if (mode == 2) {
|
||
emitline("\tLEAQ\t");
|
||
emitsymname(c, srcname);
|
||
emitline("(SB), BX\n");
|
||
};
|
||
if (isfloattype(c, fi.tnode)) {
|
||
let mov: str = "MOVSD";
|
||
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
|
||
emitline("\t");
|
||
emitline(mov);
|
||
emitline("\tX0, ");
|
||
if (mode == 0) {
|
||
emitoff((disp + fi.foff): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg((disp + fi.foff): i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
fi = nil;
|
||
} else {
|
||
// Explicit {1→MOVB, 4→MOVL, else MOVQ}
|
||
// dispatch (not fieldstoreop) to match
|
||
// cstage byte-identically. wwstage's
|
||
// fieldstoreop would return MOVW for
|
||
// fsz==2 which cstage doesn't emit —
|
||
// tracked as task #13.
|
||
let fsz: i32 = fi.fsz;
|
||
let op: str = "MOVQ";
|
||
if (fsz == 1) { op = "MOVB"; };
|
||
if (fsz == 4) { op = "MOVL"; };
|
||
emitline("\t");
|
||
emitline(op);
|
||
emitline("\tAX, ");
|
||
if (mode == 0) {
|
||
emitoff((disp + fi.foff): i64);
|
||
emitline("(BP)\n");
|
||
} else {
|
||
emitdispreg((disp + fi.foff): i64, basereg);
|
||
emitline("\n");
|
||
};
|
||
fi = nil;
|
||
};
|
||
};
|
||
};
|
||
} else {
|
||
fi = fi.finext;
|
||
};
|
||
};
|
||
};
|
||
fieldnode = fieldnode.next;
|
||
};
|
||
};
|
||
|
||
// Thin wrapper preserving the BP-rel call shape used by cglet,
|
||
// cgreturn, cgassign N_IDENT-lhs N_STRUCTLIT, and the C1.25
|
||
// @placescr materialise (cg_structlit_fill_bp's named twin).
|
||
fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *syntax.node, bpoff: i32) void = {
|
||
if (si == nil) { return; };
|
||
cgstructlitfill(c, si, lit, 0, 0, "", bpoff);
|
||
};
|