One class: alias-blind base+esz at the array/slice ELEMENT paths — index read/write, slice-expr, for-range, and literal-init store. The wwstage cgen derived element size and base addressing from the type-AST tnode; an alias-typed base (`type arr = [4]int; let a: arr`) shows only the N_TNAME leaf, so esz fell to a sentinel (1 on the read side, 8 on the init-store side) and the base classified as a POINTER (MOVQ of array words, no IMULQ): m8b_idx1/range1 SEGV 139, m8b_slice1 silent-wrong past little-endian prefix-luck (m8c_slice1big exit 2), m7c global [2]row read SEGV via the alias-blind element-is-array classify, and (#79, ken F2a1 oracle) `type A=[4]u32; let a:A=[...]` stored MOVQ stride-8 over a stride-4 slot — elements 2/3 landed at 0(BP)/+8(BP), a saved-BP/RIP smash masked whenever esz==8. cstage reads everything off the chased stamped type (type_chase_named/ idx_eff, correct post-F1), so every fixed shape graduates ww-SEGV/silent-wrong -> 0/0 byte-id. New tichase() in cgenutil.ww: nil-passthrough transitive TY_NAMED peel, exact twin of cmd/wcc/type.c:160-162. Routed sites, all gated on the stamped type being TY_NAMED (non-alias paths byte-identical): - cgindex (cgenexpr.ww): elem facts (esz/signed/float/f32) off tichase(n.type_); etn falls back to n for the tagged/str/slice classify; LEAQ-vs-MOVQ base off the chased kind; elem-is-array supplemented by tinfoisarray(n.type_) for alias ELEMENTS (m7c). - cgassign N_INDEX store + compound arms (cgenexpr.ww): esz + elemtn=lhs (the stamped-element idiom of the N_DOT/N_INDEX arms); chased-kind base classify at all four LEAQ/MOVQ sites. - cgslice + cgbasecap (cgenexpr.ww): esz, base classify, default-hi (TY_ARRAY -> $alen / TY_SLICE|TY_STR -> +8 len), cap word at +16; global-str cap keeps the #73 carve-out. - cgforrange (cgenstmt.ww, cross-file leg: the range pin cannot green without it): esz, element-node synthesis off .sub (FC0 precedent), isarr/isslicestr classify, alen off the chased tinfo. - cgarrlitfillbp (cgenstmt.ww, #79): an alias [count]T arrtn is the N_TNAME leaf (elemn nil) — synthesise the element node off the chased sub so the existing prim/agg/slice/tagged/narrow dispatch works unchanged; `...` repeat bound off the chased alen (cstage cg_arrlit_fill_bp receives the pre-chased bu and reads bu->alen). #8-PAIR COVERAGE: this is the STORE half of #8's two size-sources. The elemsizeofc READ half chases the ELEMENT internally (idxeffti + esub peel, the #8 fix) but NOT an alias-typed INDEXABLE node — that leg is covered at its #60-family call sites by the gates above (cgindex/cgslice/store/compound/cgforrange/pusharg). Remaining alias-blind elemsizeofc callers are enumerated as residuals below. - bare-let classify (cgenstmt.ww, #79 rider): `let a: arrk;` with an alias-to-array type took the composite zero-fill cstage doesn't emit (cstage keys the no-init shape on the chased lu->kind: arrays keep the per-index-write contract; an 8B alias-array still falls to the single MOVQ $0 arm). Required for the loopfill_1024 pin's byte-id; closes the array kind of the uninit-alias divergence. - pusharg N_SLICE (cgenutil.ww, pulled in by the same pin rule: the 944 slice_of_alias_arg row is a distinct lowering from cgslice): esz, base classify, default-hi. Tests: new 944_alias_idx_family_run (19 rows: idx/slice/range/init controls + 1-level + 2-level + decl-order permutations + index store + compound (+=, *=) + #79 [4]u32 literal-init + alias `[v...]` repeat + uninit [1024] loop-fill + slice1big (1000 elems, values >255, LAST-element readback, default-hi, .cap, range count) + re-slice of an alias slice + range over an alias slice + m7c global 2D + GLOBAL alias-slice indexed read + slice-as-call-arg; dual-stage run + per-row byte-id; LAST elements asserted throughout). The six 944_alias_accept_run rows citing "#60 (F2 batch 1)" flip K_RUN_CS -> K_RUN (incl. slicefield_wholeread_2lvl: its 738d7f4-era receive-spine divergence no longer reproduces at the F1-merged base, verified byte-id + 0/0). 989_lib_byteid checked: no DIVERGE entry graduates (the test fails loudly on graduation; lib has no alias-base consumers — the shape SEGVed before this fix). NOT pinned (g-fold territory, #77/#78): direct alias-typed global ARRAY rows. Expected state probe-verified UNCHANGED by this diff: `let g: arr = [...]` -> ww link-ERR (no DATA emitted), cs 1-level runs 0, cs 2-level runs WRONG (silent). The alias-GLOBAL base legs added here (isglobalarr reclassify, global default-hi/cap) are cs-aligned but runtime-unreachable until the DATA emit lands. Residuals filed with the team: alias-blind elemsizeofc callers not in the #60 pin family — cgun &a[i] addr-of (cgenexpr.ww:4638 region, task #82), append() on an alias-typed slice local (:5287), `alloc([], n)` into an alias-slice let (cgenstmt.ww:2159), arr[i].field= float store (:8536); tagged-element READ under an alias base keeps the ident-arm nullable semantics; checker asserttyped on `untyped_lit * rangevar` over an alias slice (pre-existing, check.ww is batch 4, task #80); uninit alias-to-STRUCT zero-fill unchanged (correct: cstage fills composites); range-destructure over alias-to-tuple-slice. selfhost/cmd/{w6c,wwdump}/main.combined.ww regenerated (cgen*.ww are embedded sources).
4898 lines
192 KiB
Plaintext
4898 lines
192 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 ast;
|
||
import tok;
|
||
import typ;
|
||
import sym;
|
||
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: *node) *node = {
|
||
let s: *node = newnode(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: *node, nfixed_out: *i32) *node = {
|
||
*nfixed_out = 0;
|
||
let p: *node = ps;
|
||
for (p != nil) {
|
||
if (p.kind == nkind.N_PARAM) {
|
||
if (p.op == 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: *node, nfixed_out: *i32) *node = {
|
||
*nfixed_out = 0;
|
||
if (callee == nil) { return nil; };
|
||
let ps: *node = nil;
|
||
if (callee.kind == nkind.N_IDENT) {
|
||
if (callee.str.len == 0) { return nil; };
|
||
ps = fnparamslookup(c, callee.str);
|
||
} else { if (callee.kind == 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 == nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
ps = fnparamslookupmod(c, callee.str, cmod);
|
||
}; };
|
||
return findvariadicparam(ps, nfixed_out);
|
||
};
|
||
|
||
// ---- 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).
|
||
fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
|
||
if (arg == nil) { return 0; };
|
||
let nextparam: *node = nil;
|
||
if (param != nil) { nextparam = param.next; };
|
||
let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase);
|
||
// 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: *node = nil;
|
||
if (param != nil) { if (param.kind == nkind.N_PARAM) {
|
||
if (param.op != tkind.TK_ELLIPSIS) {
|
||
memptype = param.lhs;
|
||
if (memptype != nil) {
|
||
memsz = taggedmemargsize(memptype.type_: *tinfo);
|
||
};
|
||
};
|
||
}; };
|
||
if (memsz == 0) {
|
||
memsz = taggedmemargsize(arg.type_: *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: *tinfo = arg.type_: *tinfo;
|
||
if (memptype != nil) {
|
||
let pt: *tinfo = memptype.type_: *tinfo;
|
||
let pu: *tinfo = pt;
|
||
for (pu != nil && pu.kind == tykind.TY_NAMED) {
|
||
pu = pu.under;
|
||
};
|
||
let au: *tinfo = at;
|
||
for (au != nil && au.kind == tykind.TY_NAMED) {
|
||
au = au.under;
|
||
};
|
||
if (pu != nil && pu == au) { same = true; };
|
||
if (!same && pt != nil && at != nil) {
|
||
if (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_: *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 == 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 == 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 == 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 == tkind.TK_ELLIPSIS) {
|
||
widensz = 0;
|
||
} else {
|
||
let ptype: *node = param.lhs;
|
||
if (istaggedtype(c, ptype)) {
|
||
let aistagged: bool = false;
|
||
if (arg.kind == nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, arg.str);
|
||
if (lc != nil) {
|
||
aistagged = istaggedtype(c, lc.tnode);
|
||
};
|
||
};
|
||
// #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 == 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 == 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 == nkind.N_UN && arg.op == tkind.TK_STAR) {
|
||
if (istaggedtype(c, arg)) {
|
||
if (slotsize(c, arg) == slotsize(c, ptype)) {
|
||
aistagged = true;
|
||
};
|
||
};
|
||
};
|
||
if (!aistagged) {
|
||
widensz = slotsize(c, ptype);
|
||
let tagged: *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: *tinfo = arg.type_: *tinfo;
|
||
for (argtup != nil && argtup.kind == tykind.TY_NAMED) {
|
||
argtup = argtup.under;
|
||
};
|
||
let argistuple: bool = false;
|
||
if (argtup != nil) {
|
||
if (argtup.kind == tykind.TY_TUPLE) {
|
||
argistuple = true;
|
||
};
|
||
};
|
||
let pname: str = rhsstructpayload(c, arg);
|
||
if (pname.len > 0 || argistuple) {
|
||
let ptype: *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_: *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;
|
||
};
|
||
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 == nkind.N_SLICE) {
|
||
let base: *node = arg.lhs;
|
||
let lo: *node = arg.rhs;
|
||
let hi: *node = arg.cond;
|
||
let baselocal: *local = nil;
|
||
let globaltn: *node = nil;
|
||
let globalname: str;
|
||
globalname.ptr = nil; globalname.len = 0;
|
||
if (base != nil) {
|
||
if (base.kind == nkind.N_IDENT) {
|
||
let bn: str = base.str;
|
||
baselocal = localfindnode(c, bn);
|
||
if (baselocal == nil) {
|
||
let gt: *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: *tinfo = nil;
|
||
if (base != nil) { if (base.kind == nkind.N_DOT) {
|
||
dotbu = base.type_: *tinfo;
|
||
for (dotbu != nil && dotbu.kind == tykind.TY_NAMED) {
|
||
dotbu = dotbu.under;
|
||
};
|
||
};};
|
||
// #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: *tinfo = nil;
|
||
if (base != nil) { if (base.kind == nkind.N_IDENT) {
|
||
let bt60: *tinfo = base.type_: *tinfo;
|
||
if (bt60 != nil) { if (bt60.kind == 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: *tinfo = tichase(bu60.sub);
|
||
if (es60 != nil) { esz = es60.size: i32; };
|
||
};
|
||
// base address → push
|
||
if (baselocal != nil) {
|
||
let tn: *node = baselocal.tnode;
|
||
let isarr60: bool = false;
|
||
if (tn != nil) {
|
||
if (tn.kind == nkind.N_TARRAY) { isarr60 = true; };
|
||
};
|
||
// #60: alias-NAMED base — chased kind (see cgindex twin).
|
||
if (bu60 != nil) { isarr60 = bu60.kind == 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 == nkind.N_TARRAY;
|
||
// #60: alias-NAMED base — chased kind; runtime-
|
||
// unreachable until #77/#78 global DATA.
|
||
if (bu60 != nil) { gisarr60 = bu60.kind == 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: *node = baselocal.tnode;
|
||
if (tn != nil) {
|
||
if (tn.kind == nkind.N_TARRAY) {
|
||
let lenn: *node = tn.rhs;
|
||
if (lenn != nil) {
|
||
if (lenn.kind == nkind.N_INTLIT) {
|
||
emitline("\tMOVQ\t$");
|
||
emituint(lenn.uval);
|
||
emitline(", AX\n");
|
||
};
|
||
};
|
||
} else { if (tn.kind == nkind.N_TSLICE) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((baselocal.off + 8): i64);
|
||
emitline("(BP), AX\n");
|
||
} else { if (tn.kind == nkind.N_TNAME) {
|
||
if (streq(tn.str, "str")) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((baselocal.off + 8): i64);
|
||
emitline("(BP), AX\n");
|
||
};
|
||
};};};
|
||
};
|
||
// #60: alias-NAMED base default-hi — chased kind
|
||
// (see the cgslice twin).
|
||
if (bu60 != nil) {
|
||
if (bu60.kind == tykind.TY_ARRAY) {
|
||
emitline("\tMOVQ\t$");
|
||
emitint(bu60.alen: i64);
|
||
emitline(", AX\n");
|
||
} else { if (bu60.kind == tykind.TY_SLICE
|
||
|| bu60.kind == tykind.TY_STR) {
|
||
emitline("\tMOVQ\t");
|
||
emitoff((baselocal.off + 8): i64);
|
||
emitline("(BP), AX\n");
|
||
};};
|
||
};
|
||
} else { if (globaltn != nil) {
|
||
if (globaltn.kind == nkind.N_TARRAY) {
|
||
let lenn: *node = globaltn.rhs;
|
||
if (lenn != nil) {
|
||
if (lenn.kind == nkind.N_INTLIT) {
|
||
emitline("\tMOVQ\t$");
|
||
emituint(lenn.uval);
|
||
emitline(", AX\n");
|
||
};
|
||
};
|
||
} else { if (globaltn.kind == nkind.N_TSLICE) {
|
||
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 == tykind.TY_ARRAY) {
|
||
emitline("\tMOVQ\t$");
|
||
emitint(bu60.alen: i64);
|
||
emitline(", AX\n");
|
||
} else { if (bu60.kind == tykind.TY_SLICE
|
||
|| bu60.kind == 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 == 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;
|
||
};
|
||
};
|
||
};
|
||
// #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_: *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 == nkind.N_IDENT) {
|
||
let st: *tinfo = arg.type_: *tinfo;
|
||
for (st != nil && st.kind == tykind.TY_NAMED) {
|
||
st = st.under;
|
||
};
|
||
if (st != nil) { if (st.kind == 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 == 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: *tinfo = arg.type_: *tinfo;
|
||
if (typeisf32(at)) { fk = 1; }
|
||
else { if (typeisfloat(at)) { fk = 2; }; };
|
||
};
|
||
if (fk != 0) {
|
||
cgexpr(c, arg);
|
||
let mov: str = "MOVSD";
|
||
if (fk == 1) { mov = "MOVSS"; };
|
||
emitline("\tSUBQ\t$8, SP\n");
|
||
emitline("\t");
|
||
emitline(mov);
|
||
emitline("\tX0, (SP)\n");
|
||
return rest + 1;
|
||
};
|
||
cgexpr(c, arg);
|
||
// #163/#32 (C-t2): tuple ARG (param twin of #164's return). cgexpr
|
||
// 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. An N_TUPLE literal's elements are VALUE exprs —
|
||
// classify them the way cgtuplelittocursor does (nodeisstr/
|
||
// nodeisslice), kind-discriminated; the stride is the slot formula
|
||
// (wide ? header : 8), the same number slotsize() gives a type node.
|
||
let tuparg: *node = nodetuplearg(c, arg);
|
||
if (tuparg != nil) {
|
||
let tuplit: bool = tuparg.kind == nkind.N_TUPLE;
|
||
let gptot: i32 = 0;
|
||
let sstot: i32 = 0;
|
||
let tsz: i32 = 0;
|
||
let p: *node = tuparg.list;
|
||
for (p != nil) {
|
||
let et: *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: *tinfo = et.type_: *tinfo;
|
||
for (eti != nil && eti.kind == tykind.TY_NAMED) {
|
||
eti = eti.under;
|
||
};
|
||
if (eti != nil) {
|
||
if (eti.kind == tykind.TY_TUPLE
|
||
|| eti.kind == tykind.TY_STRUCT
|
||
|| eti.kind == tykind.TY_ARRAY
|
||
|| eti.kind == tykind.TY_TAGGED) {
|
||
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);
|
||
};
|
||
};
|
||
// tagged is guarded loud above, so wide-vs-scalar is
|
||
// the full slot split here; eslot keeps the stride
|
||
// arithmetic on the accessor scale (#22).
|
||
let wide: bool = false;
|
||
if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); }
|
||
else { wide = isstrtype(c, et) || isslicetype(c, et); };
|
||
let eslot: i32 = 8;
|
||
if (wide) { eslot = tyslicesize(): i32; };
|
||
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: *node = p.lhs;
|
||
if (tuplit) { et = p; };
|
||
let wide: bool = false;
|
||
if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); }
|
||
else { wide = isstrtype(c, et) || isslicetype(c, et); };
|
||
let eslot: i32 = 8;
|
||
if (wide) { eslot = tyslicesize(): i32; };
|
||
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: *tinfo = arg.type_: *tinfo;
|
||
for (ati != nil && ati.kind == tykind.TY_NAMED) {
|
||
ati = ati.under;
|
||
};
|
||
if (ati != nil) {
|
||
if (ati.kind == 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 == nkind.N_INDEX || arg.kind == nkind.N_DOT
|
||
|| (arg.kind == nkind.N_UN && arg.op == 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: *node) i32 = {
|
||
if (n == nil) { return 0; };
|
||
if (n.kind != nkind.N_CALL) { return 0; };
|
||
let callee: *node = n.lhs;
|
||
if (callee == nil) { return 0; };
|
||
if (callee.kind != nkind.N_IDENT) { return 0; };
|
||
let rtyp: *node = fnretlookup(c, callee.str);
|
||
if (!istaggedtype(c, rtyp)) { return 0; };
|
||
return slotsize(c, rtyp);
|
||
};
|
||
|
||
fn nodeisslice(c: *cgen, n: *node) bool = {
|
||
if (n == nil) { return false; };
|
||
let k: nkind = n.kind;
|
||
if (k == nkind.N_IDENT) {
|
||
let nm: str = n.str;
|
||
let lc: *local = localfindnode(c, nm);
|
||
if (lc != nil) { return isslicetype(c, lc.tnode); };
|
||
return false;
|
||
};
|
||
if (k == nkind.N_SLICE) { return true; };
|
||
if (k == 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 == nkind.N_CALL) {
|
||
let callee: *node = n.lhs;
|
||
if (callee != nil) {
|
||
if (callee.kind == nkind.N_IDENT) {
|
||
let rtyp: *node = fnretlookupmod(c, callee.str, c.curmod);
|
||
return isslicetype(c, rtyp);
|
||
};
|
||
if (callee.kind == nkind.N_DOT) {
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
let rtyp: *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 == nkind.N_DOT) {
|
||
return typeisslice(n.type_: *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).
|
||
//
|
||
// TODO(#11): every consumer of "is-str" here reconstructs the answer
|
||
// from raw N_kind because wwstage has no typed AST. Each new expression
|
||
// shape needs an explicit arm or it silently falls through to false,
|
||
// which downstream drops the second slot (BX/len) at the call site.
|
||
// A typed AST check (cstage reads n->type) would replace this whole
|
||
// function. Covered arms below: N_STRLIT, N_IDENT (local/let-typed),
|
||
// N_CALL (return type), N_INDEX (element type of [N]T / []T / *T base),
|
||
// N_DOT (reads checker-stamped 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: *node) bool = {
|
||
if (n == nil) { return false; };
|
||
let k: nkind = n.kind;
|
||
if (k == nkind.N_STRLIT) { return true; };
|
||
if (k == nkind.N_IDENT) {
|
||
let nm: str = n.str;
|
||
let lc: *local = localfindnode(c, nm);
|
||
if (lc != nil) {
|
||
// Use isstrtype so `!str` aliases (parserr = !str) and
|
||
// `type foo = str;` chains resolve through. The bare
|
||
// `streq("str", ...)` test missed them and dropped the
|
||
// MOVQ BX,CX shuffle on returns of str-aliased locals.
|
||
if (isstrtype(c, lc.tnode)) { return true; };
|
||
};
|
||
return false;
|
||
};
|
||
if (k == nkind.N_CALL) {
|
||
let callee: *node = n.lhs;
|
||
if (callee != nil) {
|
||
if (callee.kind == nkind.N_IDENT) {
|
||
let rtyp: *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 == nkind.N_DOT) {
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
let rtyp: *node = fnretlookupmod(c, callee.str, cmod);
|
||
return isstrtype(c, rtyp);
|
||
};
|
||
};
|
||
return false;
|
||
};
|
||
// N_INDEX: `arr[i]` whose base is an indexable type carrying a
|
||
// str element. cgindex correctly loads (AX=ptr, BX=len) for a
|
||
// 16B element; without this arm pushargsrev only pushes AX and
|
||
// the call-arg pop reads .len from stack residue. Mirror of
|
||
// cstage's node_isstr → type_isstr(n->type), where n->type is
|
||
// the resolved element type after check.
|
||
if (k == nkind.N_INDEX) {
|
||
let base: *node = n.lhs;
|
||
if (base != nil) {
|
||
if (base.kind == nkind.N_IDENT) {
|
||
let bt: *node = nil;
|
||
let lc: *local = localfindnode(c, base.str);
|
||
if (lc != nil) { bt = lc.tnode; }
|
||
else { bt = letvartnode(c, base.str); };
|
||
if (bt != nil) {
|
||
// idxelemtn: `*[N]T` drills to the pointee
|
||
// array's element (#61).
|
||
let elem: *node = idxelemtn(bt);
|
||
if (elem != nil) {
|
||
return isstrtype(c, elem);
|
||
};
|
||
};
|
||
};
|
||
// N_INDEX through a struct field: e.g. cmd.argsptr[i]
|
||
// where argsptr: *str. cgindex correctly loads the
|
||
// (ptr, len) pair off the stamped element size; without
|
||
// this arm pushargsrev would only push AX and lose .len.
|
||
if (base.kind == nkind.N_DOT) {
|
||
let fld: str = base.str;
|
||
if (streq(fld, "ptr")) { return false; };
|
||
if (streq(fld, "len")) { return false; };
|
||
if (streq(fld, "cap")) { return false; };
|
||
let inner: *node = base.lhs;
|
||
if (inner != nil) {
|
||
if (inner.kind == nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, inner.str);
|
||
if (lc != nil) {
|
||
let tn: *node = lc.tnode;
|
||
let sname: str;
|
||
sname.ptr = nil; sname.len = 0;
|
||
if (tn != nil) {
|
||
if (tn.kind == nkind.N_TNAME) { sname = tn.str; };
|
||
if (tn.kind == nkind.N_TPTR) {
|
||
let pinner: *node = tn.lhs;
|
||
if (pinner != nil) {
|
||
if (pinner.kind == nkind.N_TNAME) {
|
||
sname = pinner.str;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
if (sname.len > 0) {
|
||
let si: *structinfo = structlookup(c, sname);
|
||
if (si != nil) {
|
||
let fi: *fieldinfo = si.fields;
|
||
for (fi != nil) {
|
||
if (streq(fi.fname, fld)) {
|
||
let ft: *node = fi.tnode;
|
||
if (ft != nil) {
|
||
// idxelemtn: `*[N]T` drill (#61).
|
||
let elem: *node = idxelemtn(ft);
|
||
if (elem != nil) {
|
||
return isstrtype(c, elem);
|
||
};
|
||
};
|
||
};
|
||
fi = fi.finext;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
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. Cstage
|
||
// cgen.c:168-170 node_isstr = type_isstr(n->type) — same shape.
|
||
// Collapsed per A.6.3h (#56).
|
||
if (k == nkind.N_DOT) {
|
||
return typeisstr(n.type_: *tinfo);
|
||
};
|
||
if (k == 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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
return typeis8byteprim(t.type_: *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: *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: *tinfo = idxeffti(t.type_: *tinfo);
|
||
if (ti == nil) { return false; };
|
||
return 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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
// idxeffti = TY_NAMED peel + the `*[N]T` drill (#61).
|
||
let ti: *tinfo = idxeffti(t.type_: *tinfo);
|
||
if (ti == nil) { return false; };
|
||
return 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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
// idxeffti = TY_NAMED peel + the `*[N]T` drill (#61).
|
||
let ti: *tinfo = idxeffti(t.type_: *tinfo);
|
||
if (ti == nil) { return false; };
|
||
return 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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
let k: nkind = t.kind;
|
||
let elem: *node = nil;
|
||
if (k == nkind.N_TPTR) { elem = t.lhs; };
|
||
if (k == nkind.N_TSLICE) { elem = t.lhs; };
|
||
if (k == nkind.N_TARRAY) { elem = t.lhs; };
|
||
if (elem == nil) { return false; };
|
||
if (k == nkind.N_TPTR) {
|
||
if (elem.kind == nkind.N_TARRAY) {
|
||
if (elem.lhs != nil) { elem = elem.lhs; };
|
||
};
|
||
};
|
||
return elem.kind == 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: *tinfo) *tinfo = {
|
||
let t: *tinfo = t0;
|
||
for (t != nil && t.kind == 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: *tinfo) bool = {
|
||
let u: *tinfo = t;
|
||
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
|
||
if (u == nil) { return false; };
|
||
return u.kind == 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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
return typeissigned(t.type_: *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: *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: *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: *node) str = {
|
||
if (tnode == nil) { return "MOVQ"; };
|
||
let ti: *tinfo = tnode.type_: *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 = 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: *tinfo) *tinfo = {
|
||
let t: *tinfo = t0;
|
||
for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; };
|
||
if (t != nil && t.kind == tykind.TY_PTR) {
|
||
let p: *tinfo = t.sub;
|
||
for (p != nil && p.kind == tykind.TY_NAMED) { p = p.under; };
|
||
if (p != nil && p.kind == 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: *node) *node = {
|
||
if (tn == nil) { return nil; };
|
||
let k: nkind = tn.kind;
|
||
if (k != nkind.N_TARRAY && k != nkind.N_TSLICE
|
||
&& k != nkind.N_TPTR) { return nil; };
|
||
let elem: *node = tn.lhs;
|
||
if (k == nkind.N_TPTR && elem != nil
|
||
&& elem.kind == 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: *node) i32 = {
|
||
if (t == nil) { return 1; };
|
||
let k: nkind = t.kind;
|
||
let elem: *node = nil;
|
||
if (k == nkind.N_TPTR) { elem = t.lhs; };
|
||
if (k == nkind.N_TSLICE) { elem = t.lhs; };
|
||
if (k == nkind.N_TARRAY) { elem = t.lhs; };
|
||
if (k == nkind.N_TNAME) {
|
||
let nm: str = t.str;
|
||
// 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 (streq(nm, "str")) { return primtypesize("u8"): i32; };
|
||
// Indexing a primitive name (rare): element size = the prim.
|
||
let ps: i32 = primsize(nm);
|
||
if (ps > 0) { return ps; };
|
||
return 1;
|
||
};
|
||
if (elem == nil) { return 1; };
|
||
// `*[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 == 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 == nkind.N_TSLICE) { return tyslicesize(): i32; };
|
||
if (elem.kind == nkind.N_TNAME) {
|
||
let nm: str = elem.str;
|
||
// str element is 16B (ptr+len). primsize returns 0 for it.
|
||
if (streq(nm, "str")) { return primtypesize("str"): i32; };
|
||
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: *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: nkind = t.kind;
|
||
let nest: *node = nil;
|
||
if (nk == nkind.N_TSLICE) { nest = t.lhs; };
|
||
if (nk == nkind.N_TARRAY) { nest = t.lhs; };
|
||
if (nest != nil && nest.kind == nkind.N_TARRAY) {
|
||
let eti: *tinfo = nest.type_: *tinfo;
|
||
for (eti != nil && eti.kind == tykind.TY_NAMED) { eti = eti.under; };
|
||
if (eti != nil) { return eti.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: *tinfo = idxeffti(t.type_: *tinfo);
|
||
if (ti != nil) {
|
||
let esub: *tinfo = ti.sub;
|
||
for (esub != nil && esub.kind == tykind.TY_NAMED) { esub = esub.under; };
|
||
if (esub != nil) { return esub.size: i32; };
|
||
};
|
||
let elem: *node = idxelemtn(t);
|
||
if (elem == nil) { return direct; };
|
||
if (elem.kind == nkind.N_TNAME) {
|
||
let ps: i32 = primsize(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: *node) bool = {
|
||
if (n == nil) { return false; };
|
||
let k: nkind = n.kind;
|
||
if (k == nkind.N_IDENT) {
|
||
let nm: str = n.str;
|
||
let lc: *local = localfindnode(c, nm);
|
||
if (lc != nil) {
|
||
if (lc.tnode == nil) { return false; };
|
||
return typeisunsigned(lc.tnode.type_: *tinfo);
|
||
};
|
||
return false;
|
||
};
|
||
if (k == nkind.N_DOT) {
|
||
return typeisunsigned(n.type_: *tinfo);
|
||
};
|
||
if (k == nkind.N_CAST) {
|
||
if (n.rhs == nil) { return false; };
|
||
return typeisunsigned(n.rhs.type_: *tinfo);
|
||
};
|
||
if (k == nkind.N_BIN) {
|
||
if (nodeisunsigned(c, n.lhs)) { return true; };
|
||
return nodeisunsigned(c, n.rhs);
|
||
};
|
||
if (k == nkind.N_UN) { return nodeisunsigned(c, n.lhs); };
|
||
// nkind.N_INDEX: `p[i]` is unsigned iff 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 == nkind.N_INDEX) {
|
||
return typeisunsigned(n.type_: *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 == nkind.N_CALL) {
|
||
return typeisunsigned(n.type_: *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: *node) i32 = {
|
||
if (n == nil) { return 0; };
|
||
let k: nkind = n.kind;
|
||
if (k == nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, n.str);
|
||
if (lc != nil) {
|
||
let tn: *node = lc.tnode;
|
||
if (tn != nil) {
|
||
if (tn.kind == nkind.N_TNAME) { return primsize(tn.str); };
|
||
};
|
||
};
|
||
return 0;
|
||
};
|
||
if (k == nkind.N_CAST) {
|
||
let tn: *node = n.rhs;
|
||
if (tn != nil) {
|
||
if (tn.kind == nkind.N_TNAME) { return primsize(tn.str); };
|
||
};
|
||
return 0;
|
||
};
|
||
if (k == 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: *tinfo = fi.tnode.type_: *tinfo;
|
||
for (ti != nil && ti.kind == tykind.TY_NAMED) {
|
||
ti = ti.under;
|
||
};
|
||
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: *node) *structinfo = {
|
||
if (tn == nil) { return nil; };
|
||
if (tn.kind != nkind.N_TNAME) { return nil; };
|
||
let si: *structinfo = structlookup(c, tn.str);
|
||
if (si != nil) { return si; };
|
||
let cur: *node = tn;
|
||
for (cur != nil && cur.kind == nkind.N_TNAME && si == nil) {
|
||
let aliased: *node = aliaslookup(c, cur.str);
|
||
if (aliased == nil) { cur = nil; }
|
||
else {
|
||
if (aliased.kind == nkind.N_TNAME) {
|
||
si = structlookup(c, aliased.str);
|
||
cur = aliased;
|
||
} else { cur = nil; };
|
||
};
|
||
};
|
||
return si;
|
||
};
|
||
|
||
export fn sretretsize(c: *cgen, t: *node) i32 = {
|
||
if (t == nil) { return 0; };
|
||
let r: *node = t;
|
||
if (r.kind == 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 == 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: *node = r.list;
|
||
for (pt != nil) {
|
||
let et: *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: *tinfo = r.type_: *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 == nkind.N_TARRAY) {
|
||
let ati: *tinfo = r.type_: *tinfo;
|
||
for (ati != nil && ati.kind == tykind.TY_NAMED) { ati = ati.under; };
|
||
if (ati == nil) { return 0; };
|
||
let asz: i32 = ati.size: i32;
|
||
if (asz <= 24) { return 0; };
|
||
return asz;
|
||
};
|
||
if (r.kind != nkind.N_TNAME) { return 0; };
|
||
// Primitives / aliased-to-primitives are never sret.
|
||
if (primsize(r.str) > 0) { return 0; };
|
||
if (streq(r.str, "str")) { return 0; };
|
||
let si: *structinfo = structlookup(c, r.str);
|
||
if (si == nil) {
|
||
if (c != nil) {
|
||
let aliased: *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: *node) i32 = {
|
||
if (n == nil) { return 0; };
|
||
if (n.kind != nkind.N_CALL) { return 0; };
|
||
let callee: *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 == nkind.N_IDENT) {
|
||
cn = callee.str;
|
||
cmod = c.curmod;
|
||
};
|
||
if (callee.kind == nkind.N_DOT) {
|
||
cn = callee.str;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
};
|
||
if (cn.len == 0) { return 0; };
|
||
let rtyp: *node = fnretlookupmod(c, cn, cmod);
|
||
return sretretsize(c, rtyp);
|
||
};
|
||
|
||
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 (streq(s.sname, name)) {
|
||
if (streq(s.smod, c.curmod)) { return s; };
|
||
};
|
||
s = s.sinext;
|
||
};
|
||
s = c.structs;
|
||
for (s != nil) {
|
||
let sn: str = s.sname;
|
||
if (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);
|
||
let b: *structinfo = c.structs;
|
||
for (b != nil) {
|
||
if (streq(b.sname, leaf)) {
|
||
if (streq(b.smod, pkg)) {
|
||
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 (streq(s.sname, name)) {
|
||
if (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;
|
||
};
|
||
|
||
fn primsize(name: str) i32 = {
|
||
if (streq(name, "u8")) { return 1; };
|
||
if (streq(name, "i8")) { return 1; };
|
||
if (streq(name, "bool")) { return 1; };
|
||
if (streq(name, "u16")) { return 2; };
|
||
if (streq(name, "i16")) { return 2; };
|
||
if (streq(name, "u32")) { return 4; };
|
||
if (streq(name, "i32")) { return 4; };
|
||
if (streq(name, "f32")) { return 4; };
|
||
if (streq(name, "u64")) { return 8; };
|
||
if (streq(name, "i64")) { return 8; };
|
||
if (streq(name, "uint")) { return 8; };
|
||
if (streq(name, "int")) { return 8; };
|
||
if (streq(name, "uintptr")) { return 8; };
|
||
if (streq(name, "size")) { return 8; };
|
||
if (streq(name, "f64")) { return 8; };
|
||
if (streq(name, "rune")) { return 4; };
|
||
if (streq(name, "void")) { return 0; };
|
||
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: *node,
|
||
sz_out: *i32, unsigned_out: *bool) void = {
|
||
*sz_out = 0;
|
||
*unsigned_out = false;
|
||
let cur: *node = t;
|
||
for (cur != nil) {
|
||
let k: nkind = cur.kind;
|
||
if (k == nkind.N_TBANG) { cur = cur.lhs; }
|
||
else { if (k == nkind.N_TENUM) { cur = cur.lhs; }
|
||
else { if (k == 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 (streq(nm, "bool")) { return; };
|
||
let ps: i32 = primsize(nm);
|
||
if (ps > 0) {
|
||
*sz_out = ps;
|
||
*unsigned_out = typeisunsigned(cur.type_: *tinfo);
|
||
return;
|
||
};
|
||
let al: *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: *node,
|
||
sz_out: *i32, unsigned_out: *bool) void = {
|
||
*sz_out = 0;
|
||
*unsigned_out = false;
|
||
if (n == nil) { return; };
|
||
let k: nkind = n.kind;
|
||
if (k == 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) {
|
||
let ps: i32 = primsize(s);
|
||
if (ps > 0) {
|
||
*sz_out = ps;
|
||
*unsigned_out = typeisunsigned(n.type_: *tinfo);
|
||
};
|
||
};
|
||
return;
|
||
};
|
||
if (k == nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, n.str);
|
||
if (lc != nil) {
|
||
typenodeprimresolved(c, lc.tnode,
|
||
sz_out, unsigned_out);
|
||
};
|
||
return;
|
||
};
|
||
if (k == nkind.N_CAST) {
|
||
typenodeprimresolved(c, n.rhs, sz_out, unsigned_out);
|
||
return;
|
||
};
|
||
if (k == nkind.N_UN) {
|
||
exprprimresolved(c, n.lhs, sz_out, unsigned_out);
|
||
return;
|
||
};
|
||
if (k == 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). One TY_NAMED peel, then
|
||
// typeisint ? size : 0; bool falls out because typeisint(bool) is
|
||
// false — the same exclusion the old streq("bool") arm encoded.
|
||
let bu: *tinfo = nil;
|
||
if (n.lhs != nil) { bu = n.lhs.type_: *tinfo; };
|
||
if (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
|
||
if (bu != nil && bu.kind == tykind.TY_PTR) { bu = bu.sub; };
|
||
if (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
|
||
if (bu != nil && bu.kind == tykind.TY_STRUCT) {
|
||
let u: *tinfo = n.type_: *tinfo;
|
||
if (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
|
||
if (typeisint(u)) {
|
||
*sz_out = u.size: i32;
|
||
*unsigned_out = 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 (streq(vname, pname)) { return true; };
|
||
// `pname` is qualified, `vname` is bare: drop module prefix.
|
||
let i: i32 = 0;
|
||
for (i < pname.len) {
|
||
if (pname[i] == '.': u8) {
|
||
let tail: str;
|
||
tail.ptr = pname.ptr + i + 1;
|
||
tail.len = pname.len - i - 1;
|
||
if (streq(tail, vname)) { return true; };
|
||
};
|
||
i += 1;
|
||
};
|
||
// `vname` is qualified, `pname` is bare: same trick in reverse.
|
||
let j: i32 = 0;
|
||
for (j < vname.len) {
|
||
if (vname[j] == '.': u8) {
|
||
let tail: str;
|
||
tail.ptr = vname.ptr + j + 1;
|
||
tail.len = vname.len - j - 1;
|
||
if (streq(tail, pname)) { return true; };
|
||
};
|
||
j += 1;
|
||
};
|
||
return false;
|
||
};
|
||
|
||
// inferletcalltype — for an annotation-less `let x = expr;`, return
|
||
// a usable tnode for cgen's struct-aware paths. Today: `let x =
|
||
// f()?` infers x's type from the success variant of f's tagged
|
||
// return; without this, x has tnode = nil and `x.field` falls into
|
||
// the SB-symbol fallback (linker reports `undefined reference to
|
||
// <fieldname>`). We don't infer for plain `let x = f()` yet —
|
||
// non-tagged returns don't carry their type back the same way.
|
||
fn inferletcalltype(c: *cgen, rhs: *node) *node = {
|
||
if (rhs == nil) { return nil; };
|
||
// `?` (N_TRYPROP) and `!` (N_TRYUNW) both unwrap a tagged
|
||
// return to its success variant; the rhs we want the type of
|
||
// is the inner call expression.
|
||
let unwrap: bool = false;
|
||
let call: *node = rhs;
|
||
if (rhs.kind == nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; };
|
||
if (rhs.kind == nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; };
|
||
if (call == nil) { return nil; };
|
||
if (call.kind != nkind.N_CALL) { return nil; };
|
||
let callee: *node = call.lhs;
|
||
if (callee == nil) { return nil; };
|
||
let cname: str;
|
||
cname.ptr = nil; cname.len = 0;
|
||
let cmod: str;
|
||
cmod.ptr = nil; cmod.len = 0;
|
||
if (callee.kind == nkind.N_IDENT) {
|
||
cname = callee.str;
|
||
cmod = c.curmod;
|
||
};
|
||
if (callee.kind == nkind.N_DOT) {
|
||
cname = callee.str;
|
||
if (callee.lhs != nil) {
|
||
if (callee.lhs.kind == nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
};
|
||
if (cname.len == 0) { return nil; };
|
||
let rtyp: *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 != 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: *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).
|
||
if (n.lhs != nil) { return slotsize(c, n.lhs); };
|
||
// Annotation-less init: defer to the call's return type if we
|
||
// can infer it. Tagged-union returns need 24B; everything else
|
||
// matches slotsize on the inferred type.
|
||
let inferred: *node = inferletcalltype(c, n.rhs);
|
||
if (inferred != nil) { return slotsize(c, inferred); };
|
||
return 8;
|
||
};
|
||
|
||
// #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: *node) i32 = {
|
||
if (typn == nil) { return 8; };
|
||
let ti: *tinfo = typn.type_: *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.
|
||
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
|
||
if (ti == nil) { return 8; };
|
||
let kk: tykind = ti.kind;
|
||
if (kk == tykind.TY_VOID) { return 0; };
|
||
if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE ||
|
||
kk == tykind.TY_CHAN || kk == tykind.TY_FN ||
|
||
kk == tykind.TY_STR || kk == tykind.TY_TAGGED) {
|
||
return ti.size: i32;
|
||
};
|
||
if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE ||
|
||
kk == 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: *node) i32 = {
|
||
if (tnode == nil) { return 8; };
|
||
let ti: *tinfo = tnode.type_: *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.
|
||
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
|
||
if (ti == nil) { return 8; };
|
||
let k: tykind = ti.kind;
|
||
if (k == tykind.TY_STRUCT) { return ti.slotsize: i32; };
|
||
if (k == tykind.TY_ARRAY) { return ti.slotsize: i32; };
|
||
if (k == tykind.TY_TAGGED) { return ti.size: i32; };
|
||
if (k == tykind.TY_SLICE) { return ti.size: i32; };
|
||
if (k == tykind.TY_PTR || k == tykind.TY_FN ||
|
||
k == tykind.TY_CHAN) { return 8; };
|
||
if (k == 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;
|
||
};
|
||
|
||
fn registerstruct(c: *cgen, name: str, srcmod: str, tstruct: *node) void = {
|
||
let si: *structinfo = alloc(structinfo{
|
||
sname = name,
|
||
smod = srcmod,
|
||
})!;
|
||
let head: *fieldinfo = nil;
|
||
let tail: *fieldinfo = nil;
|
||
let off: i32 = 0;
|
||
let f: *node = tstruct.list;
|
||
for (f != nil) {
|
||
if (f.kind == nkind.N_TFIELD) {
|
||
let sz: i32 = fieldsize(c, f.lhs);
|
||
// Align to 8 for any field >= 4 bytes (matches our other
|
||
// cgen choices). i8/u8/bool may sit on odd byte offsets;
|
||
// the C cgen does similar best-effort packing.
|
||
let aln: i32 = 1;
|
||
if (sz >= 8) { aln = 8; }
|
||
else { if (sz >= 4) { aln = 4; }
|
||
else { if (sz >= 2) { aln = 2; }; }; };
|
||
if ((off & (aln - 1)) != 0) {
|
||
off = (off + aln - 1) & ~(aln - 1);
|
||
};
|
||
let fi: *fieldinfo = alloc(fieldinfo{
|
||
fname = f.str,
|
||
foff = off,
|
||
fsz = sz,
|
||
tnode = f.lhs,
|
||
})!;
|
||
if (head == nil) { head = fi; tail = fi; }
|
||
else { tail.finext = fi; tail = fi; };
|
||
off += sz;
|
||
};
|
||
f = f.next;
|
||
};
|
||
// Round total to 8 for stack-slot use.
|
||
if ((off & 7) != 0) { off = (off + 7) & ~7; };
|
||
si.fields = head;
|
||
si.totsize = off;
|
||
si.sinext = c.structs;
|
||
c.structs = si;
|
||
};
|
||
|
||
fn collectstructs(c: *cgen, file: *node) void = {
|
||
c.structs = nil;
|
||
if (file == nil) { return; };
|
||
let d: *node = file.list;
|
||
for (d != nil) {
|
||
if (d.kind == nkind.N_TYPEDECL) {
|
||
let body: *node = d.lhs;
|
||
// #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 == nkind.N_TBANG) {
|
||
body = body.lhs;
|
||
};
|
||
if (body != nil) {
|
||
if (body.kind == 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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
return typeisstr(t.type_: *tinfo);
|
||
};
|
||
|
||
fn isslicetype(c: *cgen, t: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
return typeisslice(t.type_: *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: *node) *node = {
|
||
let r: *node = resolvetype(c, t);
|
||
if (r == nil) { return nil; };
|
||
if (r.kind == nkind.N_TBANG) {
|
||
let inner: *node = r.lhs;
|
||
if (inner == nil) { return nil; };
|
||
r = resolvetype(c, inner);
|
||
if (r == nil) { return nil; };
|
||
};
|
||
if (r.kind == 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: *node) *node = {
|
||
if (scrut == nil) { return nil; };
|
||
let k: nkind = scrut.kind;
|
||
if (k == nkind.N_IDENT) { return nil; };
|
||
if (k == nkind.N_CALL) {
|
||
let callee: *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 == nkind.N_IDENT) { cnm = callee.str; };
|
||
if (callee.kind == 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 == nkind.N_IDENT) {
|
||
cmod = callee.lhs.str;
|
||
};
|
||
};
|
||
};
|
||
if (cnm.len > 0) {
|
||
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
|
||
if (rtyp != nil) { return resolvetagged(c, rtyp); };
|
||
};
|
||
};
|
||
return nil;
|
||
};
|
||
if (k == nkind.N_INDEX) {
|
||
let ibase: *node = scrut.lhs;
|
||
if (ibase == nil) { return nil; };
|
||
if (ibase.kind != 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: *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: *node = idxelemtn(btn);
|
||
if (etn == nil) { return nil; };
|
||
return resolvetagged(c, etn);
|
||
};
|
||
if (k == 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 == nkind.N_UN && scrut.op == 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: *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: *node) i32 = {
|
||
if (c == nil) { return 0; };
|
||
let r: *node = resolvetype(c, t);
|
||
if (r == nil) { return 0; };
|
||
if (r.kind != nkind.N_TNAME) { return 0; };
|
||
let nm: str = r.str;
|
||
if (streq(nm, "str")) { return 0; };
|
||
if (primsize(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: *tinfo) i32 = {
|
||
if (t == nil) { return 0; };
|
||
let u: *tinfo = t;
|
||
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
|
||
if (u == nil) { return 0; };
|
||
if (u.kind == tykind.TY_STRUCT || u.kind == 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: *tinfo) i32 = {
|
||
if (t == nil) { return 0; };
|
||
let u: *tinfo = t;
|
||
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
|
||
if (u == nil) { return 0; };
|
||
if (u.kind != 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: *node) bool = {
|
||
if (n == nil) { return false; };
|
||
return aggargsizetn(n.type_: *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: *node) bool = {
|
||
if (n == nil) { return false; };
|
||
let st: *tinfo = n.type_: *tinfo;
|
||
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
|
||
if (st == nil) { return false; };
|
||
if (st.kind != tykind.TY_STRUCT) { return false; };
|
||
if (st.size: i32 > 16) { return false; };
|
||
let f: *tfield = st.fields;
|
||
for (f != nil) {
|
||
if (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: *node) i32 = {
|
||
if (c == nil) { return 0; };
|
||
let r: *node = resolvetype(c, t);
|
||
if (r == nil) { return 0; };
|
||
if (r.kind != nkind.N_TNAME) { return 0; };
|
||
let nm: str = r.str;
|
||
if (streq(nm, "str")) { return 0; };
|
||
if (primsize(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: *node = resolvetype(c, fi.tnode);
|
||
if (rf != nil) {
|
||
if (rf.kind == nkind.N_TARRAY) { return 0; };
|
||
if (rf.kind == 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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
return typeistagged(t.type_: *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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
let u: *tinfo = t.type_: *tinfo;
|
||
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
|
||
if (u == nil) { return false; };
|
||
if (u.kind == tykind.TY_STRUCT) { return true; };
|
||
if (u.kind == tykind.TY_ARRAY) { return true; };
|
||
return u.kind == 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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
return typeisfloat(t.type_: *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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
return typeisf32(t.type_: *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: *node) bool = {
|
||
if (t == nil) { return false; };
|
||
return typeisnullable(t.type_: *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: *node) i32 = {
|
||
if (t == nil) { return 0; };
|
||
let ti: *tinfo = t.type_: *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.
|
||
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
|
||
if (ti == nil) { return 0; };
|
||
if (ti.kind != tykind.TY_TAGGED) { return 0; };
|
||
let p: *tparam = ti.params;
|
||
let i: i32 = 0;
|
||
for (p != nil) {
|
||
let vt: *tinfo = p.type_;
|
||
if (vt != nil) {
|
||
if (vt.kind == tykind.TY_NAMED) { vt = vt.under; };
|
||
if (vt != nil) {
|
||
if (vt.kind == 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 expr, -1 if absent. Used by cgreturn to map bare
|
||
// `return;` in a tagged-union-returning fn to the void variant's tag.
|
||
fn voidvariantindex(tagged: *node) i32 = {
|
||
if (tagged == nil) { return -1; };
|
||
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
|
||
let v: *node = tagged.list;
|
||
let idx: i32 = 0;
|
||
for (v != nil) {
|
||
if (v.kind == nkind.N_TNAME) {
|
||
if (streq(v.str, "void")) { return idx; };
|
||
};
|
||
v = v.next;
|
||
idx += 1;
|
||
};
|
||
return -1;
|
||
};
|
||
|
||
// taggedvariantindex — given the tagged-union type expr and the
|
||
// returned value's surface type, find the matching variant's 0-based
|
||
// index. Compare by exact type name first; if no match, fall back to
|
||
// "any str-shape variant matches an str-typed value".
|
||
fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = {
|
||
if (tagged == nil) { return -1; };
|
||
return taggedvariantindext(c, tagged.type_: *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: *tinfo, rhs: *node) i32 = {
|
||
if (du == nil) { return -1; };
|
||
if (rhs == nil) { return -1; };
|
||
let ti: *tinfo = du;
|
||
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
|
||
if (ti == nil) { return -1; };
|
||
if (ti.kind != tykind.TY_TAGGED) { return -1; };
|
||
let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo);
|
||
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: *tparam = ti.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
let vt: *tinfo = p.type_;
|
||
let visstr: bool = typeisstr(vt);
|
||
let visslice: bool = 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: *node, pat: *node) i32 = {
|
||
if (tagged == nil) { return -1; };
|
||
if (pat == nil) { return -1; };
|
||
return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo);
|
||
};
|
||
|
||
// 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).
|
||
fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
|
||
if (want == nil) { return -1; };
|
||
let ti: *tinfo = tagged;
|
||
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
|
||
if (ti == nil) { return -1; };
|
||
if (ti.kind != 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: *tparam = ti.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
if (cgvariantmatch(p.type_, want)) { return idx; };
|
||
p = p.tnext;
|
||
idx += 1;
|
||
};
|
||
// 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 != tykind.TY_NAMED) {
|
||
let q: *tparam = ti.params;
|
||
let qi: i32 = 0;
|
||
let found: i32 = -1;
|
||
let n: i32 = 0;
|
||
for (q != nil) {
|
||
// One-level NAMED unwrap: a chained ptr-alias variant
|
||
// (type a=*X; type b=a) isn't reached here, so it would
|
||
// silently mis-tag — unexercised (zero in corpus), see
|
||
// task #17.
|
||
let pu: *tinfo = q.type_;
|
||
if (pu != nil && pu.kind == tykind.TY_NAMED
|
||
&& pu.under != nil && typeeq(pu.under, 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: *tinfo, want: *tinfo) bool = {
|
||
if (dst == nil) { return false; };
|
||
if (want == nil) { return false; };
|
||
let du: *tinfo = dst;
|
||
if (du.kind == tykind.TY_NAMED && du.under != nil) { du = du.under; };
|
||
if (du.kind == 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.
|
||
let p: *tparam = du.params;
|
||
for (p != nil) {
|
||
let pu: *tinfo = p.type_;
|
||
if (pu != nil && pu.kind == tykind.TY_NAMED
|
||
&& pu.under != nil) { pu = pu.under; };
|
||
let nestedtagged: bool = false;
|
||
if (pu != nil) {
|
||
if (pu.kind == tykind.TY_TAGGED) { nestedtagged = true; };
|
||
};
|
||
if (!nestedtagged) {
|
||
if (tyassignableuntyped(p.type_, want)) { return true; };
|
||
};
|
||
p = p.tnext;
|
||
};
|
||
return false;
|
||
};
|
||
// type.c:357-369 — the per-kind holds checks run on the UNPEELED
|
||
// dst: typeisnum/typeisfloat/typeisint self-recurse TY_NAMED like
|
||
// their cstage twins; STR/BOOL spell out the one-level unwrap.
|
||
if (want.kind == tykind.TY_UNTYPED_INT) { return typeisnum(dst); };
|
||
if (want.kind == tykind.TY_UNTYPED_FLOAT) { return typeisfloat(dst); };
|
||
if (want.kind == tykind.TY_UNTYPED_STR) {
|
||
if (dst.kind == tykind.TY_STR) { return true; };
|
||
if (dst.kind == tykind.TY_NAMED && dst.under != nil) {
|
||
return dst.under.kind == tykind.TY_STR;
|
||
};
|
||
return false;
|
||
};
|
||
if (want.kind == tykind.TY_UNTYPED_RUNE) {
|
||
if (typeisint(dst)) { return true; };
|
||
return dst.kind == tykind.TY_RUNE;
|
||
};
|
||
if (want.kind == tykind.TY_UNTYPED_BOOL) {
|
||
if (dst.kind == tykind.TY_BOOL) { return true; };
|
||
if (dst.kind == tykind.TY_NAMED && dst.under != nil) {
|
||
return dst.under.kind == tykind.TY_BOOL;
|
||
};
|
||
return false;
|
||
};
|
||
if (want.kind == tykind.TY_UNTYPED_NIL) {
|
||
if (du.kind == tykind.TY_PTR) { return true; };
|
||
if (du.kind == tykind.TY_SLICE) { return true; };
|
||
if (du.kind == tykind.TY_CHAN) { return true; };
|
||
return du.kind == 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: *tinfo, want: *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 (typeisuntyped(want)) { return tyassignableuntyped(vt, want); };
|
||
if (vt.kind == tykind.TY_NAMED && want.kind == tykind.TY_NAMED) {
|
||
return typeeq(vt, want);
|
||
};
|
||
if (vt.kind == tykind.TY_NAMED || want.kind == tykind.TY_NAMED) {
|
||
let vu: *tinfo = vt;
|
||
for (vu != nil && vu.kind == tykind.TY_NAMED) { vu = vu.under; };
|
||
let wu: *tinfo = want;
|
||
for (wu != nil && wu.kind == tykind.TY_NAMED) { wu = wu.under; };
|
||
if (vu != nil && wu != nil
|
||
&& vu.kind == tykind.TY_TAGGED
|
||
&& wu.kind == tykind.TY_TAGGED) {
|
||
return typeeq(vu, wu);
|
||
};
|
||
return false;
|
||
};
|
||
return 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: *tinfo, want: *tinfo) bool = {
|
||
let vu: *tinfo = vt;
|
||
for (vu != nil && vu.kind == tykind.TY_NAMED) { vu = vu.under; };
|
||
let wu: *tinfo = want;
|
||
for (wu != nil && wu.kind == tykind.TY_NAMED) { wu = wu.under; };
|
||
if (vu == nil) { return false; };
|
||
if (wu == nil) { return false; };
|
||
return 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: *node, elem: *node) i32 = {
|
||
if (tagged == nil) { return -1; };
|
||
let ti: *tinfo = tagged.type_: *tinfo;
|
||
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
|
||
if (ti == nil) { return -1; };
|
||
if (ti.kind != tykind.TY_TAGGED) { return -1; };
|
||
let want: *tinfo = nil;
|
||
if (elem != nil) { want = elem.type_: *tinfo; };
|
||
let fallback: i32 = -1;
|
||
let p: *tparam = ti.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
let vt: *tinfo = p.type_;
|
||
if (vt != nil) {
|
||
if (typeisslice(vt)) {
|
||
if (fallback < 0) { fallback = idx; };
|
||
if (want != nil) {
|
||
let su: *tinfo = vt;
|
||
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
|
||
if (su != nil) {
|
||
if (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: *tinfo, su: *tinfo, slot_off: i32) void = {
|
||
let dt: *tinfo = du;
|
||
for (dt != nil && dt.kind == tykind.TY_NAMED) { dt = dt.under; };
|
||
if (dt == nil) { return; };
|
||
if (dt.kind != tykind.TY_TAGGED) { return; };
|
||
let st: *tinfo = su;
|
||
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
|
||
if (st == nil) { return; };
|
||
if (st.kind != tykind.TY_TAGGED) { return; };
|
||
let identity: bool = true;
|
||
let p: *tparam = st.params;
|
||
let idx: i32 = 0;
|
||
for (p != nil) {
|
||
let di: i32 = flatvariantidxt(dt, p.type_);
|
||
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_);
|
||
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: *node) str = {
|
||
let empty: str;
|
||
empty.ptr = nil; empty.len = 0;
|
||
if (src == nil) { return empty; };
|
||
if (src.kind == nkind.N_STRUCTLIT) {
|
||
let trefn: *node = src.lhs;
|
||
if (trefn != nil) {
|
||
let nm: str;
|
||
nm.ptr = nil; nm.len = 0;
|
||
if (trefn.kind == nkind.N_IDENT) { nm = trefn.str; };
|
||
if (trefn.kind == nkind.N_TNAME) { nm = trefn.str; };
|
||
if (nm.len > 0) {
|
||
if (structlookup(c, nm) != nil) { return nm; };
|
||
};
|
||
};
|
||
return empty;
|
||
};
|
||
if (src.kind == nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, src.str);
|
||
if (lc != nil) {
|
||
let tn: *node = lc.tnode;
|
||
if (tn != nil) {
|
||
if (tn.kind == nkind.N_TNAME) {
|
||
if (structlookup(c, tn.str) != nil) {
|
||
return tn.str;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
};
|
||
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: *node) *node = {
|
||
if (src == nil) { return nil; };
|
||
if (src.kind != nkind.N_IDENT) { return nil; };
|
||
let lc: *local = localfindnode(c, src.str);
|
||
if (lc == nil) { return nil; };
|
||
let tn: *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: *node) bool = {
|
||
if (src == nil) { return false; };
|
||
if (src.kind == 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 (typeistagged(src.type_: *tinfo)) { return true; };
|
||
return false;
|
||
};
|
||
if (src.kind == 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 (typeistagged(src.type_: *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 == nkind.N_DOT) {
|
||
if (typeistagged(src.type_: *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 == nkind.N_UN && src.op == tkind.TK_STAR) {
|
||
if (typeistagged(src.type_: *tinfo)) { return true; };
|
||
};
|
||
if (src.kind == nkind.N_TRYPROP || src.kind == nkind.N_TRYUNW) {
|
||
if (typeistagged(src.type_: *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: *node) bool = {
|
||
if (e == nil) { return false; };
|
||
if (e.kind == nkind.N_UN && e.op == tkind.TK_STAR) {
|
||
let du: *tinfo = e.type_: *tinfo;
|
||
for (du != nil && du.kind == tykind.TY_NAMED) { du = du.under; };
|
||
if (du == nil) { return false; };
|
||
if (du.kind != tykind.TY_TAGGED) { return false; };
|
||
if (du.nullable != 0) { return false; };
|
||
return du.size: i32 > 8;
|
||
};
|
||
if (e.kind != nkind.N_INDEX && e.kind != nkind.N_DOT) { return false; };
|
||
let u: *tinfo = e.type_: *tinfo;
|
||
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
|
||
if (u == nil) { return false; };
|
||
if (u.kind != 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: *node) *node = {
|
||
for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) {
|
||
let cu: *tinfo = e.type_: *tinfo;
|
||
for (cu != nil && cu.kind == tykind.TY_NAMED) { cu = cu.under; };
|
||
if (cu == nil) { return e; };
|
||
if (cu.kind != tykind.TY_TAGGED) { return e; };
|
||
if (cu.nullable != 0) { return e; };
|
||
let iu: *tinfo = e.lhs.type_: *tinfo;
|
||
for (iu != nil && iu.kind == tykind.TY_NAMED) { iu = iu.under; };
|
||
if (iu == nil) { return e; };
|
||
if (iu.kind != 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: *node) *node = {
|
||
for (e != nil && e.kind == nkind.N_CAST && e.lhs != nil) {
|
||
if (!typeeq(e.type_: *tinfo, e.lhs.type_: *tinfo)) { return e; };
|
||
let cu: *tinfo = e.type_: *tinfo;
|
||
for (cu != nil && cu.kind == tykind.TY_NAMED) { cu = cu.under; };
|
||
if (cu == nil) { return e; };
|
||
if (cu.kind != 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) 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");
|
||
// word2 → R8 (slice variant: slot = 8 tag + 24 payload = 32).
|
||
if (slot_sz > 24) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg((foff + 24): i64, basereg);
|
||
emitline(", R8\n");
|
||
};
|
||
// word1 → CX (load LAST; conflicts with CX-base globals).
|
||
if (slot_sz > 16) {
|
||
emitline("\tMOVQ\t");
|
||
emitdispreg((foff + 16): i64, basereg);
|
||
emitline(", CX\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: *tinfo, src: *node,
|
||
basereg: str, slot_off: i32, slot_sz: i32) void = {
|
||
if (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: *tinfo, src: *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: *tinfo = dst;
|
||
for (dt != nil && dt.kind == tykind.TY_NAMED) { dt = dt.under; };
|
||
if (dt == nil) { return; };
|
||
if (dt.kind != 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 == nkind.N_CAST) {
|
||
if (src.lhs != nil) {
|
||
let inner: *node = src.lhs;
|
||
let inneristagged: bool = false;
|
||
if (inner.kind == 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 castrhs: *node = src.rhs;
|
||
if (castrhs != nil) {
|
||
let castt: *tinfo = castrhs.type_: *tinfo;
|
||
let castu: *tinfo = castt;
|
||
for (castu != nil && castu.kind == tykind.TY_NAMED) {
|
||
castu = castu.under;
|
||
};
|
||
if (castu != nil) {
|
||
if (castu == dt) {
|
||
castisdst = true;
|
||
} else { if (castu.kind == tykind.TY_TAGGED) {
|
||
if (typeeq(castt, dst)) {
|
||
castisdst = true;
|
||
};
|
||
}; };
|
||
};
|
||
};
|
||
if (castisdst && !inneristagged) {
|
||
src = inner;
|
||
};
|
||
};
|
||
};
|
||
};
|
||
// #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);
|
||
if (srctagged) {
|
||
let nested: i32 = flatvariantidxt(dt, src.type_: *tinfo);
|
||
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: *tparam = dt.params;
|
||
for (gp != nil) {
|
||
if (cgvariantstructmatch(gp.type_,
|
||
src.type_: *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 su: *tinfo = src.type_: *tinfo;
|
||
for (su != nil && su.kind == tykind.TY_NAMED) {
|
||
su = su.under;
|
||
};
|
||
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 == nkind.N_IDENT) {
|
||
let lc: *local = localfindnode(c, src.str);
|
||
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 {
|
||
// #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 == 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 == 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: *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_: *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 == 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 su37: *tinfo = src.type_: *tinfo;
|
||
for (su37 != nil && su37.kind == tykind.TY_NAMED) {
|
||
su37 = su37.under;
|
||
};
|
||
let ssz37: i32 = su37.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_: *tinfo, slot_off);
|
||
return;
|
||
};
|
||
cgexpr(c, src);
|
||
emitline("\tMOVQ\tAX, ");
|
||
emitoff(slot_off: i64);
|
||
emitline("(BP)\n");
|
||
if (slot_sz > 8) {
|
||
emitline("\tMOVQ\tDX, ");
|
||
emitoff((slot_off + 8): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
if (slot_sz > 16) {
|
||
emitline("\tMOVQ\tCX, ");
|
||
emitoff((slot_off + 16): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
if (slot_sz > 24) {
|
||
emitline("\tMOVQ\tR8, ");
|
||
emitoff((slot_off + 24): i64);
|
||
emitline("(BP)\n");
|
||
};
|
||
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).
|
||
let sf37: *tinfo = src.type_: *tinfo;
|
||
for (sf37 != nil && sf37.kind == tykind.TY_NAMED) {
|
||
sf37 = sf37.under;
|
||
};
|
||
if (sf37 != nil && sf37.kind == tykind.TY_TAGGED
|
||
&& sf37.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 == nkind.N_CAST && sf37 != nil
|
||
&& sf37.kind == tykind.TY_TAGGED && sf37.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: *node = nil;
|
||
let tupcast: bool = false;
|
||
if (src != nil) {
|
||
if (src.kind == nkind.N_TUPLE) { tupsrc = src; };
|
||
if (src.kind == nkind.N_CAST) {
|
||
if (src.lhs != nil) {
|
||
if (src.lhs.kind == nkind.N_TUPLE) {
|
||
tupsrc = src.lhs;
|
||
tupcast = true;
|
||
};
|
||
};
|
||
};
|
||
// #72: any OTHER tuple-typed source (ident, call result,
|
||
// match binding) would fall to the scalar arm below and
|
||
// silently drop payload slot 1+ — loud-stop (rule 7) until
|
||
// the word-copy / cursor-receive arms are wired. Mirrors
|
||
// cstage cg_widen_tagged_store.
|
||
if (tupsrc == nil) {
|
||
let stu72: *tinfo = src.type_: *tinfo;
|
||
for (stu72 != nil && stu72.kind == tykind.TY_NAMED) {
|
||
stu72 = stu72.under;
|
||
};
|
||
if (stu72 != nil) {
|
||
if (stu72.kind == tykind.TY_TUPLE) {
|
||
let m72: str = "cgwidentaggedstore: tuple-typed source shape unwired (only the bare/cast tuple literal carries a full payload; see #72)\n";
|
||
os.write(2, m72.ptr, m72.len: u64);
|
||
os.exit(1);
|
||
};
|
||
};
|
||
};
|
||
};
|
||
if (tupsrc != nil) {
|
||
let stu: *tinfo = src.type_: *tinfo;
|
||
for (stu != nil && stu.kind == tykind.TY_NAMED) { stu = stu.under; };
|
||
if (stu != nil) { if (stu.kind == 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: *node = tupsrc.list;
|
||
for (bl != nil && !tupcast) {
|
||
let bare: bool = false;
|
||
if (bl.kind == nkind.N_TRUE) { bare = true; };
|
||
if (bl.kind == nkind.N_FALSE) { bare = true; };
|
||
if (bl.kind == nkind.N_INTLIT && bl.tsuffix.len == 0) {
|
||
bare = true;
|
||
};
|
||
if (bl.kind == 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_: *tinfo);
|
||
// #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: *node = tupsrc.list;
|
||
for (ce != nil) {
|
||
// #22a (rule 7): a tagged element's box can't
|
||
// ride the scalar/wide store arms below —
|
||
// pre-guard it silently stored word0. Nested
|
||
// tagged-in-tuple-in-union packing is the
|
||
// #242/#22b family. Mirrors cstage.
|
||
let ceti: *tinfo = ce.type_: *tinfo;
|
||
for (ceti != nil && ceti.kind == tykind.TY_NAMED) {
|
||
ceti = ceti.under;
|
||
};
|
||
if (ceti != nil && ceti.kind == tykind.TY_TAGGED) {
|
||
let m22: str = "cgwidentaggedstore: tagged element in a tuple-in-union payload unwired (see #242/#22b)\n";
|
||
os.write(2, m22.ptr, m22.len: u64);
|
||
os.exit(1);
|
||
};
|
||
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: *node = tupsrc.list;
|
||
for (te != nil) {
|
||
let isflt: bool = isfloattype(c, te);
|
||
let wide: bool = nodeisstr(c, te) || nodeisslice(c, te);
|
||
let esz: i32 = 8;
|
||
let eti: *tinfo = te.type_: *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;
|
||
}; };
|
||
};
|
||
// 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 == 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: *tinfo = src.type_: *tinfo;
|
||
if (typeisf32(srct)) { fkind = 1; }
|
||
else { if (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: *tinfo = dt;
|
||
for (fti != nil && fti.kind == tykind.TY_NAMED) { fti = fti.under; };
|
||
if (fti != nil) { if (fti.kind == tykind.TY_TAGGED) {
|
||
let fp: *tparam = fti.params;
|
||
let fidx: i32 = 0;
|
||
for (fp != nil) {
|
||
let fvt: *tinfo = fp.type_;
|
||
for (fvt != nil && fvt.kind == tykind.TY_NAMED) { fvt = fvt.under; };
|
||
if (fvt != nil) {
|
||
if (typeisfloat(fvt)) {
|
||
if (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: *node,
|
||
outrootname: *str, outrootoff: *i32, outtotaloff: *i32,
|
||
outleaftype: **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 != nkind.N_DOT) { return false; };
|
||
let stk: [16]*node;
|
||
let nsteps: i32 = 0;
|
||
let cur: *node = n;
|
||
for (cur != nil) {
|
||
if (cur.kind != 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 != 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 == 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 == nkind.N_TPTR) {
|
||
let pe: *node = lc.tnode.lhs;
|
||
if (pe != nil) {
|
||
if (pe.kind == nkind.N_TNAME) {
|
||
*outrootoff = lc.off;
|
||
*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: *tinfo = cur.type_: *tinfo;
|
||
for (curstruct != nil && curstruct.kind == tykind.TY_NAMED) {
|
||
curstruct = curstruct.under;
|
||
};
|
||
if (*outptrroot) {
|
||
if (curstruct == nil) { return false; };
|
||
if (curstruct.kind != tykind.TY_PTR) { return false; };
|
||
curstruct = curstruct.sub;
|
||
for (curstruct != nil && curstruct.kind == tykind.TY_NAMED) {
|
||
curstruct = curstruct.under;
|
||
};
|
||
};
|
||
let i: i32 = nsteps - 1;
|
||
for (i >= 0) {
|
||
if (curstruct == nil) { return false; };
|
||
if (curstruct.kind != tykind.TY_STRUCT) { return false; };
|
||
if (stk[i] == nil) { return false; };
|
||
let stepnm: str = stk[i].str;
|
||
let tf: *tfield = curstruct.fields;
|
||
let found: *tfield = nil;
|
||
for (tf != nil) {
|
||
if (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: *tinfo = found.type_;
|
||
for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; };
|
||
if (ft == nil) { return false; };
|
||
if (ft.kind == 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 (streq(pseudo, "ptr")) { delta = 0; }
|
||
else { if (streq(pseudo, "len")) { delta = 8; }
|
||
else { if (streq(pseudo, "cap")) { delta = 16; }; }; };
|
||
if (delta < 0) { return false; };
|
||
*outtotaloff = *outtotaloff + foff;
|
||
*outslicedelta = delta;
|
||
return true;
|
||
};
|
||
if (ft.kind == tykind.TY_SLICE) {
|
||
if (i != 1) { return false; };
|
||
let pseudo: str = stk[0].str;
|
||
let delta: i32 = -1;
|
||
if (streq(pseudo, "ptr")) { delta = 0; }
|
||
else { if (streq(pseudo, "len")) { delta = 8; }
|
||
else { if (streq(pseudo, "cap")) { delta = 16; }; }; };
|
||
if (delta < 0) { return false; };
|
||
*outtotaloff = *outtotaloff + foff;
|
||
*outslicedelta = delta;
|
||
return true;
|
||
};
|
||
if (ft.kind != 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: *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 == 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: *node = lit.list;
|
||
for (fieldnode != nil) {
|
||
if (fieldnode.kind == nkind.N_FIELD) {
|
||
let fname: str = fieldnode.str;
|
||
let fi: *fieldinfo = si.fields;
|
||
for (fi != nil) {
|
||
let fn_: str = fi.fname;
|
||
if (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_: *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 == nkind.N_STRUCTLIT) {
|
||
if (fi.tnode != nil) {
|
||
if (fi.tnode.kind == nkind.N_TNAME) {
|
||
if (primsize(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 == nkind.N_CALL) {
|
||
if (fi.tnode != nil) {
|
||
if (fi.tnode.kind == nkind.N_TNAME) {
|
||
if (primsize(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 == nkind.N_TARRAY
|
||
&& fieldnode.lhs != nil
|
||
&& fieldnode.lhs.kind == 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: *node = fi.tnode.lhs;
|
||
let isstrel: bool = isstrtype(c, elemn);
|
||
let istagel: bool = istaggedtype(c, elemn);
|
||
let issliceel: bool = false;
|
||
let isstructel: bool = false;
|
||
if (elemn != nil) {
|
||
if (elemn.kind == nkind.N_TSLICE) {
|
||
issliceel = true;
|
||
};
|
||
if (elemn.kind == nkind.N_TNAME) {
|
||
if (primsize(elemn.str) == 0) {
|
||
if (structlookup(c, elemn.str) != nil) {
|
||
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 == nkind.N_TNAME) {
|
||
let ps: i32 = primsize(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: *node = fieldnode.lhs.list;
|
||
for (e != nil) {
|
||
let isellip: bool = false;
|
||
if (e.kind == nkind.N_FIELD) {
|
||
if (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 == 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: *tinfo = fi.tnode.type_: *tinfo;
|
||
for (agti49 != nil && agti49.kind == tykind.TY_NAMED) {
|
||
agti49 = agti49.under;
|
||
};
|
||
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: *node, bpoff: i32) void = {
|
||
if (si == nil) { return; };
|
||
cgstructlitfill(c, si, lit, 0, 0, "", bpoff);
|
||
};
|