Files
ww/cmd/w6c/cgen.c
Hojun-Cho 9659a0dfbe cgen: B6-c2 call-arg family single peels fold into type_chase_named — 8 lines, loud-preserving
The exact B6-c2 set (rob b6 spec §2): :8402 (callee fn-type resolve cu),
:8435 (variadic slice param vsu), :8561/:8563 + :8577/:8579 (tagged
widen-detect pu/au pairs, arg-class + #38b MEMORY-class) + the two
LOUD-PRESERVING chases :8829 (float-struct rule-7 fatal st) and :8934
(#32 tuple-arg rule-7 fatal targ). Raw `->under` in cgen.c 49→41.

LOUD-PRESERVING discipline: the :8829/:8934 chases widen what the gate
SEES, never what it ACCEPTS. DESIGNED ACCEPTANCE NARROWING (ken b6
oracle c2): kb6_fsarg2 — a 1-level-alias float-struct from a non-ident
source previously DODGED the #271/#165 fatal via the single peel; cs
accepted and GP-passed it runtime-correct by self-consistent luck
(caller+callee agreed on the wrong transport, no SSE eightbyte). Post-
c2 cs louds with the pinned #271/#165 text. ww's cell was already loud
at its own alias-return bound (#272/#276/#277 class) — fsarg2_bound
pins BOTH texts per-stage (experr_ww). fsarg0 plain control stays loud
both stages. :8934 is WATCH-ONLY (alias tuple-args are checker-blocked
upstream, #86/#99): kb5_tuparg_c two-key cells verified unmoved
(cs ok/0 + ww ok/1).

TRAIN INVARIANT: cs-only; w6c_ww/ww_ww bit-identical to the 4cac1cb
baselines (b6bddc8e…/4e9ca874…). cs movers bounded to exactly
kb6_fsarg2 (run-cell ok/0→ERR, no asm emitted — zero run-row movers);
rest of the corpus + five mains byte-NEUTRAL; kw1_101/fill2/tuparg_c/
xampdef/amplen1 detectors unmoved; 989 ratchet zero flips. kb6_sarg /
kb6_strarg / kb6_fsarg (ident twin) latent byte-NEUTRAL per ken's
structural bound.

944_alias_cgen_b6_run grows 3→8 rows, 9→22 checks: fsarg0_loud_ctl +
fsarg2_bound (per-stage experr pins; row struct gains experr_ww for
two-site loud pairs) + fsarg_ident_ctl/sarg_2lvl/strarg_2lvl controls.
All 944-family suites green; sizelint 0.
2026-06-06 03:51:34 +09:00

15195 lines
537 KiB
C

/*
* cgen.c — typed AST → Prog list, expressed as Plan 9-flavoured
* amd64 assembly text. This is the simplest thing that works:
*
* - Every function gets a stack frame sized for spilled locals + a
* 16-byte alignment pad.
* - Expressions are evaluated stack-machine style: result in AX,
* intermediate stuff pushed on the hardware stack via PUSHQ AX.
* - The first six integer args go in DI, SI, DX, CX, R8, R9
* (SysV amd64 ABI). We don't yet handle struct-by-value or
* floats; floats and slices are deferred.
*
* Calling our own functions: emit CALL <name>(SB), let w6a/w6l resolve.
* Calling C externs: same — extern symbols are just unresolved CALLs.
*/
#include "gc.h"
#include <string.h>
#include <stdlib.h>
static const int sysv_argregs[] = { D_DI, D_SI, D_DX, D_CX, D_R8, D_R9 };
static const int sysv_fargregs[] = { D_X0, D_X1, D_X2, D_X3, D_X4, D_X5, D_X6, D_X7 };
/* per-fn cursor, reset before each cgfn: counts how many 8-byte
* stack-arg slots above BP have been claimed. */
int cg_stack_arg_cursor;
/* return type of the current function, set by cgfn before walking
* the body. Drives tagged-union return construction and the `?` /
* `!` propagation paths. */
static Type *cg_ret_type;
/* Pointer to the current function's frame size accumulator. cgexpr
* needs this to allocate scratch slots (e.g. match bindings) without
* threading it through every signature. */
static int *cg_frame;
/* Per-fn @retscr offset (single-slot SSoT, task #14). Returns are
* terminal: at most one return path fires per call, so all retscr
* uses share one slot. Mirrors wwstage's `@retscr` convention
* (cgen.ww localadd '@'-prefix dedup; #38 ratified single-slot
* semantics for synthetic scratches). 0 means "not yet allocated";
* negative offsets returned by local_alloc are the live value. */
static int cg_retscr;
/* Per-fn @tupfscr offset (single-slot SSoT). A multi-float tuple return
* (#164/#107) spills each float out of X0 to this scratch as the L→R
* element walk clobbers X0, then reloads X0/X1 by SSE index after the
* integer POPQ dance. Sized to the SSE register cap (X0,X1). Mirrors the
* @retscr single-slot convention + wwstage's `@tupfscr` '@'-prefix dedup;
* 0 means "not yet allocated". */
static int cg_tupfscr;
/* Per-fn @tupargscr staging slot (#163, single-slot SSoT). A tuple
* PASSED AS AN ARGUMENT (the param twin of #164's tuple return) is left
* by its producing call in the return-ABI cursor (AX/DX/CX/R8 + X0/X1);
* the SEND restages it into this slot positionally (tuple_store), then
* pushes the slot words onto the stack so the pop drains them into the
* SysV ARG cursor (DI/SI/.. + X0..X7). A frame slot decouples the
* return-class regs (which overlap the arg-class regs) from the arg
* placement. Reused per tuple arg (drained to the stack before the next
* arg); 0 means "not yet allocated", cg_tupargscr_sz the cached width. */
static int cg_tupargscr;
static int cg_tupargscr_sz;
/* #271: per-fn @aggargscr scratch for a >24B (sret-class) aggregate
* arg sourced from a CALL — the result is sret'd here, then pushed
* word-by-word into the arg convention. 0 = not yet allocated. */
static int cg_aggargscr;
static int cg_aggargscr_sz;
/* Per-fn @-prefix scratch SSoT (task #26, follow-up to #15-cstage's
* @retscr). Pre-#26 each site allocated a labelseq-stamped fresh slot
* per call (mklabel "tagbase" / "tagscr" / "argscr" / "idxscr"); the
* labelseq bumps drifted cstage's ct/ce/end labels ahead of wwstage,
* and the per-call frame growth drifted cstage's framesize ahead too.
*
* Two cached slots match wwstage's `@`-prefix namespace exactly:
* cg_tagbase — 8B base-register spill for cg_widen_tagged_store
* via_outer (mirrors wwstage @tagbase, 1 site).
* @tagscr<sz> — sized scratch shared across THREE sites: cg_widen_
* tagged_store via_outer write target, cg_widen_tagged_
* push struct/tagged-source widen, N_INDEX tagged-element
* assign. Mirrors wwstage @tagscr<sz> — wwstage shares
* the slot via localadd `@`-prefix dedup against
* c.atlocals.
*
* Both stages size at first use (per name). Pre-#44 the tagged scratch
* was a SINGLE slot and a later site asking for a larger size fatal'd
* (rule 7 — pinned offset can't grow in place once neighbours are
* allocated); a fn mixing two tagged slot sizes smaller-first (regex
* compile(): 56B append-element widen then 64B sret return) was
* uncompilable. #44 keys the scratch by slot size — one cached slot
* per distinct size, allocated in first-use order in BOTH stages, so
* the grow-fatal is unreachable for @tagscr by construction. All
* three sites funnel through cg_tagscr_slot (no other alloc path).
* Per-fn convergence completed by #15 (#26c follow-up): wwstage
* dropped its scanlocals pre-pass and aligned DOWN to cstage's
* first-use shape. */
static int cg_tagbase;
static int cg_tagbase_sz;
enum { CG_NTAGSCR = 16 };
static int cg_tagscr_off[CG_NTAGSCR];
static int cg_tagscr_sz[CG_NTAGSCR];
static int cg_ntagscr;
/* #34: per-fn @appendscr — 8B dst-pointer spill for the append()
* struct-literal element fill (cg_structlit_fill DST_PTR_LOCAL needs
* a BP-rooted slot to reload BX from across its internal cgexprs).
* Cached per name per fn to mirror wwstage's localadd `@`-prefix
* dedup, else two struct appends in one fn diverge the frame. */
static int cg_appendscr;
/* #49 split-resolve stashes: the source chain's PRE-grow rvalues —
* deref-root pointer value (@appendsroot) and scaled index offset
* (@appendsoff) — must survive rt_ensure so the POST-grow base
* re-derivation can add them back. Same per-fn name-cache discipline
* as @appendscr. */
static int cg_appendsroot;
static int cg_appendsoff;
/* FA1 (#15) @apphdrscr — 8B spill of the resolver-derived slice-header
* ADDRESS for append() through a non-ident-local target (`append(*p,
* v)`). rt_ensure may realloc .ptr but never moves the header, so the
* spilled address stays valid across the call; every access reloads
* from the slot because registers don't survive it. Allocated fresh
* per append SITE (no per-fn cache, no decl here): a nested
* append-through-pointer inside a value expression (match-yield arm)
* spills its own resolve, and a shared slot would feed the outer
* grow/slot reloads the INNER target's header — silent cross-slice
* corruption (806 reentrant_value row). */
/* System V AMD64 sret discipline (task #23). Plain TY_STRUCT returns
* with size > 24B are passed via a hidden first-arg pointer (RDI) to
* a caller-prealloc dest; the callee writes through that pointer and
* returns it in RAX. Tagged returns (slot ≤ 32B in AX/DX/CX/R8) and
* tuples (16/24B in AX/DX/CX) keep their existing register-return ABI.
*
* cg_sret_arg_off — callee-side @sretarg slot (8B, holds saved RDI).
* Set in cgfn prologue when ret > 24B plain struct.
* cg_sret_dest_off — caller-side dest offset, propagated from a receive
* site (N_LET / N_ASSIGN ident) to the nested N_CALL
* so the call emits `LEAQ off(BP), RDI` instead of
* allocating a scratch. 0 means no receiver wired.
* cg_sretscr_off — per-fn @sretscr discard slot for sret CALLs whose
* result is dropped (no named receiver). Single-slot
* SSoT mirroring cg_retscr. Sized to the largest
* discarded sret return type in the fn.
* cg_sret_forward — set by cgreturn `return f();` from an sret callee
* to signal cgcall: source RDI for inner from outer's
* saved @sretarg (MOVQ) instead of LEAQ'ing a local
* dest. Inner writes into outer's caller-prealloc;
* inner's RAX (the dest pointer) is already outer's
* return value. No temporary in outer's frame. */
static int cg_sret_arg_off;
static int cg_sret_dest_off;
/* #220: caller-side dest for an sret receive into a GLOBAL lvalue. A
* BP-relative i32 offset (cg_sret_dest_off) can't name a top-level let,
* so the symbol name is carried instead and emitted as LEAQ name(SB),DI.
* Mutually exclusive with cg_sret_dest_off. */
static const char *cg_sret_dest_sym;
static int cg_sretscr_off;
static int cg_sretscr_sz;
static int cg_sret_forward;
/* Per-fn defer stack: pushed in registration order, popped (emitted)
* in reverse at each return. */
#define DEFER_MAX 32
static Node *defers[DEFER_MAX];
static int ndefers;
/* Loop stack: each `for` records the labels its `break`/`continue`
* target. The continue label is where the iterator step + cond test
* happens; the end label sits past the loop. */
#define LOOP_MAX 16
static const char *loop_cont[LOOP_MAX];
static const char *loop_brk[LOOP_MAX];
static int nloops;
/* Yield-target stack. Each entry is the end label of an enclosing
* match-as-expression; `yield expr;` evaluates expr (AX) and JMPs
* to the topmost entry. */
#define YIELD_MAX 16
static const char *yield_target[YIELD_MAX];
static int nyields;
static int
cg_isfloat(Type *t)
{
/* Transitive chase (#5 F1): the acceptance align opened 2-level
* float/str/slice aliases to these kind classifiers — a single
* peel mis-classed them scalar/INT (ken v3: f64-alias param read
* the wrong register class once the checker admitted it). */
t = type_chase_named(t);
if (t == NULL) return 0;
return t->kind == TY_F32 || t->kind == TY_F64
|| t->kind == TY_UNTYPED_FLOAT;
}
/* type_chase_named lives in cmd/wcc/type.c since the #5 alias arc — the
* checker's acceptance sites share the transitive peel with cgen. */
/* cg_sret_retsize — sret classifier; defined after the tuple register-
* return helpers (tuple_rseq / tuple_eslot / fld_isfloat) it consults
* for the over-cap-tuple arm. Forward-declared here for the earlier
* callers (cgcall, fn prologue). Task #23 / #10. */
static int cg_sret_retsize(Type *rt);
static int
node_isfloat(Node *n)
{
return n && cg_isfloat(n->type);
}
static int
type_isstr(Type *t)
{
t = type_chase_named(t);
if (t == NULL) return 0;
return t->kind == TY_STR || t->kind == TY_UNTYPED_STR;
}
static int
node_isstr(Node *n)
{
return n && type_isstr(n->type);
}
static int
type_isslice(Type *t)
{
t = type_chase_named(t);
return t && t->kind == TY_SLICE;
}
static int
node_isslice(Node *n)
{
return n && type_isslice(n->type);
}
/* node_tuplearg — the underlying TY_TUPLE Type of a tuple-typed argument
* VALUE, else NULL. #163/#32 (C-t2): admits every producer whose cgexpr
* leaves the tuple in the return-ABI cursor (AX/DX/CX/R8 + X0/X1, per
* #164) — a CALL (return ABI), an IDENT (cg_tuple_slot_to_cursor, #241),
* a LITERAL (cg_tuple_lit_to_cursor, #241), a `?`/`!` unwrap
* (cg_tagged_tuple_payload_shift, #241). Pre-C-t2 this was N_CALL-scoped
* and the comment claimed the rest "loud-stop" — they did NOT: a tuple
* ident arg fell to the scalar single-PUSHQ default, skewing every later
* arg register (callee read garbage word 2). The cgcall push site now
* loud-stops any OTHER tuple-typed source shape (rule 7). */
static Type *
node_tuplearg(Node *n)
{
if (n == NULL) return NULL;
if (n->kind != N_CALL && n->kind != N_IDENT && n->kind != N_TUPLE
&& n->kind != N_TRYUNW && n->kind != N_TRYPROP)
return NULL;
Type *t = n->type;
Type *u = type_chase_named(t);
return (u && u->kind == TY_TUPLE) ? u : NULL;
}
/* #83: positional tuple register-return ABI. Tuple elements ride
* consecutive eightbytes over tuple_rseq[]; a slice/str rides its 3-word
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8, ty_str->size SSoT), a
* scalar rides 1. SEND (N_RETURN) and RECEIVE (N_MLET/N_MASSIGN) walk the
* SAME widths so element->register agrees — mirrors harec's
* create_unpack_bindings element walk (ref/harec/src/check.c:1354-1416). */
static const int tuple_rseq[] = { D_AX, D_DX, D_CX, D_R8 };
/* #164 (#107): SysV dual register-class return. A tuple (and, per #171,
* a struct) return places each element by SysV class — a float rides the
* SSE row [X0,X1], everything else the INTEGER row [AX,DX,CX,R8]
* (tuple_rseq) — with the two rows advancing on INDEPENDENT counters, so
* a float lands in the next XMM regardless of its positional slot
* (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},{XMM0,XMM1}}).
* ww extends the INTEGER row to 4 eightbytes; the SSE row keeps SysV's 2.
* tuple_store is the shared per-element receive lowering so the struct-
* return convergence (#171) is a call-site swap, not a redesign. */
static const int tuple_sse_seq[] = { D_X0, D_X1 };
/* #10: the register-return-ABI caps — the SINGLE SSoT shared by the sret
* classifier (cg_sret_retsize over-cap-tuple arm) AND every emit/receive
* site (N_RETURN tuple SEND, N_MLET/N_MASSIGN destructure, cgcall guard).
* Classify and emit MUST agree on these, else a tuple gets classified
* sret by one and in-reg by the other → corruption. */
#define TUPLE_GPCAP ((int)nelem(tuple_rseq))
#define TUPLE_SSECAP ((int)nelem(tuple_sse_seq))
/* tuple_eslot — THE tuple element-stride accessor (#22): the slot a
* tuple element occupies, in bytes. slot = roundup8(size(elem)), 8B a
* FLOOR not a ceiling (user-ratified 2026-06-04): str/slice carry
* their 24B header, a tagged element its full tag+payload box
* ((str,str)=48B predates this; tagged was the one truncated >8B
* kind — the #237 fieldslotsize precedent), narrow scalars pad UP to
* one 8B eightbyte. Every tuple walk (cursor send/receive, t.N read,
* destructure, sret classify, DATA emit) MUST take its stride and
* its eightbyte count (eslot/8) from here — the per-site
* wide=(STR||SLICE)-else-8 predicates this absorbs were the #22
* neighbor-slot/zeros miscompile. Checker twin: check.c N_TTUPLE /
* check.ww tupleelemslot. */
static int
tuple_eslot(Type *t)
{
Type *u = type_chase_named(t);
if (u == NULL) return 8;
if (u->kind == TY_VOID) return 0;
/* a literal tuple's stamped element can be TY_UNTYPED_STR
* (size 0) — it occupies the str header slot (the C-t2
* type_isstr lesson at the arg restage). */
if (u->kind == TY_UNTYPED_STR) return (int)ty_str->size;
if (u->kind == TY_STR || u->kind == TY_SLICE
|| u->kind == TY_TAGGED)
return (int)((u->size + 7) & ~(u64)7);
return 8;
}
static int
type_isf32(Type *t)
{
t = type_chase_named(t);
return t && t->kind == TY_F32;
}
static int
node_isf32(Node *n)
{
return n && type_isf32(n->type);
}
/* fld_isfloat — true iff f's underlying type is f32, f64, or
* untyped_float. The cgen passes float values in X0 (via MOVSD/MOVSS),
* integer/ptr values in AX (via MOVQ). Without this check, a field
* store/load on an f64 slot runs through AX and the bits never reach
* the SSE side — see the vfloat / L.curfval traps documented in
* examples/lisp/CLAUDE.md.
*
* TY_UNTYPED_FLOAT defaults to f64 (no TY_UNTYPED_F32 exists). Every
* field/element/pointee caller passes a declared type that is never
* UNTYPED — adding the case is a no-op for them. The variant-widen
* call site (cg_widen_tagged_store) is the only one passing an
* expression type, where `let _: (i64|f64) = -2.5;` arrives with
* src->type = ty_untyped_float (cunop returns the operand type for
* TK_MINUS, untyped_float for an untyped float literal). The earlier
* narrow predicate dropped the payload via the AX scalar fallback —
* matches cg_isfloat's acceptance set now.
*
* Sets *isf32 to 1 for f32, 0 for f64 / untyped_float. */
static int
fld_isfloat(Type *t, int *isf32)
{
if (isf32) *isf32 = 0;
t = type_chase_named(t);
if (t == NULL) return 0;
if (t->kind == TY_F64) return 1;
if (t->kind == TY_UNTYPED_FLOAT) return 1;
if (t->kind == TY_F32) { if (isf32) *isf32 = 1; return 1; }
return 0;
}
/* cg_sret_retsize — sret classification by natural return size:
* - plain TY_STRUCT > 24B → its natural size (the #23 threshold).
* - TY_TUPLE whose SysV register-return footprint exceeds the caps
* (> TUPLE_GPCAP integer eightbytes or > TUPLE_SSECAP float
* eightbytes) → its natural total size, so the callee returns it
* via sret instead of registers (#10). The element footprint walk
* matches the N_RETURN tuple SEND exactly (a float = 1 SSE
* eightbyte, a slice/str its 3-word header, a scalar 1 GP word).
* - TY_TAGGED whose slot exceeds the AX/DX/CX/R8 cursor
* (> TUPLE_GPCAP eightbytes) → its natural size (#38).
* Everything else (in-cap tuples, in-cap tagged unions, str, slices,
* scalars) routes through its register-return ABI → 0. */
static int
cg_sret_retsize(Type *rt)
{
rt = type_chase_named(rt);
if (rt == NULL) return 0;
if (rt->kind == TY_STRUCT)
return (int)rt->size <= 24 ? 0 : (int)rt->size;
/* #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. */
if (rt->kind == TY_TAGGED) {
if (rt->nullable) return 0;
return (int)rt->size <= TUPLE_GPCAP * 8 ? 0 : (int)rt->size;
}
/* #267: arrays ride the struct-return ABI — same ≤24 reg / >24 sret
* split. Pure-int element arrays only; no float-array-return
* consumer exists, so struct_float_class stays struct-only. */
if (rt->kind == TY_ARRAY)
return (int)rt->size <= 24 ? 0 : (int)rt->size;
if (rt->kind == TY_TUPLE) {
int gptotal = 0, ssecount = 0, f32;
for (Tparam *p = rt->params; p; p = p->next) {
if (fld_isfloat(p->type, &f32))
ssecount++;
else
gptotal += tuple_eslot(p->type) / 8;
}
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
return (int)rt->size;
return 0;
}
return 0;
}
/* fld_issigned — true iff a sub-word field/element load needs sign
* extension (i8 → MOVSBQ, i16 → MOVSWQ, i32 → MOVSXD). Follows NAMED
* and ENUM aliases via type_isunsigned, then peels off the unsigned
* cases (u*, bool, rune) so what remains is the genuinely-signed
* narrow integers. The literal-kind ladder this replaces missed
* TY_ENUM aliases entirely (`type myflag = i8` silently emitted
* MOVZBQ on a field load). */
static int
fld_issigned(Type *t)
{
Type *u = type_chase_named(t);
if (u == NULL) return 0;
if (u->kind == TY_BOOL) return 0;
if (type_isunsigned(u)) return 0;
return type_isint(u);
}
static int
fldloadop(Type *t, int sz)
{
int sigd = fld_issigned(t);
if (sz == 1) return sigd ? A_MOVSBQ : A_MOVZBQ;
if (sz == 2) return sigd ? A_MOVSWQ : A_MOVZWQ;
if (sz == 4) return sigd ? A_MOVSXD : A_MOVL;
return A_MOVQ;
}
static int
fldstoreop(Type *t, int sz)
{
(void)t;
if (sz == 1) return A_MOVB;
if (sz == 2) return A_MOVW;
if (sz == 4) return A_MOVL;
return A_MOVQ;
}
/* castsrcprim — structural (size, unsigned) of an N_CAST's source
* expression, mirroring wwstage's exprprimresolved in
* selfhost/cmd/wcc/cgenutil.ww. The cgen-stage match has to be
* structural, not "use n->type": cstage's checker decorates every
* node with a precise Type, but wwstage has no checker and must
* derive the source type from the AST shape. To keep cstage and
* wwstage emitting byte-identical asm under the #33 identity-width
* identity-sign clamp-skip, both must agree on what a "knowable
* source type" is. The shape menu:
* N_INTLIT — typed literal (`7u32`) via tsuffix.
* N_IDENT, N_CAST — type set by checker; trust it. Wwstage
* reaches the same answer via localfindnode +
* typenodeprimresolved (alias / enum walk)
* and via the cast's rhs type-node.
* N_UN — recurse on operand.
* N_DOT real field — base resolves to TY_STRUCT (or ptr-to);
* use the field's checker-set type. Pseudo-
* fields .len/.cap/.ptr are excluded — they
* are i32 / *T but wwstage's exprprimresolved
* doesn't recognise them, and asymmetry there
* breaks 995_self_rebuild. Tuple positional
* access likewise excluded.
* default — sz=0, identity check fails, clamp emits.
* Matches wwstage's conservative fallback. */
static void
castsrcprim(Node *n, int *sz, int *unsignd)
{
*sz = 0;
*unsignd = 0;
if (n == NULL) return;
Type *t = NULL;
switch (n->kind) {
case N_INTLIT:
/* tsuffix-typed literal: checker resolved n->type via
* lookup_builtin. Untyped int leaves n->type at
* TY_UNTYPED_INT — we conservatively skip those (wwstage
* matches: no tsuffix → sz=0). */
if (n->tsuffix && n->type) {
Type *u = type_chase_named(n->type);
if (u && u->kind != TY_UNTYPED_INT
&& u->kind != TY_UNTYPED_RUNE
&& type_isint(u)) {
t = u;
}
}
break;
case N_IDENT:
case N_CAST:
t = n->type;
break;
case N_UN:
castsrcprim(n->lhs, sz, unsignd);
return;
case N_DOT: {
/* Real struct field only. .len / .cap / .ptr on str /
* slice / array are pseudo-fields wwstage doesn't see. */
Type *bt = n->lhs ? n->lhs->type : NULL;
Type *bu = type_chase_named(bt);
if (bu && bu->kind == TY_PTR)
bu = type_chase_named(bu->sub);
if (bu && bu->kind == TY_STRUCT) {
t = n->type;
}
break;
}
default:
break;
}
Type *u = type_chase_named(t);
if (u && type_isint(u)) {
*sz = (int)u->size;
*unsignd = type_isunsigned(u);
}
}
/* localloadop — read instruction for a scalar local/let load. Same
* dispatch as fldloadop, but keyed on the value's own 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
* already store the value as a sign-extended 8B word so a MOVQ read
* accidentally works; deref-stores are the only path that touches
* fewer bytes than MOVQ reads. Fixing the read makes the slot's
* representation honest regardless of which store path wrote it. */
static int
localloadop(Type *t)
{
int sz = (t && t->size > 0) ? (int)t->size : 8;
if (sz != 1 && sz != 2 && sz != 4) return A_MOVQ;
return fldloadop(t, sz);
}
/* struct ≤16B all-INTEGER: 1 or 2 eightbyte regs.
* Returns 0 if not a struct or too large. */
static int
struct_arg_size(Type *t)
{
/* Transitive chase (#61): `type row = st; type st = struct` is two
* NAMED layers — the single peel classified the param SCALAR, so
* caller and callee both moved one eightbyte of a 3-word struct
* (silent caller-frame garbage reads in the callee). */
t = type_chase_named(t);
if (t == NULL || t->kind != TY_STRUCT) return 0;
return (int)t->size;
}
/* struct_float_class — SysV per-eightbyte classification for the #165
* float-bearing-struct param case (the param twin of #171's struct
* return, classifying per-eightbyte rather than #163's per-element).
* Fills cls[e] = 1 (SSE) / 0 (INTEGER) for each of the struct's 1-2
* eightbytes and returns the eightbyte count, but ONLY for a qualifying
* struct: every eightbyte is either pure-INTEGER or a lone f64 exactly
* filling it, AND at least one is f64. Returns 0 (caller keeps the all-
* GP transport, which is correct + byte-identical for those) when the
* type is not a <=16B struct, has an all-integer layout (no float to
* route), carries an f32 field, packs >1 float into an eightbyte, has a
* float straddling the 8-byte SysV eightbyte boundary, or holds an
* aggregate field (SysV would recurse — out of scope here). f32 / sub-
* eightbyte packing is deferred (#165b). */
static int
struct_float_class(Type *t, int *cls)
{
/* Transitive chase (#61) — same classify choke as struct_arg_size. */
t = type_chase_named(t);
if (t == NULL || t->kind != TY_STRUCT) return 0;
int sz = (int)t->size;
if (sz <= 0 || sz > 16) return 0;
/* SysV classifies aggregates in 8-byte eightbytes (§3.2.3); 8 is
* the eightbyte stride, not a type footprint. */
int nb = (sz > 8) ? 2 : 1;
int nflt[2], nint[2];
nflt[0] = nflt[1] = nint[0] = nint[1] = 0;
for (Tfield *f = t->fields; f; f = f->next) {
Type *fu = type_chase_named(f->type);
if (fu == NULL) return 0;
int foff = (int)f->offset;
int fsz = (int)fu->size;
int e = foff / 8;
if (e < 0 || e >= nb) return 0;
int f32;
if (fld_isfloat(f->type, &f32)) {
if (f32) return 0;
if (foff % 8 != 0 || fsz != 8) return 0;
nflt[e]++;
} else {
if (fu->kind == TY_STRUCT || fu->kind == TY_ARRAY
|| fu->kind == TY_SLICE || fu->kind == TY_STR
|| fu->kind == TY_TAGGED || fu->kind == TY_TUPLE)
return 0;
if (fsz > 8 || (foff + fsz - 1) / 8 != e) return 0;
nint[e]++;
}
}
int hasfloat = 0;
for (int e = 0; e < nb; e++) {
if (nflt[e] == 1 && nint[e] == 0) {
cls[e] = 1;
hasfloat = 1;
} else if (nflt[e] == 0) {
cls[e] = 0;
} else {
return 0;
}
}
return hasfloat ? nb : 0;
}
/* Tagged-union arg byte size: 16 (8B variants) or 24 (16B variants).
* Nullable-folded `(*T | void)` collapses to 8 bytes (just the
* pointer). Returns 0 if not a tagged union or too large to pass
* in registers. */
static int
tagged_arg_size(Type *t)
{
if (t == NULL) return 0;
t = type_chase_named(t);
if (t == NULL || t->kind != TY_TAGGED) return 0;
/* Param/let/struct contexts have 6 int regs (DI..R9) so a 48B
* tagged union (6 words) still fits in registers. Return values
* are stricter (AX:DX:CX, max 24B) — gated separately in
* cgreturn. */
if (t->size > 48) return 0;
return (int)t->size;
}
/* #38b: a tagged-union arg past the 6-reg register convention (>48B
* slot, where tagged_arg_size returns 0) 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.
* ABI shape per ref/qbe/amd64/sysv.c:80-85 (inmem aggregates) /
* :411-426 (stack blit, left-to-right offsets). The ≤48B register
* convention is pinned in-tree (test/926 boundary rows). */
static int
tagged_memarg_size(Type *t)
{
if (t == NULL) return 0;
t = type_chase_named(t);
if (t == NULL || t->kind != TY_TAGGED || t->nullable) return 0;
if (t->size <= 48) return 0;
return (int)t->size;
}
/* cg_tagged_memread — #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 cg_sret_retsize). Every cursor-spill
* consumer must branch on this before reading AX as the tag.
* Family C (#35/#46): a DEREF source is mem-based at ANY size — the
* pointer value IS the box address, so the N_UN(STAR) emitter skips
* the scalar load (which carried only the tag word) and the
* 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. */
static int
cg_tagged_memread(Node *e)
{
Type *u;
if (e == NULL) return 0;
if (e->kind == N_UN && e->op == TK_STAR) {
u = type_chase_named(e->type);
return u && u->kind == TY_TAGGED && !u->nullable
&& u->size > 8;
}
if (e->kind != N_INDEX && e->kind != N_DOT) return 0;
u = type_chase_named(e->type);
if (u == NULL || u->kind != TY_TAGGED) return 0;
return (int)u->size > TUPLE_GPCAP * 8;
}
/* cg_tagged_castpeel — 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 so
* variant-tag lookup sees the cast's type. The nullable one-word
* fold never spills a cursor — excluded. */
static Node *
cg_tagged_castpeel(Node *e)
{
while (e && e->kind == N_CAST && e->lhs) {
Type *cu = type_chase_named(e->type);
Type *iu = type_chase_named(e->lhs->type);
if (cu == NULL || cu->kind != TY_TAGGED || cu->nullable)
break;
if (iu == NULL || iu->kind != TY_TAGGED || iu->nullable)
break;
e = e->lhs;
}
return e;
}
/* cg_tagged_idcastpeel — 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. */
static Node *
cg_tagged_idcastpeel(Node *e)
{
while (e && e->kind == N_CAST && e->lhs
&& type_eq(e->type, e->lhs->type)) {
Type *cu = type_chase_named(e->type);
if (cu == NULL || cu->kind != TY_TAGGED) break;
e = e->lhs;
}
return e;
}
/* type_isnullable — TY_TAGGED with the (*T | void) one-word fold. */
static int
type_isnullable(Type *t)
{
if (t == NULL) return 0;
t = type_chase_named(t);
return t && t->kind == TY_TAGGED && t->nullable;
}
/* nullable_ptr_tag — index of the *T variant in a nullable union.
* Returns 0 or 1; the void variant takes the other slot. */
static int
nullable_ptr_tag(Type *t)
{
if (t == NULL) return 0;
t = type_chase_named(t);
if (t == NULL || t->kind != TY_TAGGED) return 0;
int i = 0;
for (Tparam *p = t->params; p; p = p->next, i++) {
/* peel-ok: single peel PROBE-CLEARED (batch-2 c3-B2,
* 018ef66) — constructible variant params never carry
* 2+-level NAMED at this scan; ww twin cgenutil.ww:2758
* carries the identical annotated peel. */
Type *pu = (p->type && p->type->kind == TY_NAMED)
? p->type->under : p->type;
if (pu && pu->kind == TY_PTR) return i;
}
return 0;
}
static int
node_istaggedarg(Node *n)
{
return n && tagged_arg_size(n->type) > 0;
}
static int
node_isstructarg(Node *n)
{
if (n == NULL) return 0;
int sz = struct_arg_size(n->type);
return sz > 0 && sz <= 16;
}
/* aggarg_size — byte size of a by-value aggregate (struct OR array)
* call arg, else 0. The size axis the ≤16B-struct node_isstructarg
* carve-out doesn't cover: arrays of any size and structs > 16B (#271).
* Pure-int transport only; a float-bearing struct keeps the #165 SSE
* eightbyte path (gated separately at the push/drain sites). */
static int
aggarg_size(Type *t)
{
/* Transitive chase (#61) — same classify choke as struct_arg_size. */
t = type_chase_named(t);
if (t == NULL) return 0;
if (t->kind == TY_STRUCT || t->kind == TY_ARRAY)
return (int)t->size;
return 0;
}
static int
node_isaggarg(Node *n)
{
return n && aggarg_size(n->type) > 0;
}
/* Pick the appropriate scalar SSE opcode (SS vs SD) for a node's
* float type. Untyped float defaults to SD. */
static int
op_for(Node *n, int sd_op, int ss_op)
{
return node_isf32(n) ? ss_op : sd_op;
}
/* Strict variant matcher. Returns 1 iff a value of `src` should be
* tagged as variant `vt` in a tagged-union dispatch:
* - untyped src: first variant whose type can hold it (type_assignable)
* - both NAMED: pointer-identical (same `type` declaration node)
* - one NAMED, the other not: no match (different nominal types)
* - otherwise: structural type_eq
* The pointer-identity rule is what keeps `(str | linerr)` distinguishable
* even though linerr unwraps to str. */
static int
cg_variant_match(Type *vt, Type *src)
{
if (vt == NULL || src == NULL) return 0;
if (type_isuntyped(src)) return type_assignable(vt, src);
if (vt->kind == TY_NAMED && src->kind == TY_NAMED) return vt == src;
if (vt->kind == TY_NAMED || src->kind == TY_NAMED) {
/* #218: nominal identity is lost when the source's stamped
* type was collapsed to its unwrapped tagged (project
* tinfo_lossy_nominal). A NAMED multi-variant union variant vs
* an unwrapped-tagged source can still be THE nested variant —
* fall back to structural equality of the two unwrapped tagged
* unions so the outer widen tag (cg_tag_for_variant) computes.
* Sound only while the model is nominal-lossy; the collision
* guard at the widen site (cg_widen_tagged_store) enforces the
* invariant for when #199b/B-full lands true nominal layout.
* peel-ok (#218, B5-c1): these single peels serve ONLY the
* both-TAGGED structural fallback — a NAMED struct source vs
* a NAMED variant falls through every arm at ANY depth
* (.ai/ken-b5-oracle.md §4: kb5_v2s1i both-wrong-identical
* at depth ONE, gate-blind; kb5_v2sE2 pointer-id exact-match
* works). Chasing here graduates zero rows; the real fix is
* a NEW NAMED-source structural arm under the >=2-candidate
* guard, BOTH stages — task #95. */
Type *vu = (vt->kind == TY_NAMED) ? vt->under : vt;
Type *su = (src->kind == TY_NAMED) ? src->under : src;
if (vu && su && vu->kind == TY_TAGGED && su->kind == TY_TAGGED)
return type_eq(vu, su);
return 0;
}
return type_eq(vt, src);
}
/* cg_variant_struct_match — structural equality of two variants ignoring
* nominal identity (peel NAMED, then type_eq). #218: the collision guard
* at the nested-widen site counts how many du variants share the source's
* *shape*; ≥2 means the structural fallback could not disambiguate them
* once nominal identity is lost. cg_variant_match (pointer-id for both-
* NAMED) would under-count here, so the guard needs the shape-only view. */
static int
cg_variant_struct_match(Type *vt, Type *src)
{
/* peel-ok (#218, B5-c1): shape-only collision count for the
* both-TAGGED fallback above — same probe record, task #95
* (.ai/ken-b5-oracle.md §4). */
Type *vu = (vt && vt->kind == TY_NAMED) ? vt->under : vt;
Type *su = (src && src->kind == TY_NAMED) ? src->under : src;
if (vu == NULL || su == NULL) return 0;
return type_eq(vu, su);
}
/* cg_tagged_success_tag — index of the success variant in a tagged
* union. Mirrors check.c tagged_success_type: explicit-flag mode
* picks the first non-`!`-marked variant; legacy mode picks index 0. */
static int
cg_tagged_success_tag(Type *t)
{
if (t == NULL) return 0;
t = type_chase_named(t);
if (t == NULL || t->kind != TY_TAGGED) return 0;
int has_err = 0;
for (Tparam *p = t->params; p; p = p->next)
if (p->type && p->type->iserror) { has_err = 1; break; }
if (!has_err) return 0;
int idx = 0;
for (Tparam *p = t->params; p; p = p->next, idx++)
if (p->type && !p->type->iserror) return idx;
return 0;
}
static int
cg_variant_is_error(Type *t, int idx)
{
if (t == NULL) return 0;
t = type_chase_named(t);
if (t == NULL || t->kind != TY_TAGGED) return 0;
int has_err = 0;
for (Tparam *p = t->params; p; p = p->next)
if (p->type && p->type->iserror) { has_err = 1; break; }
int i = 0;
for (Tparam *p = t->params; p; p = p->next, i++) {
if (i == idx) {
if (has_err) return p->type && p->type->iserror;
/* legacy: index 0 is success, rest are errors */
return idx != 0;
}
}
return 0;
}
/* Find the variant-tag index of `vt` inside the tagged-union type `t`.
* Returns -1 if `t` is not tagged or `vt` does not match a variant.
* Used by N_MATCH dispatch and by the let/assign/return tag synthesis. */
static int
cg_tag_for_variant(Type *t, Type *vt)
{
if (t == NULL || vt == NULL) return -1;
t = type_chase_named(t);
if (t == NULL || t->kind != TY_TAGGED) return -1;
/* Pass 1: exact match (NAMED-vs-NAMED pointer-id, tagged-vs-tagged,
* bare type_eq). Exact matches take precedence and need no guard —
* distinct variants don't exact-match the same source. */
int idx = 0;
for (Tparam *p = t->params; p; p = p->next, idx++) {
if (cg_variant_match(p->type, vt)) return idx;
}
/* Pass 2 (#15): no exact variant matched — try a structural match of
* a BARE source against a NAMED-alias variant (e.g. a 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 every io.write(&...vt) in cgen's
* emit path. Exact-first (pass 1) keeps a bare `i64` into
* `(i64 | oserror)` binding the exact `i64`, not the alias. drew's
* proviso: guard the structural fallback like the #218 nested-widen
* site — if a bare source structurally matches >=2 NAMED variants,
* nominal layout is needed to disambiguate, so hard-error rather
* than silently first-pick. */
if (vt->kind != TY_NAMED) {
int found = -1, n = 0;
idx = 0;
for (Tparam *p = t->params; p; p = p->next, idx++) {
/* Full chase (F2a batch-4 c2): the old one-level
* unwrap missed a chained ptr-alias variant
* (type a=*X; type b=a) — every pass fell through
* and the widen defaulted to tag 0, SILENT. The
* TY_NAMED gate keeps bare variants in pass-1's
* exact domain; the >=2-candidate hard-error below
* guards the CHASED match set. ww twin
* flatvariantidxt fused in this commit (probe:
* both-wrong-identical pre-fix). */
Type *pu = p->type;
if (pu && pu->kind == TY_NAMED
&& type_eq(type_chase_named(pu), vt)) {
if (found < 0) found = idx;
n++;
}
}
if (n >= 2)
fatal("cg_tag_for_variant: bare source structurally "
"matches >=2 NAMED variants — ambiguous without "
"nominal layout (#15/#218/#199b/#10)");
return found;
}
return -1;
}
static int
type_istagged(Type *t)
{
if (t == NULL) return 0;
t = type_chase_named(t);
return t && t->kind == TY_TAGGED;
}
/* FFI map: ww-side ident name → linker-side symbol name. Built from
* @symbol("real_name") attributes on fn declarations. */
typedef struct Ffi Ffi;
struct Ffi {
const char *ident;
const char *symbol;
Ffi *next;
};
static Ffi *ffi_map;
/* Def-as-string-literal map. `def NAME: str = "lit"` doesn't materialise
* as a real linker symbol; instead, references to NAME load the same
* (ptr, len) pair that the literal would. Avoids needing relocations
* inside DATA blocks for the ptr field of a str header. */
typedef struct Sdef Sdef;
struct Sdef {
const char *name;
const char *mod; /* raw `// MODULE:` directive on the decl,
* or NULL. Mirrors cgfn's c->cur_mod which
* stores the same raw form. */
const char *bytes;
u64 len;
Sdef *next;
};
static Sdef *sdefs;
/* Same-module-first match for Sdef walks. Mirrors wwstage deflookuprhs's
* first pass: returns 1 iff s belongs to the fn we're emitting. Caller
* still re-walks for the any-module fallback. */
static int
sdef_mod_match(Cg *c, Sdef *s)
{
const char *a = s->mod, *b = c->cur_mod;
if (a == b) return 1;
if (a == NULL || b == NULL) return 0;
return strcmp(a, b) == 0;
}
/* Explicit-hint variant for `mod.NAME` N_DOT mod-qualified Sdef walks
* (sister of wwstage deflookuprhsmod). Walk #2 needs n->lhs->str — a
* cross-module qualifier from a third module won't match c->cur_mod
* and would fall back to head-pick, possibly inlining the wrong-module
* strlit when both source modules export the same-leaf str def. */
static int
sdef_mod_match_hint(Sdef *s, const char *hint)
{
const char *a = s->mod;
if (a == hint) return 1;
if (a == NULL || hint == NULL) return 0;
return strcmp(a, hint) == 0;
}
/* Interned string literals — emitted as DATA directives after all
* function bodies, so the linker lays them out alongside .text. */
typedef struct Strlit Strlit;
struct Strlit {
const char *label;
const char *bytes;
u64 len;
Strlit *next;
};
static Strlit *strlits;
static int strlit_seq;
static const char *
intern_strlit(Cg *c, const char *bytes, u64 len)
{
for (Strlit *s = strlits; s; s = s->next)
if (s->len == len && memcmp(s->bytes, bytes, len) == 0)
return s->label;
Strlit *s = amalloc(c->a, sizeof *s);
s->label = aprintf(c->a, "_S_%d", strlit_seq++);
s->bytes = bytes;
s->len = len;
s->next = strlits;
strlits = s;
return s->label;
}
static void
emit_data(Cg *c, FILE *out)
{
for (Strlit *s = strlits; s; s = s->next) {
fprintf(out, "DATA %s(SB),\"", s->label);
for (u64 i = 0; i < s->len; i++) {
unsigned char b = (unsigned char)s->bytes[i];
switch (b) {
case '"': fputs("\\\"", out); break;
case '\\': fputs("\\\\", out); break;
case '\n': fputs("\\n", out); break;
case '\t': fputs("\\t", out); break;
case '\r': fputs("\\r", out); break;
default:
if (b < 0x20 || b >= 0x7f)
fprintf(out, "\\x%02x", b);
else
fputc(b, out);
}
}
/* Trailing NUL: lets `.ptr` be passed to libc / syscalls
* that expect a C string. The `len` field still excludes
* this byte, so iteration semantics are unchanged. */
fputs("\\x00", out);
fputs("\"\n", out);
}
(void)c;
}
static const char *
ffi_resolve(const char *ident)
{
for (Ffi *f = ffi_map; f; f = f->next)
if (strcmp(f->ident, ident) == 0) return f->symbol;
return ident;
}
static void
ffi_collect(Cg *c, Node *file)
{
ffi_map = NULL;
if (file == NULL) return;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_FNDECL) continue;
for (Node *a = d->attr; a; a = a->next) {
if (a->kind != N_ATTR) continue;
if (strcmp(a->str, "symbol") != 0) continue;
if (a->list == NULL || a->list->kind != N_STRLIT) continue;
Ffi *f = amalloc(c->a, sizeof *f);
f->ident = d->str;
f->symbol = a->list->str;
f->next = ffi_map;
ffi_map = f;
}
}
}
/* Module-private symbol map. Mirrors selfhost/cmd/wcc/cgen.ww. Every
* non-FFI top-level fn decl is mangled to <module>.<name> at emission
* time so two modules can each define the same fn leaf — including
* exported ones (lib/os and lib/io both ship `read`/`write`/`close`)
* — without colliding at link time. Non-fn decls (let/def/type) keep
* the older "non-exported only" rule: their export-side namespace is
* the user-facing data ABI and mangling them changes the surface. */
typedef struct Mod Mod;
struct Mod {
const char *name;
const char *module;
Mod *next;
};
static Mod *mod_map;
/* Top-level `let` map. Populated alongside mod_map; consulted by the
* N_IDENT store path and the &-of path to route reads/writes through
* a RIP-relative reference rather than dropping them as the (pre-
* writable-.data) compiler did. emit_lets emits a DATAW for each. */
typedef struct LetVar LetVar;
struct LetVar {
const char *name;
Type *type; /* #128b: imported-let type lookup for module-
* qualified N_INDEX base esz dispatch. */
LetVar *next;
};
static LetVar *letvars;
/* #129 A.2: struct-typed defs that now have DATA storage need the
* same LEAQ-and-field-offset N_DOT-load shape as struct-typed lets.
* Tracked separately so let_islet's existing callers (which gate
* scalar/float/str arms) don't pick up struct defs and re-route their
* narrow-load logic. */
typedef struct DefStruct DefStruct;
struct DefStruct {
const char *name;
Type *type;
DefStruct *next;
};
static DefStruct *defstructs;
/* #129 A.3: array-typed defs now have DATA storage and need the same
* LEAQ name(SB) + indexed-load shape as array-typed lets at cgindex
* and N_DOT base-resolution sites. Mirrors DefStruct (A.2). */
typedef struct DefArray DefArray;
struct DefArray {
const char *name;
Type *type;
DefArray *next;
};
static DefArray *defarrays;
/* #149: every top-level `def`, regardless of kind. Backs the address-of
* path's is-any-def check (loud error on `&<non-addressable def>`) and
* the scalar-addressable gate. Mirrors wwstage collectdefs / deflookup,
* which already track all N_DEF. */
typedef struct DefAny DefAny;
struct DefAny {
const char *name;
Type *type;
Node *rhs;
DefAny *next;
};
static DefAny *defall;
/* Slot size for a top-level `let` of type t, or 0 if the type isn't
* supported as a writable global yet. Tagged unions are deferred.
* enums route through their storage type.
* Keep this tight — extending it requires the matching load/store
* code below.
* The let_* family (this + the five kind-predicates below) chases the
* alias chain transitively (#77/#78 g-fold): a single peel left a
* 2-level-alias global TY_NAMED → size 0 / predicate false → never
* registered, no DATA, and reads fell to the frame-local path at
* offset 0 — silently reading saved BP. */
static int
let_emit_size(Type *t)
{
if (t == NULL) return 0;
Type *u = type_chase_named(t);
if (u == NULL) return 0;
switch (u->kind) {
case TY_BOOL: case TY_RUNE:
case TY_I8: case TY_I16: case TY_I32: case TY_I64:
case TY_U8: case TY_U16: case TY_U32: case TY_U64:
case TY_INT: case TY_UINT: case TY_UINTPTR: case TY_SIZE:
case TY_PTR:
return 8;
case TY_F32:
return 4; /* MOVSS loads/stores 4B via LEAQ+indir. */
case TY_F64:
return 8; /* MOVSD loads/stores 8B via LEAQ+indir. */
case TY_STR:
case TY_SLICE:
return (int)u->size; /* #43: ty_str / ty_slice SSoT. */
case TY_STRUCT:
return (int)u->size; /* zero-init only; field reads/
* scalar-field writes only. */
case TY_ARRAY:
return (int)u->size; /* zero-init only; element
* loads/stores via cgindex. Mirror
* of selfhost letemitsize's
* N_TARRAY branch. */
case TY_TUPLE:
return (int)u->size; /* C-t3 (#48): slot-sum size (C-t0).
* int/str-literal element init via
* emit_tuple_data; element reads via
* the N_DOT t.N global arm. Pre-C-t3
* the 0 here SILENTLY skipped the
* definition and every read saw
* BP-frame garbage. */
default:
return 0;
}
}
/* Is the unwrapped type a str? Used by the load/store paths so the
* (AX, BX) pair convention is preserved for str globals, mirroring
* what we already do for str locals. */
static int
let_isstr(Type *t)
{
if (t == NULL) return 0;
Type *u = type_chase_named(t);
return u && u->kind == TY_STR;
}
/* Is the unwrapped type a slice? Slice globals flow as the (AX, BX,
* CX) triple — same as the local ABI. */
static int
let_isslice(Type *t)
{
if (t == NULL) return 0;
Type *u = type_chase_named(t);
return u && u->kind == TY_SLICE;
}
/* Is the unwrapped type a struct? Struct globals only support field
* access (read + plain `=` write for scalar fields). Whole-struct
* by-value flow through expressions isn't wired. */
static int
let_isstruct(Type *t)
{
if (t == NULL) return 0;
Type *u = type_chase_named(t);
return u && u->kind == TY_STRUCT;
}
/* Is the unwrapped type a fixed-length array? Array globals are
* zero-init DATAW slots; cgindex addresses them as LEAQ name(SB)
* and lets the element load/store run as usual. */
static int
let_isarray(Type *t)
{
if (t == NULL) return 0;
Type *u = type_chase_named(t);
return u && u->kind == TY_ARRAY;
}
/* Is the unwrapped type a float (f32 or f64)? Float globals flow
* through X0 — load/store goes LEAQ name(SB),CX → MOVSS/MOVSD via the
* indirect, since the asm has no D_EXTERN form for SSE moves yet. */
static int
let_isfloat(Type *t)
{
if (t == NULL) return 0;
Type *u = type_chase_named(t);
return u && (u->kind == TY_F32 || u->kind == TY_F64);
}
/* Returns the unwrapped Type — handy when we need to walk struct
* fields. NULL if t is NULL or unresolved. */
static Type *
type_unwrap(Type *t)
{
if (t == NULL) return NULL;
return type_chase_named(t);
}
/* Element-effective type for indexing. For `*[N]T` we drill through
* the pointer to the underlying array so esz/esub reflect T, not the
* whole-array pointee. For everything else returns t unchanged. */
static Type *
idx_eff(Type *t)
{
if (t == NULL) return NULL;
/* Transitive chase (#5 alias arc): the checker now admits index
* bases through 2-level alias chains (F0 8b); a single unwrap left
* eff TY_NAMED → sub NULL → esz=1 byte loads off the chain. */
Type *u = type_chase_named(t);
if (u && u->kind == TY_PTR && u->sub) {
Type *p = type_chase_named(u->sub);
if (p && p->kind == TY_ARRAY) return p;
}
return u;
}
static int
decl_has_ffisym(Node *d)
{
for (Node *a = d->attr; a; a = a->next) {
if (a->kind != N_ATTR) continue;
if (strcmp(a->str, "symbol") == 0) return 1;
}
return 0;
}
/* Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export` for fns.
* Both stages must match exactly — ww2/ww3/ww4 byte-identity depends on it. */
static void
mod_collect(Cg *c, Node *file)
{
mod_map = NULL;
if (file == NULL) return;
for (Node *d = file->list; d; d = d->next) {
int isfn = (d->kind == N_FNDECL);
int track = isfn || (d->kind == N_TYPEDECL)
|| (d->kind == N_DEF) || (d->kind == N_LET);
if (!track) continue;
/* Non-fn decls (let/def/type) still skip exported entries —
* their export-side namespace is the user-facing data ABI
* and mangling them changes the surface. Fns mangle
* unconditionally so cross-module same-leaf exports
* (os.read vs io.read) coexist at link time. */
if (!isfn && d->export) continue;
if (d->module == NULL || d->module[0] == '\0') continue;
if (decl_has_ffisym(d)) continue;
/* `main` is the linker entry-point convention. Even when not
* marked `export`, it must keep its bare name so w6l can
* resolve `_start`'s `CALL main(SB)`. */
if (d->str && strcmp(d->str, "main") == 0) continue;
Mod *m = amalloc(c->a, sizeof *m);
m->name = d->str;
m->module = d->module;
m->next = mod_map;
mod_map = m;
}
}
/* Returns the originating module for a name, or NULL if the name
* isn't a registered private decl. By-name only — works for non-fn
* refs (let/def/type) where the mod_collect skip rule keeps each leaf
* unique across the program. Fn refs go through mod_lookup_for_fn
* since multiple modules can now export the same fn leaf. */
static const char *
mod_lookup(const char *name)
{
for (Mod *m = mod_map; m; m = m->next)
if (strcmp(m->name, name) == 0) return m->module;
return NULL;
}
/* Hint-aware variant for fn names. Walks mod_map looking for a
* (name, hint) pair; returns NULL if there's no leaf-name match at
* all, the hinted module if a match exists, or the first leaf match
* when the caller had no hint. The hint comes from AST shape:
* - N_DOT call `m.fn(...)`: hint = the SK_USE module ident's str.
* - bare N_IDENT call `fn(...)`: hint = c->cur_mod (current fn's
* module — bare names resolve same-module by ww's rules).
* Falling back to the first leaf match preserves the legacy single-
* owner shape for callers that don't (yet) thread a hint. */
static const char *
mod_lookup_for_fn(const char *name, const char *hint)
{
const char *first = NULL;
for (Mod *m = mod_map; m; m = m->next) {
if (strcmp(m->name, name) != 0) continue;
if (hint != NULL && m->module != NULL
&& strcmp(m->module, hint) == 0)
return m->module;
if (first == NULL) first = m->module;
}
return first;
}
/* Value-global variant: mangle ONLY on an exact (name, hint) match;
* otherwise return NULL so the name stays bare. Unlike the fn variant
* there is NO first-leaf-match fallback — exported value globals are
* export-skipped from mod_map (mod_collect keeps their bare-name data
* ABI, see the skip at `!isfn && d->export`), so a first-match fallback
* would mis-mangle an exported `v` onto another module's private `v`
* (#1 cgen value-global module-qualifier, the cgen residual of #55).
* Bare-on-miss is correct: a missing entry means the leaf is either an
* exported global (its own bare symbol) or not module-private at all.
*
* HONEST BOUNDARY (rule 7) — do NOT "fix" the following into a
* workaround: if two modules BOTH export the same value leaf, both stay
* bare and the linker sees a duplicate symbol. That is a CORRECT, loud,
* link-time ABI clash (identical to C's two-extern-same-name rule), NOT
* a silent miscompile. A bare reference can never legitimately resolve
* to another module's PRIVATE global, so first-match is never wanted on
* the value path; the only ambiguity left is genuine duplicate exports,
* which belong to the linker, not to a cgen disambiguation heuristic. */
static const char *
mod_lookup_value(const char *name, const char *hint)
{
if (hint == NULL) return NULL;
for (Mod *m = mod_map; m; m = m->next) {
if (strcmp(m->name, name) != 0) continue;
if (m->module != NULL && strcmp(m->module, hint) == 0)
return m->module;
}
return NULL;
}
/* Collect every top-level `let` whose declared type we can store
* in a single .data slot. Names not in this map fall through to
* the old "drop assignment" path; with a clear link-time
* undefined-symbol error on any read. */
static void
let_collect(Cg *c, Node *file)
{
letvars = NULL;
defstructs = NULL;
defarrays = NULL;
defall = NULL;
if (file == NULL) return;
for (Node *d = file->list; d; d = d->next) {
if (d->kind == N_LET) {
if (d->str == NULL || d->str[0] == '\0') continue;
if (let_emit_size(d->type) == 0) continue;
LetVar *lv = amalloc(c->a, sizeof *lv);
lv->name = d->str;
lv->type = d->type;
lv->next = letvars;
letvars = lv;
continue;
}
if (d->kind == N_DEF) {
if (d->str == NULL || d->str[0] == '\0') continue;
/* #149: track every def (any kind) so the address-of
* path can tell a def from an unknown ident and loud-
* error on `&<non-addressable def>`. */
DefAny *dn = amalloc(c->a, sizeof *dn);
dn->name = d->str;
dn->type = d->type;
dn->rhs = d->rhs;
dn->next = defall;
defall = dn;
/* #129 A.2: struct-typed defs now have DATA storage
* (emit_defs struct arm); register them so the N_DOT
* struct-let LEAQ-and-offset shape widens to cover
* them too. Other def kinds (int / float / str)
* stay on their existing load paths. */
if (let_isstruct(d->type)) {
DefStruct *ds = amalloc(c->a, sizeof *ds);
ds->name = d->str;
ds->type = d->type;
ds->next = defstructs;
defstructs = ds;
continue;
}
/* #129 A.3: array-typed defs now have DATA storage
* (emit_defs array arm); register them so cgindex's
* `let_islet`-gated LEAQ name(SB) base-load widens
* to defs too (LOAD-side twin of the struct-def
* registry). */
if (let_isarray(d->type)) {
DefArray *da = amalloc(c->a, sizeof *da);
da->name = d->str;
da->type = d->type;
da->next = defarrays;
defarrays = da;
}
}
}
}
static int
def_isstructdef(const char *name)
{
if (name == NULL) return 0;
for (DefStruct *ds = defstructs; ds; ds = ds->next)
if (strcmp(ds->name, name) == 0) return 1;
return 0;
}
static int
def_isarraydef(const char *name)
{
if (name == NULL) return 0;
for (DefArray *da = defarrays; da; da = da->next)
if (strcmp(da->name, name) == 0) return 1;
return 0;
}
/* #149: rhs peels (N_CAST / unary ±) to a float literal — the exact
* shape emit_floatlit_data (cgen.c) emits a DATA symbol for. The scalar-
* def address-of gate MUST equal that emission set, or `&def` LEAQs a
* symbol the data pass never wrote. Keep in sync with the peel inside
* emit_floatlit_data. */
static int
floatlit_leaf(Node *rhs)
{
Node *r = rhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
if (r != NULL && r->kind == N_UN
&& (r->op == TK_MINUS || r->op == TK_PLUS)) {
r = r->lhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
}
return r != NULL && r->kind == N_FLOATLIT;
}
/* #149/#147: a scalar (int/float) def is addressable iff emit_defs emits
* a DATA symbol for it — int via fold_int_literal, float via the
* FLOATLIT-leaf shape. Gate is held identical to emit_defs's emission
* gate so the addressable set matches byte-for-byte. Computed-rhs floats
* (`def NAN = 0.0/0.0`, #147) fold to no symbol and are excluded → they
* route to the address-of loud error, never a LEAQ of a missing sym. */
static int
def_isscalardef(const char *name)
{
if (name == NULL) return 0;
for (DefAny *dn = defall; dn; dn = dn->next) {
if (strcmp(dn->name, name) != 0) continue;
if (dn->rhs == NULL) return 0;
u64 v;
if (fold_int_literal(dn->rhs, &v)) return 1;
if (let_isfloat(dn->type) && floatlit_leaf(dn->rhs)) return 1;
return 0;
}
return 0;
}
static int
def_isanydef(const char *name)
{
if (name == NULL) return 0;
for (DefAny *dn = defall; dn; dn = dn->next)
if (strcmp(dn->name, name) == 0) return 1;
return 0;
}
static int
let_islet(const char *name)
{
if (name == NULL) return 0;
for (LetVar *lv = letvars; lv; lv = lv->next)
if (strcmp(lv->name, name) == 0) return 1;
return 0;
}
/* #128b: look up a top-level let's type by leaf name. Sister of
* wwstage's letvartnode (selfhost/cmd/wcc/cgen.ww:999). Used at the
* cgindex / cg_dotbase_addr sites where a module-qualified base
* (`mod.arr`) leaves n->lhs->type NULL (SK_USE-bound module ident),
* so the imported array's element type / size must come through
* this let-map lookup instead. Returns NULL if name isn't a tracked
* top-level let. */
static Type *
let_var_type(const char *name)
{
if (name == NULL) return NULL;
for (LetVar *lv = letvars; lv; lv = lv->next)
if (strcmp(lv->name, name) == 0) return lv->type;
return NULL;
}
/* Glue `<module>.<ident>` into a fresh arena buffer. */
static const char *
mod_join(Cg *c, const char *mod, const char *ident)
{
size_t mn = strlen(mod), in = strlen(ident);
char *buf = amalloc(c->a, mn + 1 + in + 1);
memcpy(buf, mod, mn);
buf[mn] = '.';
memcpy(buf + mn + 1, ident, in);
buf[mn + 1 + in] = '\0';
return buf;
}
/* Mangle an AST identifier into its asm linker symbol:
* - @symbol("...") binding wins (return mapped name).
* - module-private decl → <module>.<name>.
* - else → name unchanged.
* Used at every CALL/MOVQ/LEAQ site that targets an AST name. Plain
* `asym(s)` still emits `s` verbatim — use it for strlit labels and
* hard-coded runtime symbols like "rt_streq". */
static const char *
mod_mangle(Cg *c, const char *ident)
{
const char *resolved = ffi_resolve(ident);
if (resolved != ident) return resolved;
const char *mod = mod_lookup(ident);
if (mod == NULL) return ident;
return mod_join(c, mod, ident);
}
/* Fn-flavoured mangle: same shape as mod_mangle but consults
* mod_lookup_for_fn so the right module wins when multiple modules
* register the same fn leaf. `hint` is the explicit module from a
* N_DOT call site (or c->cur_mod for bare-ident calls); pass NULL
* to get the legacy first-match-wins behaviour. */
static const char *
mod_mangle_fn(Cg *c, const char *ident, const char *hint)
{
const char *resolved = ffi_resolve(ident);
if (resolved != ident) return resolved;
const char *mod = mod_lookup_for_fn(ident, hint);
if (mod == NULL) return ident;
return mod_join(c, mod, ident);
}
/* Value-global flavoured mangle: same shape as mod_mangle_fn but over
* mod_lookup_value (exact-(ident,hint)-or-bare, no first-match
* fallback). See mod_lookup_value for why value globals can't share the
* fn fallback. */
static const char *
mod_mangle_value(Cg *c, const char *ident, const char *hint)
{
const char *resolved = ffi_resolve(ident);
if (resolved != ident) return resolved;
const char *mod = mod_lookup_value(ident, hint);
if (mod == NULL) return ident;
return mod_join(c, mod, ident);
}
/* Forward decl — masym below depends on asym defined further down. */
static Adr asym(const char *s);
static Adr
masym(Cg *c, const char *ident)
{
return asym(mod_mangle(c, ident));
}
/* Fn-name address builder. Use at every CALL/LEAQ site whose target
* is a top-level fn — passes the hint so cross-module same-leaf
* exports resolve to the right module. */
static Adr
mafn(Cg *c, const char *ident, const char *hint)
{
return asym(mod_mangle_fn(c, ident, hint));
}
/* Value-global address builder. masym's non-hinted mod_lookup picks
* the first leaf-name match, so two modules with a same-leaf value
* global (`let v` in both) collapse onto one DATA label and a bare
* cross-module read resolves to the wrong module (#1 cgen value-global
* module-qualifier, the cgen residual of #55). Thread a per-site hint
* the way mafn does — curmod at a bare reference, the decl's own module
* at the definition label — over the same module-generic decl map.
* Kept distinct from mafn (vs renamed) to leave the fn-mangle path
* byte-for-byte untouched. Routes through mod_mangle_value (exact-or-
* bare) so an exported global stays bare instead of mis-mangling onto
* another module's same-leaf private global. */
static Adr
mahint(Cg *c, const char *ident, const char *hint)
{
return asym(mod_mangle_value(c, ident, hint));
}
void
cg_init(Cg *c, Arena *a)
{
memset(c, 0, sizeof *c);
c->a = a;
}
Prog *
newprog(Cg *c, int op)
{
Prog *p = amalloc(c->a, sizeof *p);
p->as = op;
return p;
}
void
emit(Cg *c, Prog *p)
{
if (c->head == NULL) c->head = p;
else c->tail->link = p;
c->tail = p;
}
static Adr
areg(int r)
{
Adr a = { 0 };
a.type = r;
return a;
}
static Adr
aimm(long long v)
{
Adr a = { 0 };
a.type = D_CONST;
a.offset = v;
return a;
}
static Adr
amem(int r, long long off)
{
Adr a = { 0 };
a.type = D_INDIR;
a.reg = r;
a.offset = off;
return a;
}
static Adr
asym(const char *s)
{
Adr a = { 0 };
a.type = D_EXTERN;
a.sym = s;
return a;
}
static Adr
abranch(const char *s)
{
Adr a = { 0 };
a.type = D_BRANCH;
a.sym = s;
return a;
}
static char *
mklabel(Cg *c, const char *prefix)
{
/* Module-qualified to avoid cross-module same-leaf collisions
* (task #13). w6a accepts '.' in label-cont (lex.c:18). */
return aprintf(c->a, "%s%s%s_%s_%d",
c->cur_mod ? c->cur_mod : "",
c->cur_mod ? "." : "",
c->fnname ? c->fnname : "_", prefix, c->labelseq++);
}
static void
ins2(Cg *c, int op, Adr from, Adr to)
{
Prog *p = newprog(c, op);
p->from = from;
p->to = to;
emit(c, p);
}
static void
ins1(Cg *c, int op, Adr to)
{
Prog *p = newprog(c, op);
p->to = to;
emit(c, p);
}
/* tuple_store — store one received tuple element at BP-relative `off`
* from its SysV-class register. A >8B element (slice/str 3-word
* {ptr,len,cap} header, tagged tag+payload box) rides tuple_eslot/8
* consecutive INTEGER cursor words from tuple_rseq[gp..]; a float
* rides tuple_sse_seq[sse] via MOVSD/MOVSS (#105 single-float widened to
* the SSE cursor for #164/#107 multi-float); a scalar rides one INTEGER
* word from tuple_rseq[gp]. The caller owns the dual cursor (validated +
* advanced); this just emits the store. Shared by N_LET/N_MLET/N_MASSIGN
* and, per #171, struct unpack — mirrors wwstage cgenstmt.ww tupstore. */
static void
tuple_store(Cg *c, Type *t, int gp, int sse, int off)
{
int f32 = 0;
int eslot = tuple_eslot(t);
if (eslot == 0)
return; /* void element: the checker's 0-slot */
if (eslot > 8) {
for (int k = 0; k < eslot / 8; k++)
ins2(c, A_MOVQ, areg(tuple_rseq[gp + k]),
amem(D_BP, off + k * 8));
return;
}
if (fld_isfloat(t, &f32)) {
ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(tuple_sse_seq[sse]),
amem(D_BP, off));
return;
}
ins2(c, A_MOVQ, areg(tuple_rseq[gp]), amem(D_BP, off));
}
static void
ins0(Cg *c, int op)
{
emit(c, newprog(c, op));
}
static void
label(Cg *c, const char *s)
{
Prog *p = newprog(c, A_NOP);
p->label = s;
emit(c, p);
}
/* cgslicehdr — load the 24B slice/str header at `base`+0 into the
* (AX=ptr, BX=len, CX=cap) triple. `base` holds the element address;
* the load that targets `base` destroys it, so that word is emitted
* LAST. Order otherwise mirrors the slice-FIELD arm (len, cap, ptr).
* Shared by the N_INDEX str-element arms (caller does the kind-gate)
* and, later, the typeassert str-variant leaf (#9). */
static void
cgslicehdr(Cg *c, int base)
{
if (base != D_BX) ins2(c, A_MOVQ, amem(base, 8), areg(D_BX));
if (base != D_CX) ins2(c, A_MOVQ, amem(base, 16), areg(D_CX));
if (base != D_AX) ins2(c, A_MOVQ, amem(base, 0), areg(D_AX));
if (base == D_BX) ins2(c, A_MOVQ, amem(base, 8), areg(D_BX));
else if (base == D_CX) ins2(c, A_MOVQ, amem(base, 16), areg(D_CX));
else if (base == D_AX) ins2(c, A_MOVQ, amem(base, 0), areg(D_AX));
}
/* ------------------------------------------------------------------ */
/* per-fn local table: name → stack offset (positive = below FP) */
typedef struct Local Local;
struct Local {
const char *name;
int off; /* relative to BP; negative for locals */
Local *next;
};
/* localoff — push a fresh stack slot for this binding and return its
* BP offset. Never dedups by name (post-#27): two `let a: T` in disjoint
* scopes within one fn must each get their own slot, sized to their own
* declared T. Pre-fix the dedup loop returned the first-allocated slot
* regardless of the new declaration's size, so an outer `let a: [128]u8`
* after an inner `let a: i64` would collapse onto the 8B slot and
* `a[127]` would land at +119(BP), past the saved RIP, into the
* caller's frame. localfind walks from the head, so the most recent
* binding still wins lookups inside its scope. */
static int
localoff(Cg *c, Local **head, const char *name, int size, int *frame)
{
int al = 8;
*frame = (*frame + size + al - 1) & ~(al - 1);
int off = -*frame;
Local *l = amalloc(c->a, sizeof *l);
l->name = name;
l->off = off;
l->next = *head;
*head = l;
return off;
}
/* local_alloc — synonym for localoff. Pre-#27 localoff deduped by name
* and local_alloc was the always-fresh escape hatch (match-arm bindings,
* synthetic scratch slots). Post-#27 localoff is also always-fresh, so
* the two are functionally identical; both names are kept so the call
* sites read intentfully (let-decl vs scratch). */
static int
local_alloc(Cg *c, Local **head, const char *name, int size, int *frame)
{
int al = 8;
*frame = (*frame + size + al - 1) & ~(al - 1);
int off = -*frame;
Local *l = amalloc(c->a, sizeof *l);
l->name = name;
l->off = off;
l->next = *head;
*head = l;
return off;
}
static int
localfind(Local *head, const char *name)
{
for (Local *l = head; l; l = l->next)
if (strcmp(l->name, name) == 0) return l->off;
return 0; /* 0 = not found (caller must verify) */
}
/* cg_tagscr_slot — the ONLY alloc path for the per-fn tagged scratch
* (#44). One cached slot per distinct slot size, named "@tagscr<sz>"
* so wwstage's localadd name-dedup keys the same way; first-use
* allocation order is the source order in both stages (byte-id). */
static int
cg_tagscr_slot(Cg *c, Local **locals_p, int sz)
{
for (int i = 0; i < cg_ntagscr; i++)
if (cg_tagscr_sz[i] == sz)
return cg_tagscr_off[i];
if (cg_ntagscr >= CG_NTAGSCR)
fatal("cg_tagscr_slot: more than %d distinct tagged "
"scratch sizes in one fn", CG_NTAGSCR);
cg_tagscr_off[cg_ntagscr] = local_alloc(c, locals_p,
aprintf(c->a, "@tagscr%d", sz), sz, cg_frame);
cg_tagscr_sz[cg_ntagscr] = sz;
cg_ntagscr++;
return cg_tagscr_off[cg_ntagscr - 1];
}
/* cg_base_cap — load the capacity of a sub-slice's UNDERLYING storage
* into `dst` for the #20 cap = base_cap - lo formula (drew: harec
* eval.c:1017 slice cap-=start / eval.c:1024 array cap=length-start;
* ensure.ha:4-8 distinct capacity field). array [N]T -> N (literal);
* slice/str -> the .capacity word carried in the header at +16 (the
* +16 load mirrors the hi-default +8 length dispatch, but emitted
* unconditionally). Returns 0 when base_cap isn't cleanly available so
* the caller keeps the prior cap=len: a non-ident base (cgexpr already
* discarded its header cap; recomputing would re-evaluate a possibly
* side-effecting base -- #74, which also owns the pre-existing
* defaulted-hi len gap there), or a GLOBAL str base (wwstage cgslice
* has no global-str load, #73 -- matching it keeps the stages
* byte-identical rather than introducing a fresh divergence). */
static int
cg_base_cap(Cg *c, Node *base, Type *bu, Local *locals, int dst)
{
if (!base || base->kind != N_IDENT)
return 0;
if (bu && bu->kind == TY_ARRAY) {
ins2(c, A_MOVQ, aimm((long long)bu->alen), areg(dst));
return 1;
}
if (bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) {
int boff = localfind(locals, base->str);
int isglobal = (boff == 0) && let_islet(base->str);
if (isglobal && bu->kind == TY_STR)
return 0;
if (isglobal) {
ins2(c, A_LEAQ, masym(c, base->str), areg(dst));
ins2(c, A_MOVQ, amem(dst, 16), areg(dst));
} else {
ins2(c, A_MOVQ, amem(D_BP, boff + 16), areg(dst));
}
return 1;
}
return 0;
}
/* ------------------------------------------------------------------ */
/* expressions: result lands in AX. Returns 1 on success. */
static void cgexpr(Cg*, Node*, Local*);
static void cgstmt(Cg*, Node*, Local**, int*);
static void cg_widen_tagged_push(Cg*, Local**, Type*, Node*, int);
static void cg_widen_tagged_store(Cg*, Local**, Type*, Node*, int, int, int);
static void cg_widen_tag_remap(Cg*, Type*, Type*, int);
/* cg_dotchain_addr — compute the ADDRESS of a dot/ident lvalue chain
* into `dst_reg`, dereferencing pointer links mid-chain. Returns 1 on
* success, 0 if a link isn't a struct / ptr-to-struct it can resolve.
* Recursion mirrors the read spine (cgen.c:3722 value-struct field /
* :4033 ptr-field): for `x.f`, recurse to &x, deref if x is a *struct
* (so dst holds the pointee base), then add f's offset. Touches ONLY
* dst_reg — no AX, no stack — so it honours cg_dotbase_addr's caller-
* spill contract. The chained-base arm of cg_dotbase_addr (#253) is its
* sole caller. */
static int
cg_dotchain_addr(Cg *c, Node *node, int dst_reg, Local *locals)
{
if (node == NULL) return 0;
if (node->kind == N_IDENT) {
int off = localfind(locals, node->str);
if (off != 0) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(dst_reg));
return 1;
}
if (let_islet(node->str) || def_isstructdef(node->str)) {
ins2(c, A_LEAQ, masym(c, node->str), areg(dst_reg));
return 1;
}
return 0;
}
if (node->kind != N_DOT) return 0;
Node *x = node->lhs;
if (x == NULL) return 0;
Type *xt = x->type;
if (xt == NULL || xt == ty_err) return 0;
Type *xu = type_chase_named(xt);
if (xu == NULL) return 0;
int xviaptr = 0;
Type *st = NULL;
if (xu->kind == TY_PTR) {
Type *p = type_chase_named(xu->sub);
if (p && p->kind == TY_STRUCT) { st = p; xviaptr = 1; }
} else if (xu->kind == TY_STRUCT) {
st = xu;
}
if (st == NULL) return 0;
Tfield *f = NULL;
for (Tfield *fl = st->fields; fl; fl = fl->next)
if (strcmp(fl->name, node->str) == 0) { f = fl; break; }
if (f == NULL) return 0;
if (!cg_dotchain_addr(c, x, dst_reg, locals)) return 0;
if (xviaptr)
ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg));
if ((int)f->offset != 0)
ins2(c, A_ADDQ, aimm((int)f->offset), areg(dst_reg));
return 1;
}
/* cg_dotbase_addr — compute &(inner.field) into `dst_reg` for an
* N_DOT base where `inner` is an N_IDENT local (struct value OR *struct
* pointer) OR a chained N_DOT (#253: `o.p.m` / `o.i.m` / `o.a.b.m`).
* Returns 1 if emitted, 0 if base shape isn't supported (the
* caller falls back to its prior `cgexpr(base); MOVQ AX, dst_reg`).
*
* #135: cgexpr on an N_DOT whose .field is a `[N]T`-typed field auto-
* derefs and loads the field's 8-byte VALUE as if it were a pointer.
* For an LHS or index-base shape (`d.fld[i] = v` / `d.fld[i]` read /
* `d.fld[i] OP= v`), the caller wants the field's ADDRESS — this helper
* supplies it inline, avoiding the value-load. Mirror primitive of the
* inverse template at cgen.c arr[i].field (the cgdot N_INDEX-lhs
* branch).
*
* #253: a chained inner (`inner` is itself an N_DOT) routes through
* cg_dotchain_addr to recover the container's base — the pointer VALUE
* of inner when inner is a *struct (viaptr), else the ADDRESS of inner
* — then adds the array field's offset. Closes the whole array-field-
* base-address family across every op (index r/w, addr-of, slice,
* compound) since all of them route through this helper.
*
* Caller-spill contract: the helper does NOT touch AX unless
* dst_reg == D_AX. Safe to call where AX holds an unrelated live value
* (BX dst); cg_dotchain_addr keeps the same contract. */
static int
cg_dotbase_addr(Cg *c, Node *base, int dst_reg, Local *locals)
{
if (base == NULL || base->kind != N_DOT) return 0;
Node *inner = base->lhs;
if (inner == NULL) return 0;
int chained = (inner->kind == N_DOT);
if (inner->kind != N_IDENT && !chained) return 0;
Type *bt = inner->type;
/* #128b: module-qualified `mod.arr` where arr is an imported
* top-level `let X: [N]T`. The checker leaves SK_USE module-idents
* with NULL/ty_err type; detect via let_islet + let_var_type-of-
* TY_ARRAY and emit LEAQ X(SB) for the array's base address.
* Without this, the N_INDEX fallback at cgen.c:~6760 falls to
* cgexpr(base) which auto-MOVQs the symbol contents as if it
* were a pointer-var (= load 8 bytes of the array's first
* elements + treat as junk address) — segfault-class miscompile. */
if (bt == NULL || bt == ty_err) {
if (let_islet(base->str)) {
Type *lt = let_var_type(base->str);
Type *lu = type_chase_named(lt);
if (lu && lu->kind == TY_ARRAY) {
ins2(c, A_LEAQ, masym(c, base->str),
areg(dst_reg));
return 1;
}
}
return 0;
}
Type *bu = type_chase_named(bt);
if (bu == NULL) return 0;
int viaptr = 0;
Type *struct_t = NULL;
if (bu->kind == TY_PTR) {
Type *st = type_chase_named(bu->sub);
if (st && st->kind == TY_STRUCT) { struct_t = st; viaptr = 1; }
} else if (bu->kind == TY_STRUCT) {
struct_t = bu;
}
if (struct_t == NULL) return 0;
Tfield *f = NULL;
for (Tfield *fl = struct_t->fields; fl; fl = fl->next)
if (strcmp(fl->name, base->str) == 0) { f = fl; break; }
if (f == NULL) return 0;
/* Only fire on `[N]T` fields — the field's storage IS the array
* data inline, so taking the address-of-field gives `&arr[0]`.
* For `*T` / `[]T` / `str` fields, the existing cgexpr(base) path
* is correct (loads the pointer value, then adds the scaled
* index); over-firing here would skip the deref and treat the
* pointer/slice/str field as an inline array. */
Type *ft = type_chase_named(f->type);
if (ft == NULL || ft->kind != TY_ARRAY) return 0;
int foff = (int)f->offset;
/* #253: chained inner — compute the container base via the dot-chain
* spine (pointer VALUE of inner when viaptr, else its ADDRESS), then
* add the field offset. cg_dotchain_addr keeps the spill contract. */
if (chained) {
if (!cg_dotchain_addr(c, inner, dst_reg, locals)) return 0;
if (viaptr)
ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg));
if (foff != 0)
ins2(c, A_ADDQ, aimm(foff), areg(dst_reg));
return 1;
}
int inner_off = localfind(locals, inner->str);
/* #249 (sibling of #135): a module-GLOBAL struct value base. localfind
* returns 0 for a global, so the BP-rel form below would emit `LEAQ
* (BP)` (read the stack frame, not the global). Resolve the same way
* the scalar N_DOT global-field read does (cgen.c:7532) — LEAQ
* name(SB) + field offset. const globals are def_isstructdef. */
if (viaptr) {
ins2(c, A_MOVQ, amem(D_BP, inner_off), areg(dst_reg));
if (foff != 0)
ins2(c, A_ADDQ, aimm(foff), areg(dst_reg));
} else if (inner_off == 0 && (let_islet(inner->str)
|| def_isstructdef(inner->str))) {
ins2(c, A_LEAQ, masym(c, inner->str), areg(dst_reg));
if (foff != 0)
ins2(c, A_ADDQ, aimm(foff), areg(dst_reg));
} else {
ins2(c, A_LEAQ, amem(D_BP, inner_off + foff),
areg(dst_reg));
}
return 1;
}
/* aggarg_srcaddr — land the ADDRESS of an addressable aggregate arg
* source in `dst`, reusing the closed #265/#268 let-init-copy dispatch:
* ident/global slot (LEAQ), deref operand (cgexpr of the pointer),
* N_DOT field (cg_dotchain_addr, #253), N_INDEX element (the &base[i]
* spine, #252/#270). Returns 0 for a source kind not covered (caller
* loud-stops, rule 7). The CALL source is handled separately at the
* push site (receive-to-regs / sret-to-scratch). */
static int
aggarg_srcaddr(Cg *c, Node *src, int dst, Local *locals)
{
if (src->kind == N_UN && src->op == TK_STAR) {
cgexpr(c, src->lhs, locals);
if (dst != D_AX)
ins2(c, A_MOVQ, areg(D_AX), areg(dst));
return 1;
}
if (src->kind == N_IDENT) {
int soff = localfind(locals, src->str);
if (soff != 0) {
ins2(c, A_LEAQ, amem(D_BP, soff), areg(dst));
return 1;
}
/* global value source. Gated to a module-`let` (let_islet,
* the wwstage letvartnode twin); a const array/struct `def`
* aggregate ARG is untested and out of scope (#274) — both
* stages fall through to the caller's loud-stop, aligned DOWN
* to the leaner wwstage per rule-10. */
if (let_islet(src->str)) {
ins2(c, A_LEAQ, masym(c, src->str), areg(dst));
return 1;
}
return 0;
}
if (src->kind == N_DOT)
return cg_dotchain_addr(c, src, dst, locals);
if (src->kind == N_INDEX) {
Node *base = src->lhs;
Node *idx = src->rhs;
Type *bt = base ? base->type : NULL;
Type *bu = type_chase_named(bt);
if (base && base->kind == N_IDENT && bu
&& bu->kind == TY_ARRAY) {
int esz = (bu->sub) ? (int)bu->sub->size : 1;
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
int boff = localfind(locals, base->str);
if (boff != 0)
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX));
else
ins2(c, A_LEAQ, masym(c, base->str),
areg(D_BX));
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
if (dst != D_AX)
ins2(c, A_MOVQ, areg(D_AX), areg(dst));
return 1;
}
return 0;
}
return 0;
}
/* cg_aggcopy — the ONE place-resolved mem-to-mem aggregate copy:
* sz bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail.
* Extracted verbatim from the C1.25 assign-resolver tail so every
* aggregate copy position (resolver field store, #49 ident reassign,
* #49 structlit fill-field) funnels through one loop — close-by-
* construction, no per-site width logic to skew. */
static void
cg_aggcopy(Cg *c, int sz)
{
int k = 0;
for (; k + 8 <= sz; k += 8) {
ins2(c, A_MOVQ, amem(D_SI, k), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, k));
}
if (k + 4 <= sz) {
ins2(c, A_MOVL, amem(D_SI, k), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_BX, k));
k += 4;
}
if (k + 2 <= sz) {
ins2(c, A_MOVW, amem(D_SI, k), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_BX, k));
k += 2;
}
if (k + 1 <= sz) {
ins2(c, A_MOVB, amem(D_SI, k), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_BX, k));
k += 1;
}
}
/* cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue)
* expression into dst_reg; returns 1 when the shape is wired, 0
* otherwise (the caller loud-stops — rule 7, never a silent drop).
* F6 resolver, commit C1: `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop
* over a slice/array place, N_DOT struct-field hop with one deref for
* a *struct base. C2 (F4 read-walker) adds the N_IDENT root (local /
* let / DATA-backed def) so indexed-ident spines (`threads[0].cap.end`)
* resolve too. Enumerated arms still win at every dispatch site (they
* are checked first), so shapes that worked pre-C1 keep their asm; the
* F5 let-copy accretes here in a follow-up commit. ADDRESS COMPUTATION
* ONLY — every call-site keeps its own load/store/copy emission.
* Clobbers AX/CX (cgexpr on index / pointer operands) and balances
* its own PUSHQ/POPQ; dst_reg must not be AX or CX. */
static int
cgplaceaddr(Cg *c, Node *n, int dst_reg, Local *locals)
{
if (n == NULL) return 0;
if (n->kind == N_IDENT) {
int off = localfind(locals, n->str);
if (off != 0) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(dst_reg));
return 1;
}
if (let_islet(n->str) || def_isstructdef(n->str)
|| def_isarraydef(n->str)) {
ins2(c, A_LEAQ, masym(c, n->str), areg(dst_reg));
return 1;
}
return 0;
}
if (n->kind == N_UN && n->op == TK_STAR) {
/* &(*e) is e's value — no load. */
cgexpr(c, n->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(dst_reg));
return 1;
}
if (n->kind == N_INDEX) {
Node *base = n->lhs;
Node *idx = n->rhs;
if (base == NULL || idx == NULL) return 0;
/* C2: any addressable base — recursion decides (deref /
* ident / dot / index spine). Ident-rooted shapes with
* enumerated arms never reach the resolver (those arms
* dispatch first), so their asm is untouched. */
Type *bu = type_chase_named(base->type);
if (bu == NULL) return 0;
if (bu->kind != TY_SLICE && bu->kind != TY_ARRAY)
return 0;
Type *et = type_chase_named(n->type);
if (et == NULL) return 0;
int esz = (int)et->size;
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX));
if (!cgplaceaddr(c, base, dst_reg, locals)) return 0;
/* A slice place holds the {ptr,len,cap} header — the
* element base is its .ptr word; an array place IS the
* element storage. */
if (bu->kind == TY_SLICE)
ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg));
ins1(c, A_POPQ, areg(D_AX));
ins2(c, A_ADDQ, areg(D_AX), areg(dst_reg));
return 1;
}
if (n->kind == N_DOT) {
Node *base = n->lhs;
if (base == NULL) return 0;
Type *bu = type_chase_named(base->type);
if (bu == NULL) return 0;
int viaptr = 0;
Type *st = NULL;
if (bu->kind == TY_PTR) {
Type *p = type_chase_named(bu->sub);
if (p && p->kind == TY_STRUCT) { st = p; viaptr = 1; }
} else if (bu->kind == TY_STRUCT) {
st = bu;
}
if (st == NULL) return 0;
Tfield *f = NULL;
for (Tfield *fl = st->fields; fl; fl = fl->next)
if (strcmp(fl->name, n->str) == 0) { f = fl; break; }
if (f == NULL) return 0;
if (!cgplaceaddr(c, base, dst_reg, locals)) return 0;
if (viaptr)
ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg));
if ((int)f->offset != 0)
ins2(c, A_ADDQ, aimm((int)f->offset), areg(dst_reg));
return 1;
}
return 0;
}
/* FA1 (#15): append() header-place access, cgplaceaddr's append
* consumer. direct = ident-local header in the frame (BP-disp — the
* legacy emission, kept byte-identical); indirect = header address
* pre-spilled to @apphdrscr by the resolver. grow = len+=1, &hdr→DI,
* esz→SI, CALL rt_ensure. In indirect mode the len bump goes through
* DI so the loaded address doubles as the call argument. */
static void
cg_append_grow(Cg *c, int direct, int off, int scr, int esz)
{
if (direct) {
ins2(c, A_ADDQ, aimm(1), amem(D_BP, off + 8));
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_DI));
} else {
ins2(c, A_MOVQ, amem(D_BP, scr), areg(D_DI));
ins2(c, A_ADDQ, aimm(1), amem(D_DI, 8));
}
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
}
/* Post-rt_ensure slot address: CX = (len-1)*esz, dst = .ptr + CX.
* Clobbers AX (the IMUL immediate) and CX, like the emission it
* replaces; dst must not be AX or CX. */
static void
cg_append_slot(Cg *c, int direct, int off, int scr, int esz, int dst)
{
if (direct) {
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_CX));
} else {
ins2(c, A_MOVQ, amem(D_BP, scr), areg(dst));
ins2(c, A_MOVQ, amem(dst, 8), areg(D_CX));
}
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
if (direct)
ins2(c, A_MOVQ, amem(D_BP, off), areg(dst));
else
ins2(c, A_MOVQ, amem(dst, 0), areg(dst));
ins2(c, A_ADDQ, areg(D_CX), areg(dst));
}
/* cg_structlit_fill modes — see helper docstring. */
enum {
DST_BP = 0,
DST_PTR_LOCAL = 1,
DST_GLOBAL = 2,
};
static void cg_structlit_fill(Cg*, Local**, Type*, Node*, int, int, const char*, int);
static void cg_structlit_fill_bp(Cg*, Local**, Type*, Node*, int);
static void
cgexpr_int(Cg *c, long long v)
{
ins2(c, A_MOVQ, aimm(v), areg(D_AX));
}
/* Materialise a float constant in X0: MOVQ the IEEE bits into AX, PUSH,
* MOVSD off the stack into X0. Shared by N_FLOATLIT and the f64/f32-typed
* N_INTLIT arm (#103 FACE X): a no-decimal `0f64`/`8f64` is an N_INTLIT
* carrying float TYPE, so it must reach X0 like a true float literal does
* — the integer-immediate path left the value stranded in AX, so an SSE
* compare/mul read a stale X0. */
static void
cgexpr_float(Cg *c, double val)
{
union { double d; u64 u; } x;
x.d = val;
ins2(c, A_MOVQ, aimm((long long)x.u), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVSD, amem(D_SP, 0), areg(D_X0));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
}
/* cg_widen_tag_remap — when widening from one tagged union to another,
* rewrite the source's variant tag at BP+slot_off+0 to use the dst
* union's variant indices. No-op when src and dst index orders coincide.
*
* Mirrors Hare's tagged-subset assignment: a value of type (A|B) flows
* into (A|B|C) by re-tagging the discriminator to the position the
* variant occupies in the wider union. Both must already match by
* cg_variant_match — the checker enforces that.
*
* Emits a CMPQ-chain switch over the source tag because w6a has no
* CMOVQ encoding. The chain is linear in nvariants; in practice tagged
* unions are small. */
static void
cg_widen_tag_remap(Cg *c, Type *du, Type *su, int slot_off)
{
if (du == NULL || du->kind != TY_TAGGED) return;
if (su == NULL || su->kind != TY_TAGGED) return;
int identity = 1, idx = 0;
for (Tparam *p = su->params; p; p = p->next, idx++) {
int di = cg_tag_for_variant(du, p->type);
if (di < 0) di = 0;
if (di != idx) { identity = 0; break; }
}
if (identity) return;
const char *done = mklabel(c, "remap_done");
ins2(c, A_MOVQ, amem(D_BP, slot_off + 0), areg(D_AX));
idx = 0;
for (Tparam *p = su->params; p; p = p->next, idx++) {
const char *next = mklabel(c, "remap_next");
int di = cg_tag_for_variant(du, p->type);
if (di < 0) di = 0;
ins2(c, A_CMPQ, aimm(idx), areg(D_AX));
ins1(c, A_JNE, abranch(next));
ins2(c, A_MOVQ, aimm(di), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0));
ins1(c, A_JMP, abranch(done));
label(c, next);
}
label(c, done);
}
/* cg_widen_tagged_store — write the tagged-union slot bytes for `src`
* into base_reg+slot_off, sized to `sz` (8 for nullable fold, else
* 16/24+). Used by call-site widening (via cg_widen_tagged_push) and
* by the let/assign/return/struct-field-init paths.
*
* base_reg picks the addressing root for every write:
* - D_BP: function-frame slot. The original layout — callers pass
* a BP-relative slot_off and the function writes directly.
* - else (e.g. D_BX for a *struct field, D_CX for a top-level
* struct field): pointer-rooted dst. cgexpr inside this function
* trashes every GPR, so we can't carry base_reg across — instead
* we route every write through a fresh BP-rooted scratch slot,
* reload base_reg from a temp spill at the end, and word-copy
* scratch → (base_reg, slot_off). Caller is responsible for
* loading base_reg with the dst address before the call; the
* function preserves it across cgexpr via the spill.
*
* Branches by source shape (tagged_arg_size > 0 source counts as a
* tagged subset — possibly with different variant indices):
* - nullable: dst is folded (*T|void); store pointer at +0.
* - tagged ident: byte-copy slot words then remap tag at +0.
* - tagged expression: cgexpr leaves AX=tag, DX=val0, [CX=val1] —
* spill into slot then remap.
* - struct ident: zero-fill, byte-copy struct words to +8.
* - struct literal: zero-fill, store each field at slot+8+field_off.
* - str: cgexpr leaves AX=ptr, BX=len.
* - scalar: cgexpr leaves AX; store at +8 with zero pad. */
static void
cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
int base_reg, int slot_off, int sz)
{
/* For pointer-rooted dst, materialise into a BP-rooted scratch
* slot — body writes via `amem(D_BP, write_off + k)` — then copy
* out. Spill base_reg first so cgexpr can clobber freely. */
int via_outer = (base_reg != D_BP);
int base_spill = 0;
int write_off = slot_off;
if (via_outer) {
if (cg_tagbase != 0) {
base_spill = cg_tagbase;
} else {
base_spill = local_alloc(c, locals_p, "@tagbase", 8,
cg_frame);
cg_tagbase = base_spill;
cg_tagbase_sz = 8;
}
ins2(c, A_MOVQ, areg(base_reg), amem(D_BP, base_spill));
write_off = cg_tagscr_slot(c, locals_p, sz);
/* Pre-zero so str/scalar branches (which leave high words
* untouched when sz exceeds the variant's footprint) still
* deliver a clean slot to the copy-out. */
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
}
Type *du = type_chase_named(dst);
if (du == NULL || du->kind != TY_TAGGED) return;
if (du->nullable) {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
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 cursor arm, whose cgexpr collapsed to
* word0 (`let w: un3 = (v: un3)` stored garbage payload). */
src = cg_tagged_castpeel(src);
/* `expr: TaggedAlias` where the cast's destination IS the union
* itself is a widening, not a re-interpret. cgexpr on the cast
* leaves the inner expression's register shape (str: AX=ptr,
* BX=len), not the tagged AX/DX/CX triple — so route through the
* concrete-variant branches below by peeling the cast. Casts to
* a concrete variant (`7: i32`) keep their type for proper tag
* lookup and fall through to the matching branch. */
if (src && src->kind == N_CAST && src->lhs) {
Type *castt = src->type;
Type *castu = type_chase_named(castt);
Type *innert = src->lhs->type;
Type *innu = type_chase_named(innert);
int cast_is_widen = (castu == du) ||
(castu && castu->kind == TY_TAGGED && type_eq(castt, dst));
int inner_is_tagged = innu && innu->kind == TY_TAGGED;
if (cast_is_widen && !inner_is_tagged) {
src = src->lhs;
}
}
Type *st = src ? src->type : NULL;
/* Transitive chase (#62 Layer-2): a 2-level chain ali->base->struct
* left su TY_NAMED, so the alias-named union member fell past the
* struct arm to the SCALAR store — word0-only payload, words 1+
* zero-filled (both-wrong-identical with wwstage at normal decl
* order; gate-blind). The tag still keys on the un-chased st: the
* member's nominal identity (cg_tag_for_variant) is the alias. */
Type *su = type_chase_named(st);
/* Tagged → tagged subset: copy slot words then tag-remap. */
if (su && su->kind == TY_TAGGED) {
int ssz = (int)su->size;
/* #218: is the source itself a single NESTED variant of du
* (its whole tagged type matches one du variant), rather than
* a flattened SUBSET whose members spread into du? If so, the
* inner tagged value is the payload: store it at slot+8 with
* the outer tag at slot+0, exactly like the scalar/struct/str
* single-variant arms below — NOT a copy-to-+0 + sub-variant
* remap. cg_tag_for_variant's structural fallback (cgen.c
* cg_variant_match) is what recovers the index after the
* nominal-lossy collapse. */
int nested = cg_tag_for_variant(du, st);
if (nested >= 0) {
/* drew collision guard: the structural fallback over-
* matches if ≥2 nominally-distinct du 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. */
int nmatch = 0;
for (Tparam *p = du->params; p; p = p->next)
if (cg_variant_struct_match(p->type, st))
nmatch++;
if (nmatch >= 2)
fatal("cg_widen_tagged_store: structural fallback "
"cannot disambiguate nominally-distinct same-"
"shape variants without nominal layout "
"(#218/#199b/B-full)");
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
if (src->kind == N_IDENT) {
int soff = localfind(*locals_p, src->str);
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ, amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8 + k));
}
} 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 == N_CALL
&& cg_sret_retsize(st) > 0)
fatal("#40: sret-class call result "
"cannot be cursor-widened into a "
"tagged slot (mem-to-mem widen "
"unwired)");
if (cg_tagged_memread(src)) {
/* #37: >32B box read — ADDRESS in
* AX; copy the inner box from memory
* into the payload area. */
cgexpr(c, src, *locals_p);
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ,
amem(D_AX, k),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP,
write_off + 8 + k));
}
} else {
/* #37 (rule 7): >32B from a non-mem-based
* kind would spill an unfilled cursor. */
if (ssz > TUPLE_GPCAP * 8)
fatal("#37: >32B tagged payload from "
"a non-mem-based source (kind %d) "
"unwired (rule 7)", src->kind);
/* Family C catch-all (rule 7): a tagged
* cast surviving cg_tagged_castpeel (cast
* to a THIRD union) has no cursor — loud,
* not word0 garbage. */
if (src->kind == N_CAST)
fatal("#35: tagged cast source shape "
"unwired at the widen nested arm "
"(rule 7)");
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8));
if (ssz > 8)
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, write_off + 16));
if (ssz > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, write_off + 24));
if (ssz > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, write_off + 32));
}
}
ins2(c, A_MOVQ, aimm(nested),
amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
return;
}
if (src->kind == N_IDENT) {
int soff = localfind(*locals_p, src->str);
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ, amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
}
} else if (cg_tagged_memread(src)) {
/* #37: >32B box read (insts[pc], t.N, s.f) —
* cgexpr left its ADDRESS in AX; copy the whole
* box from memory. Pad + remap below are shared
* with the ident path (both mem-based). */
cgexpr(c, src, *locals_p);
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ, amem(D_AX, k), areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, write_off + k));
}
} else {
/* Tagged source returned via the tagged-return ABI
* (AX=tag, DX=word0, CX=word1, R8=word2). The unused
* ABI words are zeroed by the producer (#18 cgreturn
* variant-widen) so the unconditional store here is
* safe even when the source variant has fewer payload
* words than the dst slot. */
/* #38b: an sret-classified call result is in memory
* (AX = dest pointer), not the cursor. #40. */
if (src->kind == N_CALL && cg_sret_retsize(st) > 0)
fatal("#40: sret-class call result cannot be "
"cursor-widened into a tagged slot "
"(mem-to-mem widen unwired)");
/* #37 (rule 7): >32B from a non-mem-based kind
* would spill an unfilled cursor. */
if (ssz > TUPLE_GPCAP * 8)
fatal("#37: >32B tagged source of a non-mem-"
"based kind (%d) unwired (rule 7)",
src->kind);
/* Family C catch-all (rule 7): a tagged cast
* surviving cg_tagged_castpeel (cast to a THIRD
* union) has no cursor — loud, not word0 garbage. */
if (src->kind == N_CAST)
fatal("#35: tagged cast source shape unwired "
"at the widen subset arm (rule 7)");
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 0));
if (ssz > 8)
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, write_off + 8));
if (ssz > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, write_off + 16));
if (ssz > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, write_off + 24));
}
if (ssz < sz) {
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = ssz; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
}
cg_widen_tag_remap(c, du, su, write_off);
if (via_outer) goto copy_out;
return;
}
/* #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
* the struct-literal field-flow below, but 8B-slotted, not field-
* offset.
*
* #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; st 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. */
Node *tupsrc = NULL;
if (su && su->kind == TY_TUPLE) {
if (src->kind == N_TUPLE)
tupsrc = src;
else if (src->kind == N_CAST && src->lhs
&& src->lhs->kind == N_TUPLE)
tupsrc = src->lhs;
/* #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. */
if (tupsrc == NULL)
fatal("cg_widen_tagged_store: tuple-typed source "
"shape unwired (only the bare/cast tuple literal "
"carries a full payload; see #72)");
}
if (tupsrc != NULL) {
int tag = cg_tag_for_variant(du, st);
/* #242: a tuple built from UNTYPED/literal elements (`(true,7)`)
* leaves the src tuple type un-matchable by type_eq, so the
* variant tag can't resolve — the supported shape is a tuple of
* TYPED expressions (the strconv parseint `(neg, n)` shape).
* Loud-stop rather than silently mis-tag (tag 0) — rule 7.
* Untyped tuple-element coercion is the #241 literal-init
* family. */
if (tag < 0)
fatal("cg_widen_tagged_store: tuple-in-union variant tag "
"unresolved (untyped/literal tuple element; "
"see #242 / #241)");
/* #242: this 8B-per-eightbyte packing is correct only when no
* two scalar elements share a SysV eightbyte — e.g. (bool,u64),
* where the sub-8 bool is padded out by u64's 8-alignment. A
* tuple whose natural aligned layout packs two narrows into one
* eightbyte (e.g. (i32,i32,u64)) would overflow the union
* payload the slotted write assumes. Loud-stop (rule 7); the
* SysV eightbyte tuple classification is a deferred follow-up. */
int total = 0;
for (Node *e = tupsrc->list; e; e = e->next) {
/* #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. */
Type *eu = type_chase_named(e->type);
if (eu && eu->kind == TY_TAGGED)
fatal("cg_widen_tagged_store: tagged element "
"in a tuple-in-union payload unwired "
"(see #242/#22b)");
total += (node_isstr(e) || node_isslice(e)) ? 24 : 8;
}
if (8 + total > sz)
fatal("cg_widen_tagged_store: tuple-in-union payload needs "
"SysV eightbyte packing (narrow elements share an "
"eightbyte; see #242 follow-up)");
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
int foff = 0;
for (Node *e = tupsrc->list; e; e = e->next) {
int e_isf32 = 0;
int isflt = fld_isfloat(e->type, &e_isf32);
int wide = node_isstr(e) || node_isslice(e);
int esz = e->type ? (int)e->type->size : 8;
cgexpr(c, e, *locals_p);
if (isflt) {
ins2(c, e_isf32 ? A_MOVSS : A_MOVSD,
areg(D_X0),
amem(D_BP, write_off + 8 + foff));
} else if (wide) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8 + foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, write_off + 8 + foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, write_off + 8 + foff + 16));
} else {
ins2(c, fldstoreop(e->type, esz),
areg(D_AX),
amem(D_BP, write_off + 8 + foff));
}
foff += wide ? 24 : 8;
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
return;
}
/* Struct payload: zero the whole slot, then write fields/words
* at slot+8+ — keeping the tag word at slot+0 from the zero-fill,
* then patch it with the variant tag. */
if (su && su->kind == TY_STRUCT) {
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
int tag = cg_tag_for_variant(du, st);
if (src->kind == N_IDENT) {
int soff = localfind(*locals_p, src->str);
int ssz = (int)su->size;
int k = 0;
while (k + 8 <= ssz) {
ins2(c, A_MOVQ, amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8 + k));
k += 8;
}
if (k < ssz) {
/* Tail word: load with the right width to
* avoid stepping past the source slot. The
* zero-fill above means trailing slop is
* already clean. */
int tail = ssz - k;
int lop = (tail == 4) ? A_MOVL :
(tail == 1 ? A_MOVB : A_MOVQ);
ins2(c, lop,
amem(D_BP, soff + k), areg(D_AX));
ins2(c, lop, areg(D_AX),
amem(D_BP, write_off + 8 + k));
}
} else if (src->kind == 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. */
cg_structlit_fill(c, locals_p, su, src,
DST_BP, 0, NULL, write_off + 8);
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
return;
}
/* str IS []u8 — same 32B payload as a slice: cgexpr leaves
* (AX=ptr, BX=len, CX=cap); slot layout tag@+0, ptr@+8, len@+16,
* cap@+24, destination slot >= 32B. str folds onto the slice arm
* (#1/Phase 3 collapse). */
if (type_isslice(st) || (su && su->kind == TY_SLICE) ||
type_isstr(st) || (su && su->kind == TY_STR)) {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 16));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, write_off + 24));
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
return;
}
/* Float arm: cgexpr on an f64/f32 source leaves the bit pattern in
* X0 only — the AX-store 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.
* Same kind-specific dispatch as the str/slice branches above and
* the structlit field-flow at the top of this function. */
int wid_isf32 = 0;
if (fld_isfloat(st, &wid_isf32)) {
int mov = wid_isf32 ? A_MOVSS : A_MOVSD;
cgexpr(c, src, *locals_p);
ins2(c, mov, areg(D_X0), amem(D_BP, write_off + 8));
/* #227: zero the pad words (+16..sz) so a >16B union slot
* carries the dst's full payload width, not just the 1-word
* float value. The BP/let/assign/return-scratch path never
* pre-zeroes, so a passthrough return or a *u8 reinterpret of
* the narrow-tagged value otherwise reads stack garbage at
* slot+16/+24. Mirrors the tagged-subset tail-zero; symmetric
* with wwstage cgwidentaggedstorebp. */
if (sz > 16) {
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 16; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
}
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
return;
}
/* Scalar / pointer / etc. #227: zero the pad words (+16..sz) — see
* the float arm above. The old code left the pad uninitialised on
* the BP path (relying on cg_widen_tagged_push's pre-zero), but
* let/assign/return-scratch never pre-zeroes, so a passthrough
* return / *u8 reinterpret of the narrow-tagged value read stack
* garbage in slot+16/+24. */
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
if (sz > 16) {
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 16; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
}
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, write_off + 0));
copy_out:
if (via_outer) {
/* cgexpr above clobbered base_reg — reload from spill, then
* word-copy scratch → caller's (base_reg, slot_off). */
ins2(c, A_MOVQ, amem(D_BP, base_spill), areg(base_reg));
for (int k = 0; k < sz; k += 8) {
ins2(c, A_MOVQ, amem(D_BP, write_off + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(base_reg, slot_off + k));
}
}
}
/* cg_widen_tagged_push — call-site widening. For shapes where cgexpr
* leaves the value directly in registers (str: AX=ptr, BX=len; slice:
* AX=ptr, BX=len, CX=cap; scalar: AX), push from registers without a
* scratch slot. Struct payload and tagged-subset re-layout still
* route through a scratch slot. The direct-push form keeps wwstage's
* asm byte-identical to cstage on the byteindex / index family. */
static void
cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
{
Type *du = type_chase_named(dst);
if (du && du->nullable) {
/* Single 8B slot: just push the pointer/null. */
cgexpr(c, src, *locals_p);
ins1(c, A_PUSHQ, areg(D_AX));
return;
}
Type *st = src ? src->type : NULL;
/* Transitive chase (#62 Layer-2) — the call-arg twin of the store
* su above: an unchased 2-level alias took the direct-push scalar
* fast arm (word0-only push). */
Type *su = type_chase_named(st);
int src_is_struct = su && su->kind == TY_STRUCT;
int src_is_tagged = su && su->kind == TY_TAGGED;
/* #66: a tuple-typed source has no direct-push shape — the scalar
* fast arm below 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). */
int src_is_tuple = su && su->kind == TY_TUPLE;
/* #38b: a MEMORY-class (>48B) dst slot always routes through the
* scratch path — the str/slice fast arms push exactly 4 words,
* short of the slot's msz/8 the mem pre-pass accounts for. */
int dst_is_mem = tagged_memarg_size(dst) > 0;
if (!src_is_struct && !src_is_tagged && !src_is_tuple && !dst_is_mem) {
/* Direct-push fast path: str / slice / scalar / pointer. */
cgexpr(c, src, *locals_p);
int tag = cg_tag_for_variant(du, st);
if (tag < 0) tag = 0;
if (type_isstr(st) || (su && su->kind == TY_STR)) {
/* str IS []u8: slot 32 [+0]=tag, [+8]=ptr, [+16]=len,
* [+24]=cap — same shape as the slice arm below. Push
* cap, len, ptr, tag (high→low so pop drains tag first)
* (#1/Phase 3). */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
return;
}
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
/* slot 32: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap. */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
return;
}
/* Scalar / pointer variant. Pad with zero high words when
* the slot has room for a wider variant. */
int nwords = sz / 8;
for (int k = nwords - 1; k >= 2; k--) {
ins2(c, A_XORQ, areg(D_DX), areg(D_DX));
ins1(c, A_PUSHQ, areg(D_DX));
}
ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */
return;
}
int scr = cg_tagscr_slot(c, locals_p, sz);
/* Zero the scratch slot first so any pad word the store path
* leaves untouched (struct payload shorter than the slot's value
* area) reads as 0 on the callee. The store path then writes the
* variant bytes over the zeros. */
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k));
cg_widen_tagged_store(c, locals_p, dst, src, D_BP, scr, sz);
int nwords = sz / 8;
for (int k = nwords - 1; k >= 0; k--) {
ins2(c, A_MOVQ, amem(D_BP, scr + k * 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
}
/* cg_structlit_fill — fill a struct-typed slot from an N_STRUCTLIT
* value into one of three destination flavors. Used by N_LET, N_ASSIGN
* N_IDENT-lhs, N_RETURN N_STRUCTLIT (BP-rel), and N_ASSIGN N_DOT-lhs
* (BP-rel / via *struct local / via struct global) at single-dot and
* chained-dot sites.
*
* Destination modes:
* DST_BP — base = BP, no reload. Stores at disp+i(BP).
* srcoff/name unused.
* 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). name unused.
* DST_GLOBAL — base = BX, reloaded via `LEAQ name(SB), BX` with
* the same reload 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 `foff` as
* we descend into a nested struct-typed structlit field.
* - `srcoff` (DST_PTR_LOCAL) and `name` (DST_GLOBAL) are *constant*
* across the whole call tree — they identify the root dst, which
* doesn't change with depth. Recursion passes them through.
*
* Why a helper? The inline field-walk at each call site previously
* did `cgexpr(f->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 (or stack garbage). #17 fixed
* the BP-rel sites; #18 extends the same recursion to the four
* N_ASSIGN 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.
*
* The scalar store dispatch stays at the explicit {1->MOVB, 4->MOVL,
* else MOVQ} shape (not fieldstoreop, which emits MOVW for fsz==2) to
* stay byte-identical with cstage pending task #13. */
static void
cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit,
int mode, int srcoff, const char *name, int disp)
{
int sz = (int)lu->size;
int base_reg = (mode == DST_BP) ? D_BP : D_BX;
if (lit->op == 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). */
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX));
else if (mode == DST_GLOBAL)
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
int zi = 0;
while (zi + 8 <= sz) {
ins2(c, A_MOVQ, areg(D_AX),
amem(base_reg, disp + zi));
zi += 8;
}
while (zi + 4 <= sz) {
ins2(c, A_MOVL, areg(D_AX),
amem(base_reg, disp + zi));
zi += 4;
}
while (zi < sz) {
ins2(c, A_MOVB, areg(D_AX),
amem(base_reg, disp + zi));
zi += 1;
}
}
for (Node *f = lit->list; f; f = f->next) {
u64 foff = 0;
int fsz = 8;
Type *ft = NULL;
for (Tfield *fl = lu->fields; fl; fl = fl->next) {
if (strcmp(fl->name, f->str) == 0) {
foff = fl->offset;
fsz = (int)(fl->type ? fl->type->size : 8);
ft = fl->type;
break;
}
}
/* Transitive chase (#5-F1 fold): a 2-level-alias slice/str
* field fell past every kind arm to the word0-only scalar
* tail — silent, reachable only via this commit's
* acceptance (reviewer-F1 s1 probe). */
Type *fu = type_chase_named(ft);
if (fu && fu->kind == TY_TAGGED) {
/* Tagged store: reload BX first (if non-BP) so the
* widener sees a valid base reg. The widener itself
* preserves base_reg through its internal cgexpr. */
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff),
areg(D_BX));
else if (mode == DST_GLOBAL)
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
cg_widen_tagged_store(c, locals_p, fu, f->lhs,
base_reg, disp + (int)foff, (int)fu->size);
continue;
}
/* Nested struct-typed structlit value: recurse at the
* field's offset so all inner fields land. Pre-#17/#18 the
* cgexpr-then-store below would land AX = first qword and
* the rest silently stayed zero. */
if (fu && fu->kind == TY_STRUCT
&& f->lhs && f->lhs->kind == N_STRUCTLIT) {
cg_structlit_fill(c, locals_p, fu, f->lhs,
mode, srcoff, name, disp + (int)foff);
continue;
}
/* 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`. Mirrors #4's receive
* shape at the N_LET / N_ASSIGN call-rhs sites; the
* MOVW-for-tail==2 emission 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 (MOVQ/LEAQ) is safe; any other emission
* added here will silently corrupt the return value. */
if (fu && fu->kind == TY_STRUCT
&& f->lhs && f->lhs->kind == N_CALL
&& fsz <= 24
&& (fsz % 8 == 0 || fsz % 8 == 1
|| fsz % 8 == 2 || fsz % 8 == 4)) {
cgexpr(c, f->lhs, *locals_p);
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff),
areg(D_BX));
else if (mode == DST_GLOBAL)
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
int regs[3] = { D_AX, D_DX, D_CX };
int full = fsz / 8;
int tail = fsz % 8;
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(base_reg,
disp + (int)foff + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW : A_MOVB;
ins2(c, op, areg(regs[full]),
amem(base_reg,
disp + (int)foff + full * 8));
}
continue;
}
/* 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 below 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). */
if (fu && (fu->kind == TY_STR || fu->kind == TY_SLICE)) {
cgexpr(c, f->lhs, *locals_p);
if (mode == DST_BP) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, disp + (int)foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, disp + (int)foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, disp + (int)foff + 16));
} else {
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff),
areg(D_DX));
else
ins2(c, A_LEAQ, masym(c, name),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, disp + (int)foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_DX, disp + (int)foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, disp + (int)foff + 16));
}
continue;
}
/* #249: array-typed field initialised from an N_ARRLIT. No prior
* arm matched, so without this the generic scalar tail below
* would cgexpr the N_ARRLIT (→ AX≈0) and store one sized word,
* silently DROPPING every element. Store element-wise at
* disp+foff+i*esz, reusing the N_LET array-init shape (cgen.c:
* 8467) for int/float elements and its `...` repeat. For non-BP
* modes cgexpr clobbers BX, so reload the base before each store
* (the X0/AX value reg survives the reload). str/slice/struct/
* tagged ELEMENT arrays are the N_LET path's documented multi-
* word gap (cgen.c:8462) — loud rule-7 error, not a silent drop. */
if (fu && fu->kind == TY_ARRAY
&& f->lhs && f->lhs->kind == N_ARRLIT) {
Type *esub = fu->sub;
Type *esubu = type_chase_named(esub);
int esz = esub ? (int)esub->size : 1;
int al_isf32 = 0;
int is_float_el = fld_isfloat(esub, &al_isf32);
if (type_isstr(esub) || type_isslice(esub)
|| (esubu && (esubu->kind == TY_STRUCT
|| esubu->kind == TY_TAGGED)))
fatal("cg_structlit_fill: array field '%s' has a "
"str/slice/struct/tagged element — multi-word "
"element store is out of #249 scope (N_LET "
"array-init gap, cgen.c:8462)",
f->str ? f->str : "?");
int eop = A_MOVQ;
if (esz == 1) eop = A_MOVB;
else if (esz == 2) eop = A_MOVW;
else if (esz == 4) eop = A_MOVL;
int fmov = al_isf32 ? A_MOVSS : A_MOVSD;
int idx = 0;
Node *last = NULL;
int repeat = 0;
for (Node *e = f->lhs->list; e; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
cgexpr(c, e, *locals_p);
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff),
areg(D_BX));
else if (mode == DST_GLOBAL)
ins2(c, A_LEAQ, masym(c, name),
areg(D_BX));
int eoff = disp + (int)foff + idx * esz;
if (is_float_el)
ins2(c, fmov, areg(D_X0),
amem(base_reg, eoff));
else
ins2(c, eop, areg(D_AX),
amem(base_reg, eoff));
last = e;
idx++;
}
if (repeat && last) {
while (idx < (int)fu->alen) {
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ,
amem(D_BP, srcoff),
areg(D_BX));
else if (mode == DST_GLOBAL)
ins2(c, A_LEAQ, masym(c, name),
areg(D_BX));
int eoff = disp + (int)foff + idx * esz;
if (is_float_el)
ins2(c, fmov, areg(D_X0),
amem(base_reg, eoff));
else
ins2(c, eop, areg(D_AX),
amem(base_reg, eoff));
idx++;
}
}
continue;
}
/* #49 (f38b/x5f-h): an aggregate field from an ADDRESSABLE
* source expr — `outer{.., r = r}` — fell to the scalar
* tail below and stored word0 only. Funnel: source address
* via aggarg_srcaddr (SI), field address via LEAQ (BX —
* loaded AFTER the source walk, which clobbers BX/AX), then
* cg_aggcopy. Non-addressable aggregate sources (tuple-lit,
* >24B/odd-tail call) die loud — pre-#49 they were the same
* silent word0 (rule 7). FULL alias chase (#22 precedent),
* not the region's single-peel `fu` — the wwstage twin
* full-chases the stamped tinfo; a single peel here would
* miss `type b = a; type a = struct` and silently diverge. */
Type *fagg = type_chase_named(ft);
if (fagg && (fagg->kind == TY_STRUCT || fagg->kind == TY_ARRAY
|| fagg->kind == TY_TUPLE)) {
if (!aggarg_srcaddr(c, f->lhs, D_SI, *locals_p))
fatal("structlit fill: aggregate field '%s' "
"from a non-addressable source unwired "
"(task #49/rule-7)",
f->str ? f->str : "?");
if (mode == DST_BP)
ins2(c, A_LEAQ, amem(D_BP, disp + (int)foff),
areg(D_BX));
else {
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff),
areg(D_BX));
else
ins2(c, A_LEAQ, masym(c, name),
areg(D_BX));
if (disp + (int)foff != 0)
ins2(c, A_ADDQ,
aimm(disp + (int)foff),
areg(D_BX));
}
cg_aggcopy(c, (int)fagg->size);
continue;
}
cgexpr(c, f->lhs, *locals_p);
/* For non-BP modes, cgexpr just clobbered BX; reload it
* before the store. */
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX));
else if (mode == DST_GLOBAL)
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
int sl_isf32 = 0;
if (fld_isfloat(ft, &sl_isf32)) {
int mov = sl_isf32 ? A_MOVSS : A_MOVSD;
ins2(c, mov, areg(D_X0),
amem(base_reg, disp + (int)foff));
continue;
}
int op = A_MOVQ;
if (fsz == 1) op = A_MOVB;
else if (fsz == 4) op = A_MOVL;
ins2(c, op, areg(D_AX),
amem(base_reg, disp + (int)foff));
}
}
/* Thin wrapper preserving the BP-rel call shape used by N_LET,
* N_ASSIGN N_IDENT-lhs, and N_RETURN. Byte-identical to the pre-#18
* helper. */
static void
cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off)
{
cg_structlit_fill(c, locals_p, lu, lit, DST_BP, 0, NULL, bp_off);
}
/* tuple_lit_gpwords — INTEGER cursor words an N_TUPLE literal element
* occupies. MUST mirror the literal push arms (cg_tuple_lit_to_cursor /
* cgreturn N_TUPLE) exactly — the count drives the POP fill, so a
* count/push skew silently shifts every later element (#22 class). A
* float rides the SSE row (0 GP words); str/slice push their 3-word
* header; a tagged element its tuple_eslot/8 box words; a void element
* pushes nothing (the checker's 0-slot — pre-#22 the push/receive
* disagreed with the checker here, latent, no consumer); a scalar 1.
*
* #57: `dt` is the DECLARED tuple element type (NULL when the consumer
* has none). The N_TUPLE literal's stamped type is CONSTRUCTED from
* its elements (check.c N_TUPLE), so a concrete rvalue under a
* declared-TAGGED slot counted ONE word here while the receive walks
* the declared eslot — the cursor shifted and every later element
* read garbage. Declared-tagged keys the count on the DECLARED box. */
static int
tuple_lit_gpwords(Node *e, Type *dt)
{
Type *du = dt ? type_chase_named(dt) : NULL;
if (du && du->kind == TY_TAGGED)
return tuple_eslot(dt) / 8;
int f32;
if (fld_isfloat(e->type, &f32)) return 0;
if (node_isstr(e) || node_isslice(e)) return (int)(ty_str->size / 8);
Type *eu = type_chase_named(e->type);
if (eu && (eu->kind == TY_TAGGED || eu->kind == TY_VOID))
return tuple_eslot(e->type) / 8;
return 1;
}
/* tuple_lit_push_elem — evaluate one N_TUPLE literal element and push
* its INTEGER cursor words L→R (the pop side fills tuple_rseq in
* reverse). A tagged element loads its box words straight from its
* local slot — cgexpr's ident load is word0-only for tagged (every
* tagged consumer reads memory), so the cursor fill must too. Shared
* by cg_tuple_lit_to_cursor and the cgreturn N_TUPLE arm — count
* (tuple_lit_gpwords) and push live or die together.
*
* #57: a DECLARED-tagged element whose expr is a concrete rvalue
* (`return (5: size, 9)` — cast, literal, call) skipped the widen
* entirely: the stamped-keyed arm below saw a scalar and pushed ONE
* word, the receiver read the declared box words — silent shift, both
* stages, gate-blind (ken /tmp/ken57). Such an element now widens
* into the shared tagged scratch (cg_widen_tagged_store, the cgreturn
* tagged-@retscr shape) and pushes the box words. A tagged→tagged
* SUBSET element (eslot mismatch) needs a tag remap on the way into
* the slot — loud (rule 7, the #23/#40 widening family). */
static void
tuple_lit_push_elem(Cg *c, Local **locals_p, Node *e, Type *dt)
{
Type *du = dt ? type_chase_named(dt) : NULL;
Type *eu = type_chase_named(e->type);
if (du && du->kind == TY_TAGGED
&& !(eu && eu->kind == TY_TAGGED)) {
int eslot = tuple_eslot(dt);
int scr = cg_tagscr_slot(c, locals_p, eslot);
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < eslot; k += 8)
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k));
cg_widen_tagged_store(c, locals_p, du, e, D_BP, scr,
eslot);
for (int k = 0; k < eslot / 8; k++) {
ins2(c, A_MOVQ, amem(D_BP, scr + k * 8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
return;
}
if (du && du->kind == TY_TAGGED && eu && eu->kind == TY_TAGGED
&& tuple_eslot(dt) != tuple_eslot(e->type))
fatal("#57: tagged tuple element widening into a wider "
"declared union slot needs a tag remap (rule 7; "
"the #23/#40 widening family)");
if (eu && eu->kind == TY_TAGGED) {
int eslot = tuple_eslot(e->type);
int eoff = (e->kind == N_IDENT && e->str)
? localfind(*locals_p, e->str) : 0;
if (eoff == 0)
fatal("#22a: tagged tuple element from a non-local "
"source shape unwired (ident locals only; "
"rule 7; call-source is task #41, "
"widening #23, deref/cast #35)");
for (int k = 0; k < eslot / 8; k++) {
ins2(c, A_MOVQ, amem(D_BP, eoff + k * 8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
return;
}
cgexpr(c, e, *locals_p);
if (eu && eu->kind == TY_VOID)
return;
ins1(c, A_PUSHQ, areg(D_AX));
if (node_isstr(e) || node_isslice(e)) {
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_CX));
}
}
/* cg_tuple_lit_to_cursor — #241: materialise an N_TUPLE literal's elements
* into the SysV register-return cursor — integer words L→R over tuple_rseq
* (AX,DX,CX,R8), floats over tuple_sse_seq (X0,X1), a slice/str's
* {ptr,len,cap} header over three consecutive INTEGER regs — the SAME ABI a
* tuple-returning CALL leaves, which every tuple consumer (tuple_store at
* the N_LET/N_MLET sites) already reads. cgexpr otherwise can't make a tuple
* value (the default arm zeroed AX), so a literal/yield rvalue tuple bound
* or destructured read garbage past word0. Each element's cgexpr clobbers
* AX/X0, so integer words spill L→R and pop into the cursor reversed, floats
* spill to @tupfscr and reload by SSE index — INDEPENDENT counters (ref/qbe/
* amd64/sysv.c retr). Byte-identical extraction of cgreturn's N_TUPLE arm,
* now shared with cgexpr. Over-cap loud-stops (rule 7); a bare expression
* value can't sret, so the >cap rvalue-tuple materialisation is the #10
* follow-up.
*
* #57: `decl` is the consumer's DECLARED tuple type (NULL when it has
* none — the bare cgexpr route). A declared-TAGGED element gates the
* SSE row off (its payload may be float-stamped but the BOX rides
* INTEGER eightbytes) and keys count + push on the declared eslot —
* see tuple_lit_gpwords / tuple_lit_push_elem. */
static void
cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple, Type *decl)
{
Type *du = decl ? type_chase_named(decl) : NULL;
Tparam *dp0 = (du && du->kind == TY_TUPLE) ? du->params : NULL;
int f32;
int gptotal = 0, ssecount = 0;
Tparam *dp = dp0;
for (Node *e = tuple->list; e; e = e->next) {
Type *dtu = dp ? type_chase_named(dp->type) : NULL;
int dtagged = dtu && dtu->kind == TY_TAGGED;
if (!dtagged && fld_isfloat(e->type, &f32))
ssecount++;
else
gptotal += tuple_lit_gpwords(e,
dp ? dp->type : NULL);
if (dp) dp = dp->next;
}
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
fatal("tuple literal exceeds register-return ABI capacity "
"(integer %d/%d, SSE %d/%d); over-cap rvalue-tuple "
"materialisation is the #10 sret follow-up",
gptotal, TUPLE_GPCAP, ssecount, TUPLE_SSECAP);
int fscr = 0;
if (ssecount > 0) {
if (cg_tupfscr != 0)
fscr = cg_tupfscr;
else {
fscr = local_alloc(c, locals, "@tupfscr",
TUPLE_SSECAP * 8, cg_frame);
cg_tupfscr = fscr;
}
}
int sseidx = 0;
dp = dp0;
for (Node *e = tuple->list; e; e = e->next) {
Type *dtu = dp ? type_chase_named(dp->type) : NULL;
int dtagged = dtu && dtu->kind == TY_TAGGED;
int isflt = !dtagged && fld_isfloat(e->type, &f32);
if (isflt) {
cgexpr(c, e, *locals);
ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0),
amem(D_BP, fscr + sseidx * 8));
sseidx++;
} else {
tuple_lit_push_elem(c, locals, e,
dp ? dp->type : NULL);
}
if (dp) dp = dp->next;
}
for (int i = gptotal - 1; i >= 0; i--)
ins1(c, A_POPQ, areg(tuple_rseq[i]));
int j = 0;
dp = dp0;
for (Node *e = tuple->list; e; e = e->next) {
Type *dtu = dp ? type_chase_named(dp->type) : NULL;
int dtagged = dtu && dtu->kind == TY_TAGGED;
if (!dtagged && fld_isfloat(e->type, &f32)) {
ins2(c, f32 ? A_MOVSS : A_MOVSD,
amem(D_BP, fscr + j * 8),
areg(tuple_sse_seq[j]));
j++;
}
if (dp) dp = dp->next;
}
}
/* cg_tuple_slot_to_cursor — #241: load a tuple already materialised in a
* BP-relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound
* union payload) into the SAME register-return cursor. The slot uses the
* register-ABI stride the tuple-init / #242 destructure write (a scalar 8B,
* a slice/str its 3-word header), NOT the packed t.N field layout (#238).
* All sources are memory, so each word loads straight into its cursor reg —
* no spill dance (unlike the literal arm whose element cgexpr clobbers). So
* `yield t` / `return t` / `let q = t` over a tuple ident leave the whole
* tuple in the cursor, not just word0 in AX. Over-cap loud-stops (rule 7;
* the #10 sret follow-up). */
static void
cg_tuple_slot_to_cursor(Cg *c, int srcoff, Type *tu)
{
int f32;
int gptotal = 0, ssecount = 0;
for (Tparam *p = tu->params; p; p = p->next) {
if (fld_isfloat(p->type, &f32))
ssecount++;
else
gptotal += tuple_eslot(p->type) / 8;
}
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
fatal("tuple ident exceeds register-return ABI capacity "
"(integer %d/%d, SSE %d/%d); over-cap rvalue-tuple "
"materialisation is the #10 sret follow-up",
gptotal, TUPLE_GPCAP, ssecount, TUPLE_SSECAP);
int gp = 0, sse = 0, foff = 0;
for (Tparam *p = tu->params; p; p = p->next) {
int eslot = tuple_eslot(p->type);
int isflt = fld_isfloat(p->type, &f32);
if (isflt) {
ins2(c, f32 ? A_MOVSS : A_MOVSD,
amem(D_BP, srcoff + foff),
areg(tuple_sse_seq[sse]));
sse++;
foff += 8;
} else {
for (int k = 0; k < eslot / 8; k++)
ins2(c, A_MOVQ,
amem(D_BP, srcoff + foff + k * 8),
areg(tuple_rseq[gp + k]));
gp += eslot / 8;
foff += eslot;
}
}
}
/* cg_tagged_tuple_payload_shift — #241: a `?`-unwrapped tuple payload is an
* rvalue tuple that must fill the register cursor the let/destructure
* consumer reads. A tagged return leaves AX=tag, DX=word0, CX=word1,
* R8=word2; the scalar/str unwrap lifts only word0->AX, stranding word1+ in
* CX/R8. Shift the whole payload DOWN one INTEGER reg so element i lands in
* tuple_rseq[i]. A float/slice/str payload element rides a different SysV
* class (X regs / 3-word header) the flat down-shift can't place — loud-stop
* (rule 7); the per-eightbyte tagged-tuple-payload classification is the
* #243 follow-up. */
static void
cg_tagged_tuple_payload_shift(Cg *c, Type *tup)
{
static const int seq[] = { D_AX, D_DX, D_CX, D_R8 };
int f32;
int words = 0;
for (Tparam *p = tup->params; p; p = p->next) {
if (fld_isfloat(p->type, &f32) || tuple_eslot(p->type) != 8)
fatal("tuple-in-union ? unwrap: float/slice/str/tagged "
"payload element needs SysV per-eightbyte "
"classification (see #243); only integer tuple "
"payloads supported");
words += 1;
}
/* tag occupies AX, so only DX/CX/R8 carry payload words. */
if (words > (int)nelem(seq) - 1)
fatal("tuple-in-union ? unwrap payload exceeds the 3 integer "
"return regs past the tag (%d words); see #10/#243", words);
for (int i = 0; i < words; i++)
ins2(c, A_MOVQ, areg(seq[i + 1]), areg(seq[i]));
}
/* cg_arrlit_fill_bp — #31: fill the [count]T destination at BP-relative
* `off` from an N_ARRLIT, extracted verbatim from the N_LET array-init
* path so the slice-borrow base materialisation (the N_SLICE-over-
* N_ARRLIT arm) reuses the identical element-store sequence. `lu` is the
* [count]T array type the checker re-stamped (#25); `arrlit` the literal. */
static void
cg_arrlit_fill_bp(Cg *c, Local **locals, Type *lu, Node *arrlit, int off)
{
Type *esub = lu->sub;
int esz = esub ? (int)esub->size : 1;
/* #270-1c: an AGGREGATE (struct/array/tuple) element
* of an array literal — the scalar per-element MOVQ
* below stores only the first 8 bytes (unpopulated
* tail). Fill each element slot from its literal
* (cg_structlit_fill_bp) or source ident (word-copy). */
Type *esubu = type_chase_named(esub);
int is_agg = esubu && (esubu->kind == TY_STRUCT
|| esubu->kind == TY_ARRAY
|| esubu->kind == TY_TUPLE);
/* #12: a tagged-union element. NOT folded into is_agg —
* is_agg's body does N_STRUCTLIT/N_IDENT word-copy and
* FATALs on the literal/scalar case, never boxing the
* tag+payload. Route each element through the same
* cg_widen_tagged_store choke-point every other tagged
* store uses (let-init, vararg gather, struct-field). */
int is_tagged_el = esubu && esubu->kind == TY_TAGGED;
int is_str_el = type_isstr(esub);
/* #20/#270 str-slice arm: a slice element is a 24B
* {ptr,len,cap} header just like str; cgexpr lowers it
* into AX/BX/CX. Both must store all three words — the
* scalar 1-word MOVQ below drops .len and .cap. */
int is_slice_el = type_isslice(esub);
/* float element → store FROM X0; the AX path stores
* raw double low-bits, garbage for f32 (#122, twin of
* the arr[i]= store fix and the cgen.c:6423 read). */
int is_float_el = type_isfloat(esub);
int fmov = type_isf32(esub) ? A_MOVSS : A_MOVSD;
int op = A_MOVQ;
if (!is_str_el) {
if (esz == 1) op = A_MOVB;
else if (esz == 2) op = A_MOVW;
else if (esz == 4) op = A_MOVL;
/* #128a: esz==2 routes to MOVW (A_MOVW landed in
* both stages' w6a). Pre-fix the 2-byte case fell
* through to MOVQ, over-writing 6B into the next
* element's slot; sequential adjacent writes
* accident-corrected fully-init arrays but
* partial inits clobbered neighbours. */
}
int idx = 0;
Node *last = NULL;
int repeat = 0;
for (Node *e = arrlit->list; e; e = e->next) {
if (e->kind == N_FIELD && e->str &&
strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
int base = off + idx * esz;
if (is_agg) {
if (e->kind == N_STRUCTLIT) {
cg_structlit_fill_bp(c, locals,
esubu, e, base);
} else if (e->kind == N_IDENT) {
int soff = localfind(*locals,
e->str);
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ,
amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVQ,
areg(D_AX),
amem(D_BP, base + k));
}
if (k + 4 <= esz) {
ins2(c, A_MOVL,
amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVL,
areg(D_AX),
amem(D_BP, base + k));
k += 4;
}
if (k + 2 <= esz) {
ins2(c, A_MOVW,
amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVW,
areg(D_AX),
amem(D_BP, base + k));
k += 2;
}
if (k + 1 <= esz) {
ins2(c, A_MOVB,
amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVB,
areg(D_AX),
amem(D_BP, base + k));
k += 1;
}
} else {
fatal("#270-1c: array-literal "
"aggregate element shape "
"unsupported (rule-7)");
}
last = e;
idx++;
continue;
}
if (is_tagged_el) {
cg_widen_tagged_store(c, locals, esub,
e, D_BP, base, esz);
last = e;
idx++;
continue;
}
cgexpr(c, e, *locals);
if (is_str_el || is_slice_el) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, base));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, base + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, base + 16));
} else if (is_float_el) {
ins2(c, fmov, areg(D_X0),
amem(D_BP, base));
} else {
ins2(c, op, areg(D_AX),
amem(D_BP, base));
}
last = e;
idx++;
}
if (repeat && is_agg)
fatal("#270-1c: `...` repeat of an aggregate "
"array-literal element not wired (rule-7)");
/* #12: `...` re-stores from AX, but cg_widen_tagged_store
* consumed the node and trashed AX — a repeat-fill would
* write garbage. No consumer needs `[N]tagged=[x,...]`. */
if (repeat && is_tagged_el)
fatal("#12: `...` repeat of a tagged-union "
"array-literal element not wired (rule-7)");
if (repeat && last) {
/* fill remaining slots with the value still in
* AX (and BX for str). */
while (idx < (int)lu->alen) {
int base = off + idx * esz;
if (is_str_el || is_slice_el) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, base));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, base + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, base + 16));
} else if (is_float_el) {
ins2(c, fmov, areg(D_X0),
amem(D_BP, base));
} else {
ins2(c, op, areg(D_AX),
amem(D_BP, base));
}
idx++;
}
}
}
static void
cgexpr(Cg *c, Node *n, Local *locals)
{
if (n == NULL) {
cgexpr_int(c, 0);
return;
}
switch (n->kind) {
case N_INTLIT:
case N_RUNELIT:
if (node_isfloat(n)) {
cgexpr_float(c, (double)(long long)n->uval);
/* #104: cgexpr_float materialises a DOUBLE in X0; an
* f32-typed literal must narrow with hardware single-
* rounding so the downstream MOVSS reads a true single. */
if (node_isf32(n))
ins2(c, A_CVTSD2SS, areg(D_X0), areg(D_X0));
break;
}
cgexpr_int(c, (long long)n->uval);
break;
case N_FLOATLIT:
cgexpr_float(c, n->fval);
/* #104: narrow the double in X0 to single for an f32 literal. */
if (node_isf32(n))
ins2(c, A_CVTSD2SS, areg(D_X0), areg(D_X0));
break;
case N_STRLIT: {
/* str IS []u8: the (ptr, len, cap) triple — ptr in AX, len in
* BX, cap in CX. A static literal has no spare storage, so
* cap = len (#1/Phase 3, task (b)). */
const char *lab = intern_strlit(c, n->str, n->strlen);
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_BX));
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_CX));
break;
}
case N_TRUE: cgexpr_int(c, 1); break;
case N_FALSE:
case N_NIL:
case N_VOIDLIT: cgexpr_int(c, 0); break;
case N_IDENT: {
int off = localfind(locals, n->str);
if (off != 0) {
Type *itu = type_chase_named(n->type);
if (itu && itu->kind == TY_TUPLE) {
/* #241: a tuple ident is a value — leave the whole
* tuple in the register cursor (`yield t` / `return
* t` / `let q = t`), not just word0 in AX. */
cg_tuple_slot_to_cursor(c, off, itu);
} else if (node_isfloat(n)) {
int op = op_for(n, A_MOVSD, A_MOVSS);
ins2(c, op, amem(D_BP, off), areg(D_X0));
} else if (node_isstr(n)) {
/* str IS []u8: flow as (AX=ptr, BX=len, CX=cap),
* mirroring the slice local load below (#1/Phase 3). */
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
} else if (node_isslice(n)) {
/* slice values flow as (AX=ptr, BX=len, CX=cap)
* — mirror the global-slice load so a slice
* local can be reassigned, returned, or copied
* with the same triple convention. */
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
} else {
ins2(c, localloadop(n->type),
amem(D_BP, off), areg(D_AX));
}
} else {
/* Non-local: function symbols load by address (LEAQ),
* str-typed `def`s expand to (ptr, len) of the literal,
* other globals (def constants) load by value (MOVQ). */
Type *t = n->type;
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
if (u && u->kind == TY_FN) {
/* Take the address of a function. Apply
* @symbol resolution so taking the address
* of a body-less FFI binding yields the C
* symbol, not the ww-side ident. Hare emits
* the same `$symname` for both call and
* address-of via QBE; here we mirror that.
* Bare ident → same-module by ww's resolver,
* so c->cur_mod is the right disambiguation
* hint. */
ins2(c, A_LEAQ,
mafn(c, n->str, c->cur_mod), areg(D_AX));
break;
}
{
/* Same-module-first walk over Sdef. Without
* the prefer pass two modules with same-leaf
* `def MSG: str = "..."` silently fold the
* wrong strlit into the caller's bare-ident
* load (sister callsite of cgdot's str-def
* field fold + wwstage deflookuprhs #4c). */
Sdef *s;
for (s = sdefs; s; s = s->next) {
if (strcmp(s->name, n->str) != 0)
continue;
if (sdef_mod_match(c, s)) break;
}
if (s == NULL) {
for (s = sdefs; s; s = s->next)
if (strcmp(s->name, n->str) == 0)
break;
}
if (s != NULL) {
const char *lab = intern_strlit(c,
s->bytes, s->len);
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
ins2(c, A_MOVQ,
aimm((long long)s->len),
areg(D_BX));
/* str IS []u8: cap = len for a static
* def literal (#1/Phase 3). */
ins2(c, A_MOVQ,
aimm((long long)s->len),
areg(D_CX));
goto ident_done;
}
}
/* C-t3 (#48, rule 7): a GLOBAL tuple as a first-class
* VALUE (`let q = g;` / `return g;` / `f(g)`) has no
* slot-to-cursor path (cg_tuple_slot_to_cursor is
* BP-relative) — pre-fix it fell to the scalar MOVQ
* below, loading word0 only, and the receive read a
* STALE cursor for words 1+. Element reads (g.N)
* are the supported surface. */
if (let_islet(n->str)) {
Type *gu = type_chase_named(n->type);
if (gu && gu->kind == TY_TUPLE)
fatal("#48: global tuple as a "
"first-class value unwired "
"(element reads only; rule 7)");
}
if (let_islet(n->str)
&& (let_isstr(n->type) || let_isslice(n->type))) {
/* Top-level str/slice global: load each word
* via its address (the asm has no `name+8(SB)`
* operand form). str IS []u8 now — both carry a
* third 8B (cap); the address holder CX gets
* overwritten by the cap as the last step, after
* we no longer need it (#1/Phase 3). */
ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod),
areg(D_CX));
ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX));
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX));
ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX));
goto ident_done;
}
if (let_isfloat(n->type)) {
/* Top-level float global (let OR def): same
* LEAQ-indirect shape as str/slice, since
* MOVSS/MOVSD have no D_EXTERN operand form in
* w6a. Pre-#129 this gated on `let_islet` so
* float defs fell through to the MOVQ-AX
* integer-convention fallback below; that
* load-shape mismatched the float storage emit
* (#129 Phase A.1 LOAD-side twin of the
* emit_floatlit_data DATA-side SSoT). */
int op = type_isf32(n->type) ? A_MOVSS : A_MOVSD;
ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod),
areg(D_CX));
ins2(c, op, amem(D_CX, 0), areg(D_X0));
goto ident_done;
}
/* Top-level lets can be the target of `*p` deref-stores
* (via `&letname: *iN`), so a signed-narrow scalar let
* needs MOVSXD/MOVSWQ/MOVSBQ on the read. Defs are
* read-only constants — their address cannot escape,
* so they keep the simpler MOVQ shape (and the wwstage
* defent registry, which doesn't track the declared
* type, agrees byte-for-byte). */
int gop = let_islet(n->str)
? localloadop(n->type) : A_MOVQ;
if (gop == A_MOVQ) {
ins2(c, A_MOVQ, mahint(c, n->str, c->cur_mod),
areg(D_AX));
} else {
/* w6a has no MOVSXD/MOVSWQ/MOVSBQ D_EXTERN
* source form, so route through a LEAQ scratch
* the same way top-level str/slice/float lets
* do. */
ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod),
areg(D_CX));
ins2(c, gop, amem(D_CX, 0), areg(D_AX));
}
}
ident_done:
break;
}
case N_UN:
/* Address-of has its own evaluation strategy — we want the
* address of the operand, not its value. Special-case before
* the cgexpr pre-eval below so `&arr[i]` doesn't compile the
* value load and then discard it. */
if (n->op == TK_AMP) {
Node *opnd = n->lhs;
if (opnd && opnd->kind == N_IDENT) {
int off = localfind(locals, opnd->str);
Type *ot = opnd->type;
Type *ou = (ot && ot->kind == TY_NAMED)
? ot->under : ot;
if (off != 0) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
} else if (ou && ou->kind == TY_FN) {
/* #180: address-of a top-level fn name.
* Twin of the N_IDENT TY_FN read-arm at
* line 2330 (mafn with c->cur_mod hint).
* Previously this fell through silently —
* the AX-store at the assign site picked
* up whatever AX held from prior code, so
* `let f = &add1; (*f)(7)` jumped through
* stale AX. */
ins2(c, A_LEAQ,
mafn(c, opnd->str, c->cur_mod),
areg(D_AX));
} else if (let_islet(opnd->str)
|| def_isstructdef(opnd->str)
|| def_isarraydef(opnd->str)
|| def_isscalardef(opnd->str)) {
/* #149/#147: address-of a top-level def
* with DATA storage. emit_defs / emit_
* struct_data / emit_array_data all emit
* to mod_mangle(name), so the address is
* the same LEAQ name(SB) as a let. The
* address-of twin of A.2/A.3's LOAD-side
* widening. */
ins2(c, A_LEAQ, masym(c, opnd->str),
areg(D_AX));
} else if (def_isanydef(opnd->str)) {
/* #149/#147 rule-7: the name IS a def but
* has no DATA symbol (str def inlined, or
* computed-rhs float like `def NAN =
* 0.0/0.0`). Loud, not a wild deref. */
fatal("cannot take address of non-"
"addressable def '%s': no DATA symbol "
"(str/computed-rhs def; #149/#147)",
opnd->str);
}
break;
}
if (opnd && opnd->kind == N_DOT) {
/* #149 Shape 2: `&mod.G` — module-qualified
* address-of of an exported global (let or def).
* The checker leaves SK_USE module idents untyped
* (NULL/ty_err); detect that and LEAQ the leaf
* symbol. Kind-agnostic (covers cross-module &let
* / &def / &scalar) — the address-of twin of the
* value-read mod-qual path below. A TY_FN leaf
* resolves via mafn (fn address), mirroring the
* read path's TY_FN branch. Placed before the
* spine walk, which aborts on the untyped base
* anyway. */
if (opnd->lhs && opnd->lhs->kind == N_IDENT
&& (opnd->lhs->type == NULL
|| opnd->lhs->type == ty_err)) {
Type *lt = opnd->type;
Type *lu = (lt && lt->kind == TY_NAMED)
? lt->under : lt;
if (lu && lu->kind == TY_FN)
ins2(c, A_LEAQ,
mafn(c, opnd->str,
opnd->lhs->str),
areg(D_AX));
else
/* #229: dotted-module value
* mangle (twin of the read), so
* &aa.v takes aa's global, not a
* same-leaf collision. */
ins2(c, A_LEAQ,
mahint(c, opnd->str,
opnd->lhs->str),
areg(D_AX));
break;
}
/* Address-of through a DOT chain. The early-exit
* above handled `&ident` and `&base[i]`; everything
* else was silently dropped. Three shapes converge
* here, all returning an 8B address (so no
* fldloadop dispatch — just LEAQ).
*
* 1. Value-struct fields, any depth (`&o.f`,
* `&o.i.a`, `&o.a.b.c`): walk the spine to a
* root ident, sum field offsets, emit LEAQ at
* base + sum. Mirror of the read at line 3722.
* 2. Slice/str pseudo-field tail (`&s.len`,
* `&b.buf.len`): folds into the spine walk
* with slice_delta 0/8/16.
* 3. Pointer-field (`&p.f` where p:*T): the spine
* walk aborts at the *T base; the fallback
* below loads p into AX and adds field_off.
*/
int amped = 0;
/* Spine walk — same shape as the read at 3722.
* Records (parent_struct, field_name) leaf-first,
* then iterates root-first to sum offsets. */
struct { Type *pu; const char *name; } steps[16];
int nsteps = 0;
Node *cur = opnd;
int abort = 0;
while (cur && cur->kind == N_DOT && cur->lhs) {
Type *pt = cur->lhs->type;
/* Transitive chase (#71) — addr-of sibling of the
* chained-dot walks. */
Type *pu = type_chase_named(pt);
if (!pu) { abort = 1; break; }
if (cur == opnd && (pu->kind == TY_SLICE
|| pu->kind == TY_STR)) {
/* leaf pseudo on slice/str header */
} else if (pu->kind != TY_STRUCT) {
abort = 1;
break;
}
if (nsteps >= 16) { abort = 1; break; }
steps[nsteps].pu = pu;
steps[nsteps].name = cur->str;
nsteps++;
cur = cur->lhs;
}
if (!abort && cur && cur->kind == N_IDENT
&& nsteps > 0) {
int total_off = 0;
int slice_delta = -1;
int ok = 1;
for (int i = nsteps - 1; i >= 0; i--) {
Type *pu = steps[i].pu;
if (pu->kind == TY_SLICE
|| pu->kind == TY_STR) {
if (strcmp(steps[i].name, "ptr") == 0)
slice_delta = 0;
else if (strcmp(steps[i].name, "len") == 0)
slice_delta = 8;
else if (strcmp(steps[i].name, "cap") == 0)
slice_delta = 16;
else { ok = 0; break; }
} else {
Tfield *f = NULL;
for (Tfield *fl = pu->fields; fl; fl = fl->next)
if (strcmp(fl->name, steps[i].name) == 0)
{ f = fl; break; }
if (!f) { ok = 0; break; }
total_off += (int)f->offset;
}
}
if (ok) {
int extra = (slice_delta >= 0)
? slice_delta : 0;
int root_off = localfind(locals, cur->str);
if (root_off != 0) {
ins2(c, A_LEAQ,
amem(D_BP,
root_off + total_off + extra),
areg(D_AX));
amped = 1;
} else if (let_islet(cur->str)) {
/* Two-step global form mirrors the
* read path's `LEAQ name,CX → MOVQ
* disp(CX),AX`, swapping the MOVQ
* for LEAQ. */
ins2(c, A_LEAQ,
masym(c, cur->str), areg(D_CX));
ins2(c, A_LEAQ,
amem(D_CX, total_off + extra),
areg(D_AX));
amped = 1;
}
}
}
/* Pointer-field fallback for `&p.f` where p:*T —
* the spine walker aborts on the *T base. Load p
* into AX, then LEAQ field_off(AX),AX. Mirror of
* the read at line 4033. */
if (!amped && opnd->lhs
&& opnd->lhs->kind == N_IDENT) {
Type *bt = opnd->lhs->type;
Type *bu = (bt && bt->kind == TY_NAMED)
? bt->under : bt;
if (bu && bu->kind == TY_PTR && bu->sub) {
Type *inner = bu->sub;
if (inner->kind == TY_NAMED)
inner = inner->under;
if (inner && inner->kind == TY_STRUCT) {
for (Tfield *f = inner->fields;
f; f = f->next) {
if (strcmp(f->name, opnd->str) != 0)
continue;
int off = localfind(locals,
opnd->lhs->str);
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_AX));
ins2(c, A_LEAQ,
amem(D_AX, (int)f->offset),
areg(D_AX));
amped = 1;
break;
}
}
}
}
if (amped) break;
/* Fall through to silent-drop fallback below. */
}
if (opnd && opnd->kind == N_INDEX) {
/* &base[i] = base + i*esz, no dereference.
* esz via idx_eff (#61): `&p[i]` on `p: *[N]T`
* strides the pointee array's ELEMENT — the
* undrilled bu->sub here was the whole [N]T
* (&p[i]-&p[0] = i*N*size(T), wild pointer).
* Base load still keys off bu (is_arr stays
* false for the ptr → MOVQ of p's value). */
Node *base = opnd->lhs;
Node *idx = opnd->rhs;
Type *bt = base ? base->type : NULL;
Type *bu = type_chase_named(bt);
Type *eff = idx_eff(bt);
int esz = (eff && eff->sub)
? (int)eff->sub->size : 1;
cgexpr(c, idx, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
}
if (base && base->kind == N_IDENT) {
int boff = localfind(locals,
base->str);
int is_arr = bu &&
bu->kind == TY_ARRAY;
if (boff != 0) {
if (is_arr) {
ins2(c, A_LEAQ,
amem(D_BP, boff),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
}
} else if (let_islet(base->str)) {
if (is_arr) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_BX));
}
} else {
ins2(c, A_XORQ, areg(D_BX),
areg(D_BX));
}
ins2(c, A_ADDQ, areg(D_BX),
areg(D_AX));
break;
}
/* Complex base: eval to AX, swap into BX,
* then add the saved scaled idx. #252: an
* N_DOT `[N]T`-field base needs the field
* ADDRESS (cg_dotbase_addr LEAQ) — cgexpr would
* auto-deref + load the field VALUE as a pointer
* (segfault). Sibling of the #135 read-side wiring. */
ins1(c, A_PUSHQ, areg(D_AX));
if (!cg_dotbase_addr(c, base, D_AX, locals))
cgexpr(c, base, locals);
ins1(c, A_POPQ, areg(D_BX));
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
break;
}
/* C2 (F4 family, reviewer-A route): address-of through
* an indexed/deref dot spine (`&threads[0].cap`,
* `&(*p)[i].f`) — the spine walker above roots only at
* idents. Route the place address through cgplaceaddr
* (read-twin in case N_DOT). Any remaining shape dies
* LOUD: the pre-C2 silent drop left stale AX as the
* "address" — a gate-blind SEGFAULT at the deref. */
if (cgplaceaddr(c, opnd, D_BX, locals)) {
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
break;
}
fatal("unsupported address-of shape");
}
cgexpr(c, n->lhs, locals);
switch (n->op) {
case TK_MINUS:
if (node_isfloat(n->lhs)) {
/* Float negate: X0 = 0 - X0. cgexpr left the
* value in X0; AX-only NEGQ wouldn't touch it. */
int isf32 = node_isf32(n->lhs);
int mov = isf32 ? A_MOVSS : A_MOVSD;
int sub = isf32 ? A_SUBSS : A_SUBSD;
/* save orig X0 → stack */
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
/* load 0.0 into X0 (zero bit pattern == 0.0) */
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, mov, amem(D_SP, 0), areg(D_X0));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
/* X1 = orig; X0 = X0 - X1 = -orig */
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
ins2(c, sub, areg(D_X1), areg(D_X0));
} else {
ins1(c, A_NEGQ, areg(D_AX));
}
break;
case TK_TILDE:
/* NOTQ inverts the whole 64-bit register. For unsigned
* narrow types we clamp to the type width so the
* upper bits are 0, matching how zero-extended loads
* leave the register. Signed narrow types already
* end up sign-extended (NOTQ on a sign-extended
* positive becomes sign-extended negative), so they
* need no fix-up. u32 uses MOVL r,r (zero-extends
* upper 32) because ANDQ $0xFFFFFFFF would sign-extend
* the imm32 to all-ones and act as a no-op. */
ins1(c, A_NOTQ, areg(D_AX));
if (n->type && type_isunsigned(n->type)
&& n->type->size < 8) {
if (n->type->size == 4) {
ins2(c, A_MOVL, areg(D_AX), areg(D_AX));
} else {
u64 mask = ((u64)1 << (n->type->size * 8)) - 1;
ins2(c, A_ANDQ, aimm((i64)mask), areg(D_AX));
}
}
break;
case TK_NOT: {
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
char *t = mklabel(c, "tt");
char *e = mklabel(c, "te");
ins1(c, A_JE, abranch(t));
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
ins1(c, A_JMP, abranch(e));
label(c, t);
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
label(c, e);
break;
}
case TK_AMP:
/* Handled in the pre-cgexpr early-exit above. */
break;
case TK_STAR: /* deref */
{
/* #185: deref of *fn — the pointer value IS
* the fn address. cgexpr(opnd) already left
* AX = fn-addr; a generic MOVQ (AX),AX would
* load the first instruction word and CALL
* would segfault on that junk. Mirror
* ref/harec/src/check.c expr_call's
* STORAGE_POINTER→STORAGE_FUNCTION skip.
* #61 C: same skip for an ARRAY pointee — an
* array value IS its address everywhere in
* this cgen (#270-1a), so `*p` on `*[N]T`
* leaves AX = p's value. The scalar load
* below pulled a[0]'s VALUE and `(*p)[i]`
* then dereferenced it as the index base —
* a wild pointer, SIGSEGV on both stages.
* #93: the single peel left a 2-LEVEL alias
* pointee NAMED — the ARRAY skip missed and
* the scalar load fired (one spurious
* MOVQ (AX),AX, SEGV); ww chases and is the
* runtime-correct reference (#263 polarity). */
Type *rt = n->type;
Type *ru = type_chase_named(rt);
if (ru && (ru->kind == TY_FN
|| ru->kind == TY_ARRAY))
break;
/* Family C (#35/#46): a tagged box behind
* *p joins the mem-based class at ANY size
* (cg_tagged_memread) — AX = p's value IS
* the box address. The scalar load below
* pulled word0 (the tag) and every cursor
* consumer then transported garbage payload
* words — silent-wrong on both stages (the
* ken f35/D3a/D3b family). The nullable
* one-word fold stays a scalar deref. */
if (ru && ru->kind == TY_TAGGED
&& !ru->nullable && ru->size > 8)
break;
}
/* f64/f32 result rides X0 (SSE), not AX — an integer
* MOVQ strands the value off the float ABI and the
* caller's MOVSD X0 reads stale bits (#96). Mirrors the
* float field/ident load idiom at 1462/1838. */
if (node_isfloat(n)) {
ins2(c, node_isf32(n) ? A_MOVSS : A_MOVSD,
amem(D_AX, 0), areg(D_X0));
} else {
/* Load-twin of the signed-narrow-scalar-reads
* sweep (project_cgen_int_cast_no_truncate);
* TK_STAR was the omitted site, refiled as
* #116. A raw MOVQ pulls 8 bytes through a
* narrow `*iN` and overlaps the next element
* — the `*p` value reads honest only when the
* caller's sink happens to truncate (i32 store,
* i32 return). Width-preserving sinks (CMPQ,
* 64-bit arith) saw garbage in the high bytes.
* localloadop keys MOVSXD/MOVSWQ/MOVSBQ +
* MOVL/MOVZWQ/MOVZBQ off n->type, with the
* TY_NAMED / TY_ENUM peel pre-folded so an
* aliased narrow (`type err = !i32`) lands on
* the right opcode. */
ins2(c, localloadop(n->type),
amem(D_AX, 0), areg(D_AX));
}
break;
default: break;
}
break;
case N_BIN: {
/* Short-circuit `&&` / `||`. Operands are bool (0/1); the
* type checker enforces it. Eval LHS into AX, branch over
* RHS on the short-circuit polarity, otherwise eval RHS
* into AX. The surviving AX is the result. Must precede
* any eager-eval path below — `if (p != nil && p.x > 0)`
* would segfault on a nil deref otherwise. */
if (n->op == TK_AND || n->op == TK_OR) {
char *end = mklabel(c, n->op == TK_AND ? "andend" : "orend");
int jshrt = (n->op == TK_AND) ? A_JE : A_JNE;
cgexpr(c, n->lhs, locals);
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, jshrt, abranch(end));
cgexpr(c, n->rhs, locals);
label(c, end);
break;
}
/* str == str / str != str — delegate to rt_streq, which
* does the byte-by-byte compare. */
if ((n->op == TK_EQ || n->op == TK_NEQ) &&
node_isstr(n->lhs) && node_isstr(n->rhs)) {
/* Push rhs (len, then ptr top) */
if (n->rhs->kind == N_IDENT) {
int off = localfind(locals, n->rhs->str);
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
} else {
cgexpr(c, n->rhs, locals); /* AX=ptr, BX=len */
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_AX));
}
/* Push lhs */
if (n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
} else {
cgexpr(c, n->lhs, locals);
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_AX));
}
ins1(c, A_POPQ, areg(D_DI));
ins1(c, A_POPQ, areg(D_SI));
ins1(c, A_POPQ, areg(D_DX));
ins1(c, A_POPQ, areg(D_CX));
ins1(c, A_CALL, asym("rt_streq"));
if (n->op == TK_NEQ)
ins2(c, A_XORQ, aimm(1), areg(D_AX));
break;
}
/* Float comparison: operands are float but the BIN node's
* type is bool, so node_isfloat(n) is false — we have to
* inspect n->lhs. UCOMISD/UCOMISS sets ZF/CF as if an
* unsigned compare, so the JA family is the right Jcc set
* regardless of how the operand types are signed. Plan 9's
* own w6c picks the same pattern (txt.c around AUCOMISD).
* NaN handling: UCOMI sets PF=ZF=CF=1 on unordered (a NaN
* operand). IEEE-754: any relop with a NaN operand is
* unordered — `!=` true, the other five false. PF must steer
* `!=`/`==`/`<`/`<=` (#97): JNE keys on ZF=0 so `nan != nan`
* came out false; JE/JB/JBE all fire on the unordered ZF/CF.
* `>`/`>=` (JA/JAE) need CF=0, which unordered never gives,
* so they are ALREADY NaN-correct and stay byte-identical to
* the pre-#97 single-template arm — no redundant PF guard. */
if (n->lhs && node_isfloat(n->lhs) &&
(n->op == TK_EQ || n->op == TK_NEQ
|| n->op == TK_LT || n->op == TK_LE
|| n->op == TK_GT || n->op == TK_GE)) {
int isf32 = node_isf32(n->lhs);
int mov = isf32 ? A_MOVSS : A_MOVSD;
int ucomi = isf32 ? A_UCOMISS : A_UCOMISD;
cgexpr(c, n->rhs, locals); /* rhs → X0 */
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
cgexpr(c, n->lhs, locals); /* lhs → X0 */
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
ins2(c, ucomi, areg(D_X1), areg(D_X0));
if (n->op == TK_NEQ) {
/* not-equal OR unordered -> true */
char *t = mklabel(c, "ct");
char *e = mklabel(c, "ce");
ins1(c, A_JNE, abranch(t));
ins1(c, A_JP, abranch(t));
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
ins1(c, A_JMP, abranch(e));
label(c, t);
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
label(c, e);
break;
}
if (n->op == TK_EQ || n->op == TK_LT || n->op == TK_LE) {
/* unordered -> false; otherwise the ordered Jcc decides */
int op = (n->op == TK_EQ) ? A_JE
: (n->op == TK_LT) ? A_JB : A_JBE;
char *fl = mklabel(c, "cf");
char *t = mklabel(c, "ct");
char *e = mklabel(c, "ce");
ins1(c, A_JP, abranch(fl));
ins1(c, op, abranch(t));
label(c, fl);
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
ins1(c, A_JMP, abranch(e));
label(c, t);
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
label(c, e);
break;
}
/* `>`/`>=`: JA/JAE already reject unordered (CF=1), so
* keep the pre-#97 single-template shape verbatim. */
int op = (n->op == TK_GT) ? A_JA : A_JAE;
char *t = mklabel(c, "ct");
char *e = mklabel(c, "ce");
ins1(c, op, abranch(t));
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
ins1(c, A_JMP, abranch(e));
label(c, t);
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
label(c, e);
break;
}
if (node_isfloat(n)) {
int isf32 = node_isf32(n);
int mov = isf32 ? A_MOVSS : A_MOVSD;
cgexpr(c, n->rhs, locals); /* X0 */
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
cgexpr(c, n->lhs, locals); /* X0 */
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
switch (n->op) {
case TK_PLUS:
ins2(c, isf32 ? A_ADDSS : A_ADDSD, areg(D_X1), areg(D_X0));
break;
case TK_MINUS:
ins2(c, isf32 ? A_SUBSS : A_SUBSD, areg(D_X1), areg(D_X0));
break;
case TK_STAR:
ins2(c, isf32 ? A_MULSS : A_MULSD, areg(D_X1), areg(D_X0));
break;
case TK_SLASH:
ins2(c, isf32 ? A_DIVSS : A_DIVSD, areg(D_X1), areg(D_X0));
break;
default: break;
}
break;
}
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, n->lhs, locals);
ins1(c, A_POPQ, areg(D_BX));
switch (n->op) {
case TK_PLUS: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
case TK_MINUS: ins2(c, A_SUBQ, areg(D_BX), areg(D_AX)); break;
case TK_STAR: ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); break;
case TK_SLASH: {
/* Use DIV (unsigned) when either operand is an unsigned
* integer type — IDIV would sign-extend a u64 with high
* bit set into a negative i64 and produce wrong results
* (see strconv.u64tos with v = 1 << 63). Signed IDIV
* needs CQO to sign-extend RAX into RDX:RAX; zeroing
* DX would treat a negative dividend as a huge unsigned
* 128-bit value. */
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
if (unsignd)
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX));
break;
}
case TK_PERCENT: {
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
if (unsignd)
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX));
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
}
case TK_AMP: ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); break;
case TK_PIPE: ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break;
case TK_CARET: ins2(c, A_XORQ, areg(D_BX), areg(D_AX)); break;
case TK_LSHIFT: case TK_RSHIFT: {
/* shift amount must be in CL. #136: signed RSHIFT uses
* SAR (arithmetic, sign-extends MSB); unsigned uses SHR
* (logical, zero-fill). LSHIFT is signedness-agnostic
* (SHL == SAL at the encoder). */
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
int rop = unsignd ? A_SHRQ : A_SARQ;
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
ins2(c, n->op == TK_LSHIFT ? A_SHLQ : rop,
areg(D_CX), areg(D_AX));
break;
}
case TK_EQ: case TK_NEQ: case TK_LT: case TK_LE:
case TK_GT: case TK_GE: {
/* For ordered comparisons on unsigned operands we must
* use the JA/JAE/JB/JBE family — signed Jcc would treat
* a u64 with the high bit set as negative (e.g. the
* loop guard `n > 0` in strconv.u64tos with n=1<<63). */
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
int op = A_JE;
switch (n->op) {
case TK_EQ: op = A_JE; break;
case TK_NEQ:op = A_JNE; break;
case TK_LT: op = unsignd ? A_JB : A_JL; break;
case TK_LE: op = unsignd ? A_JBE : A_JLE; break;
case TK_GT: op = unsignd ? A_JA : A_JG; break;
case TK_GE: op = unsignd ? A_JAE : A_JGE; break;
default: break;
}
char *t = mklabel(c, "ct");
char *e = mklabel(c, "ce");
ins1(c, op, abranch(t));
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
ins1(c, A_JMP, abranch(e));
label(c, t);
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
label(c, e);
break;
}
/* TK_AND / TK_OR handled with short-circuit codegen at the
* top of N_BIN — they never reach this eager-eval switch. */
default: break;
}
break;
}
case N_ASSIGN: {
/* Discard lvalue `_ = expr;` — evaluate rhs for side effects,
* write nothing. */
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->str && n->lhs->str[0] == '\0' &&
n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals);
break;
}
/* #20 (task): struct-lit rhs into an INDEXED struct element —
* `a[i] = pt{...}`, `(*ts)[i].caps[k] = capture{...}` — a
* DEREF place (`*p = pt{...}`) or an indexed-base FIELD
* place (`a[i].f = pt{...}`, same class) skips the legacy
* arms and routes to the resolver aggregate arm below (the
* single @placescr funnel). The legacy arms' rhs handling
* (#270-1b ident/dot/deref gate; deref scalar store; the
* a[i].f fldstoreop tail) let the lit fall to a scalar
* tail: cgexpr(N_STRUCTLIT) emits nothing (AX=0) and one
* MOVQ zeroed the place's first word — every field
* silently dropped, a leading str header trashed. */
int place_slit = 0;
if (n->lhs
&& (n->lhs->kind == N_INDEX
|| (n->lhs->kind == N_UN && n->lhs->op == TK_STAR)
|| (n->lhs->kind == N_DOT && n->lhs->lhs
&& n->lhs->lhs->kind == N_INDEX))
&& n->op == TK_ASSIGN
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
Type *iet = type_chase_named(n->lhs->type);
if (iet && iet->kind == TY_STRUCT)
place_slit = 1;
}
/* Task #32: an array-LITERAL rhs at assignment is unwired
* for EVERY place kind (ident reassign, index, deref, dot)
* — only decl-init fills. Pre-#32 the same scalar tail
* zeroed one word silently; die loud until the fill lands.
* Slice-typed places are already loud in the checker. */
if (n->op == TK_ASSIGN && n->lhs
&& n->rhs && n->rhs->kind == N_ARRLIT) {
Type *alt = type_chase_named(n->lhs->type);
if (alt && alt->kind == TY_ARRAY)
fatal("array-literal store at assignment "
"unwired (task #32)");
}
/* p.x = v or p.x += v where p.x is a struct field
* (direct or via *struct). For compound ops we read-modify-
* write the field; for plain `=` we just write. The base
* accepts two parser shapes: a bare IDENT (auto-deref when
* the IDENT's type is *T, value-struct otherwise) and the
* explicit-deref form `(*p).f = ...` where the parser emits
* N_UN(STAR, IDENT(p)). For (*p).f, retarget base to the
* inner IDENT so the via_ptr branch fires identically to
* `p.f = v`. v1 scope: bare-IDENT inner only; (*expr).f
* (non-IDENT inner) falls through to the existing drop
* behaviour pending follow-up task. */
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs &&
(n->lhs->lhs->kind == N_IDENT ||
(n->lhs->lhs->kind == N_UN && n->lhs->lhs->op == TK_STAR
&& n->lhs->lhs->lhs
&& n->lhs->lhs->lhs->kind == N_IDENT))) {
Node *base = n->lhs->lhs;
if (base->kind == N_UN) base = base->lhs;
Type *bt = base->type;
/* type_chase_named (#22): a chain `type b = a; a = struct`
* left u at TY_NAMED a after a single peel, missing the
* TY_STRUCT field-walk gate below — the assignment
* silently dropped (the `break` at the bottom of the
* N_DOT-lhs arm). */
Type *u = type_chase_named(bt);
int via_ptr = 0;
if (u && u->kind == TY_PTR) {
via_ptr = 1;
u = type_chase_named(u->sub);
}
/* slice/str pseudo-field write (.ptr/.len/.cap) */
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)) {
const char *fld = n->lhs->str;
int delta = -1;
if (strcmp(fld, "ptr") == 0) delta = 0;
else if (strcmp(fld, "len") == 0) delta = 8;
else if (strcmp(fld, "cap") == 0) delta = 16;
if (delta < 0) goto after_dot_assign;
int boff = localfind(locals, base->str);
if (n->op != TK_ASSIGN) {
if (via_ptr) {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_BX));
} else {
ins2(c, A_MOVQ, amem(D_BP, boff + delta), areg(D_BX));
}
ins1(c, A_PUSHQ, areg(D_BX));
}
cgexpr(c, n->rhs, locals);
if (n->op != TK_ASSIGN) {
ins1(c, A_POPQ, areg(D_BX));
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
case TK_MINUSEQ:
/* old in BX, rhs in AX; want AX = old-rhs.
* SUBQ src,dst is dst -= src in Plan 9. */
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
break;
default: break;
}
}
if (via_ptr) {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, delta));
} else {
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + delta));
}
break;
}
after_dot_assign:
if (u && u->kind == TY_STRUCT) {
/* find field metadata */
Tfield *f = NULL;
for (Tfield *fl = u->fields; fl; fl = fl->next)
if (strcmp(fl->name, n->lhs->str) == 0)
{ f = fl; break; }
if (f == NULL) break;
/* Tagged-union field: synthesise tag and store
* value bytes. Compound ops on tagged fields are
* not meaningful, so only plain `=` is wired.
* Three base shapes:
* - via_ptr: base is *struct local; address
* pre-loaded into BX. Buggy with a str
* variant since cgexpr will overwrite BX,
* but matches the existing pre-global
* behaviour.
* - is_global: struct global. LEAQ after
* cgexpr drops the slot address into CX
* without touching AX/BX, so str variants
* work cleanly.
* - else: struct local, BP-relative. */
Type *ft = f->type;
/* Transitive chase (#5-F1 fold): 2-level alias
* slice/str field skipped the 3-word arm — ptr
* word stored, len/cap dropped (reviewer-F1
* ix2/s2/s3 probes; c1-acceptance-reached). */
Type *fu = type_chase_named(ft);
/* Tagged-union field — full slot rewrite via the
* shared widener so every rhs shape (whole-tagged
* ident or expr with tag-remap, concrete-variant
* widening of str/slice/struct/scalar/void) lands
* the right tag + payload bytes. The pre-#26
* branch synthesised a single tag from
* cg_tag_for_variant and stored only AX at +8, so
* whole-tagged rhs (vt == fu, no concrete tag)
* silently wrote tag 0 and dropped trailing words.
* cg_widen_tagged_store handles every shape by
* branching on the source's resolved type. */
if (fu && fu->kind == TY_TAGGED
&& n->op == TK_ASSIGN) {
int boff = localfind(locals, base->str);
int is_global = (boff == 0 && !via_ptr
&& let_islet(base->str));
int foff = (int)f->offset;
int fsz = (int)fu->size;
if (via_ptr) {
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
cg_widen_tagged_store(c, &locals,
fu, n->rhs, D_BX, foff, fsz);
} else if (is_global) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
cg_widen_tagged_store(c, &locals,
fu, n->rhs, D_BX, foff, fsz);
} else {
cg_widen_tagged_store(c, &locals,
fu, n->rhs, D_BP,
boff + foff, fsz);
}
break;
}
int fsz = (int)(f->type ? f->type->size : 8);
int load_op = fldloadop(f->type, fsz);
int store_op = fldstoreop(f->type, fsz);
int boff = localfind(locals, base->str);
int is_global = (boff == 0 && !via_ptr
&& let_islet(base->str));
int foff = (int)f->offset;
Type *str_fu = type_chase_named(f->type);
/* str/slice field: str IS []u8, so both store the full
* 3-word {ptr,len,cap} that rhs cgexpr leaves in
* (AX,BX,CX) at field+0/+8/+16. Address scratch must
* dodge CX (holds cap), so via_ptr/is_global stage the
* struct base in DX (#1/Phase 3). Without this the
* generic store_op below writes only AX, silently
* dropping .len/.cap. Only plain `=` is wired; compound
* on a str/slice field is not meaningful. */
if (n->op == TK_ASSIGN && str_fu
&& (str_fu->kind == TY_SLICE || str_fu->kind == TY_STR)) {
cgexpr(c, n->rhs, locals);
if (via_ptr) {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
} else if (is_global) {
ins2(c, A_LEAQ, masym(c, base->str), areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
} else {
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, boff + foff + 16));
}
break;
}
/* #234: over-cap sret STORE into a struct field —
* `s.f = wide();` where f's type returns via sret
* (cg_sret_retsize > 0: a >24B struct OR an over-cap
* tuple — Fold A made the callee sret it). The STORE-
* twin of the Fold-B sret RECEIVE (a937d67): point the
* callee's hidden RDI dest straight at the field slot
* (cg_sret_dest_off) so it writes the WHOLE value there.
* Without this the generic scalar store below emits a
* truncated `MOVQ AX, off(BP)` and silently drops the
* sret body. cg_sret_dest_off is BP-relative ONLY, so
* this covers a LOCAL struct base; a via_ptr (`p.f`) or
* global base needs the runtime RDI-pointer dest variant
* deferred to #234-tail and HARD-STOPS loud (rule 7 —
* never fall through to the truncating store). */
if (n->op == TK_ASSIGN && n->rhs
&& n->rhs->kind == N_CALL
&& cg_sret_retsize(f->type) > 0) {
if (via_ptr || is_global || boff == 0)
fatal("#234-tail: over-cap tuple "
"sret store to non-local dest "
"unsupported");
cg_sret_dest_off = boff + foff;
cgexpr(c, n->rhs, locals);
cg_sret_dest_off = 0;
break;
}
/* struct-typed field, three rhs shapes:
* - N_IDENT: word-copy from the rhs slot directly
* onto the destination field. cgexpr cannot
* materialise a whole struct value in registers
* for an arbitrary local, so we read field words
* straight from the source slot.
* - N_CALL (added with #5): cgexpr leaves the value
* in AX/DX/CX per #4's cgreturn ABI; sized stores
* write only the declared field size — MOVQ for
* full 8B chunks plus MOVL/MOVW/MOVB tail. See
* the N_LET receive site for the ASYMMETRY
* rationale. cgreturn touches only AX/DX/CX, so
* BX stays free for the dst-addr load after the
* call.
* - N_STRUCTLIT (added with #5): field-by-field
* store; for via_ptr/is_global the dst base addr
* is reloaded into BX before each store so cgexpr
* can clobber AX/BX between fields. */
if (n->op == TK_ASSIGN && str_fu
&& str_fu->kind == TY_STRUCT
&& (int)str_fu->size <= 24
&& n->rhs && n->rhs->kind == N_CALL
&& (str_fu->size % 8 == 0
|| str_fu->size % 8 == 1
|| str_fu->size % 8 == 2
|| str_fu->size % 8 == 4)) {
int ssz = (int)str_fu->size;
cgexpr(c, n->rhs, locals);
int regs[3] = { D_AX, D_DX, D_CX };
int full = ssz / 8;
int tail = ssz % 8;
int base_reg, base_disp;
if (via_ptr || is_global) {
if (via_ptr)
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
base_reg = D_BX;
base_disp = foff;
} else {
base_reg = D_BP;
base_disp = boff + foff;
}
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(base_reg,
base_disp + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW
: A_MOVB;
ins2(c, op, areg(regs[full]),
amem(base_reg,
base_disp + full * 8));
}
break;
}
if (n->op == TK_ASSIGN && str_fu
&& str_fu->kind == TY_STRUCT
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
/* Delegate to the shared structlit fill
* helper. For via_ptr/is_global, helper
* reloads BX before zero-fill loop + each
* field store. For local BP-rel, helper
* stores direct off BP. AND nested struct-
* typed structlit values recurse instead
* of silently dropping trailing bytes
* (#18 fix). */
int mode = via_ptr ? DST_PTR_LOCAL
: is_global ? DST_GLOBAL : DST_BP;
int disp = (mode == DST_BP)
? (boff + foff) : foff;
cg_structlit_fill(c, &locals, str_fu,
n->rhs, mode, boff,
is_global ? base->str : NULL, disp);
break;
}
if (n->op == TK_ASSIGN && str_fu
&& str_fu->kind == TY_STRUCT
&& n->rhs && n->rhs->kind == N_IDENT
&& localfind(locals, n->rhs->str) != 0) {
int soff = localfind(locals, n->rhs->str);
int ssz = (int)str_fu->size;
if (via_ptr)
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
else if (is_global)
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
int k = 0;
while (k + 8 <= ssz) {
ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX));
if (via_ptr || is_global)
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + k));
else
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + k));
k += 8;
}
if (k < ssz) {
int tail = ssz - k;
int lop = (tail == 4) ? A_MOVL
: (tail == 1 ? A_MOVB : A_MOVQ);
ins2(c, lop, amem(D_BP, soff + k), areg(D_AX));
if (via_ptr || is_global)
ins2(c, lop, areg(D_AX), amem(D_BX, foff + k));
else
ins2(c, lop, areg(D_AX), amem(D_BP, boff + foff + k));
}
break;
}
/* compound: load current value into BX */
if (n->op != TK_ASSIGN) {
if (via_ptr) {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
ins2(c, load_op, amem(D_BX, foff), areg(D_BX));
} else if (is_global) {
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
ins2(c, load_op, amem(D_BX, foff), areg(D_BX));
} else {
ins2(c, load_op, amem(D_BP, boff + foff), areg(D_BX));
}
ins1(c, A_PUSHQ, areg(D_BX));
}
cgexpr(c, n->rhs, locals); /* AX = rhs */
if (n->op != TK_ASSIGN) {
ins1(c, A_POPQ, areg(D_BX));
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
case TK_MINUSEQ:
/* old in BX, rhs in AX; want AX=old-rhs */
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
break;
default: break; /* others rare */
}
}
/* f64/f32 field, plain `=`: cgexpr left the value in
* X0, not AX. Route the store via MOVSD/MOVSS.
* Compound ops on float fields aren't wired here —
* see CLAUDE.md #8 in examples/lisp; same in the
* structlit-init path below. */
int b_isf32 = 0;
if (n->op == TK_ASSIGN
&& fld_isfloat(f->type, &b_isf32)) {
int mov = b_isf32 ? A_MOVSS : A_MOVSD;
if (via_ptr) {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
ins2(c, mov, areg(D_X0), amem(D_BX, foff));
} else if (is_global) {
ins2(c, A_LEAQ,
masym(c, base->str), areg(D_BX));
ins2(c, mov, areg(D_X0), amem(D_BX, foff));
} else {
ins2(c, mov, areg(D_X0),
amem(D_BP, boff + foff));
}
break;
}
/* now store AX into target */
if (via_ptr) {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
ins2(c, store_op, areg(D_AX), amem(D_BX, foff));
} else if (is_global) {
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
ins2(c, store_op, areg(D_AX), amem(D_BX, foff));
} else {
ins2(c, store_op, areg(D_AX), amem(D_BP, boff + foff));
}
break;
}
}
/* `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric
* write-side of the cgdot N_INDEX-lhs branch. Compute &arr[i]
* inline (LEAQ for `[N]Struct`, MOVQ-load for `[N]*Struct` /
* `[]Struct` / `*Struct`), deref once when the element is
* `*Struct`, then store rhs at `field.offset(addr)`. The
* chained-pointer-field branch below catches `[N]*Struct`
* writes via its `!= N_IDENT` guard, but `[N]Struct` value-arrays
* fall through and silently drop the store. Placed before the
* `!= N_IDENT` branch so both shapes share one path. */
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
&& n->lhs->lhs->kind == N_INDEX && !place_slit) {
Node *idxbase = n->lhs->lhs->lhs;
Node *idx = n->lhs->lhs->rhs;
if (idxbase && idxbase->kind == N_IDENT && idx) {
Type *elemt = n->lhs->lhs->type;
Type *elemu = (elemt && elemt->kind == TY_NAMED)
? elemt->under : elemt;
Type *struct_t = NULL;
int viaptr = 0;
if (elemu && elemu->kind == TY_PTR) {
Type *inner = elemu->sub;
if (inner && inner->kind == TY_NAMED)
inner = inner->under;
if (inner && inner->kind == TY_STRUCT) {
struct_t = inner;
viaptr = 1;
}
} else if (elemu && elemu->kind == TY_STRUCT) {
struct_t = elemu;
}
if (struct_t) {
Tfield *f = NULL;
for (Tfield *fl = struct_t->fields; fl;
fl = fl->next)
if (strcmp(fl->name,
n->lhs->str) == 0)
{ f = fl; break; }
Type *bt = idxbase->type;
Type *bu = (bt && bt->kind == TY_NAMED)
? bt->under : bt;
int is_arr = bu && bu->kind == TY_ARRAY;
int is_sl = bu && bu->kind == TY_SLICE;
int is_ptr = bu && bu->kind == TY_PTR;
int off = localfind(locals, idxbase->str);
if (f != NULL && (is_arr || is_sl || is_ptr)
&& off != 0) {
Type *ft = f->type;
Type *fu = type_chase_named(ft);
int fsz = (int)(ft ? ft->size : 8);
int store_op = fldstoreop(ft, fsz);
int foff = (int)f->offset;
int esz = (int)elemt->size;
int h_isf32 = 0;
if (n->op == TK_ASSIGN
&& fld_isfloat(ft, &h_isf32)) {
int mov = h_isf32
? A_MOVSS : A_MOVSD;
cgexpr(c, n->rhs, locals);
ins2(c, A_SUBQ, aimm(8),
areg(D_SP));
ins2(c, mov, areg(D_X0),
amem(D_SP, 0));
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ,
aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ,
areg(D_CX),
areg(D_AX));
}
if (is_arr)
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_BX));
else
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_BX));
ins2(c, A_ADDQ,
areg(D_AX),
areg(D_BX));
if (viaptr)
ins2(c, A_MOVQ,
amem(D_BX, 0),
areg(D_BX));
ins2(c, mov,
amem(D_SP, 0),
areg(D_X0));
ins2(c, A_ADDQ, aimm(8),
areg(D_SP));
ins2(c, mov, areg(D_X0),
amem(D_BX, foff));
break;
}
if (n->op == TK_ASSIGN
&& fu && (fu->kind == TY_STR
|| fu->kind == TY_SLICE)) {
/* str/slice: rhs leaves
* AX=ptr, BX=len, CX=cap
* (#1/Phase 3). Spill all
* three across the index/
* address computation
* (IMULQ's CX scratch
* clobbers cap), stage
* &arr[i] in DX off the str
* AX/BX/CX convention
* (mirrors s.f=v), then store
* the full triple at
* foff+0/+8/+16. */
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ,
areg(D_CX));
ins1(c, A_PUSHQ,
areg(D_BX));
ins1(c, A_PUSHQ,
areg(D_AX));
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ,
aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ,
areg(D_CX),
areg(D_AX));
}
if (is_arr)
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_DX));
else
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_DX));
ins2(c, A_ADDQ,
areg(D_AX),
areg(D_DX));
if (viaptr)
ins2(c, A_MOVQ,
amem(D_DX, 0),
areg(D_DX));
ins1(c, A_POPQ,
areg(D_AX));
ins1(c, A_POPQ,
areg(D_BX));
ins1(c, A_POPQ,
areg(D_CX));
ins2(c, A_MOVQ,
areg(D_AX),
amem(D_DX, foff + 0));
ins2(c, A_MOVQ,
areg(D_BX),
amem(D_DX, foff + 8));
ins2(c, A_MOVQ,
areg(D_CX),
amem(D_DX, foff + 16));
break;
}
if (n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ,
areg(D_AX));
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ,
aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ,
areg(D_CX),
areg(D_AX));
}
if (is_arr)
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_BX));
else
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_BX));
ins2(c, A_ADDQ,
areg(D_AX),
areg(D_BX));
if (viaptr)
ins2(c, A_MOVQ,
amem(D_BX, 0),
areg(D_BX));
ins1(c, A_POPQ,
areg(D_AX));
ins2(c, store_op,
areg(D_AX),
amem(D_BX, foff));
break;
}
/* compound: rhs→push; compute
* struct addr→BX (deref if *T);
* push addr; load old field→AX;
* pop addr→BX, rhs→CX; combine;
* store. Float/str compound
* not wired. */
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, idx, locals);
if (esz > 1) {
ins2(c, A_MOVQ,
aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ,
areg(D_CX),
areg(D_AX));
}
if (is_arr)
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_BX));
else
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_BX));
ins2(c, A_ADDQ, areg(D_AX),
areg(D_BX));
if (viaptr)
ins2(c, A_MOVQ,
amem(D_BX, 0),
areg(D_BX));
ins1(c, A_PUSHQ, areg(D_BX));
int load_op = fldloadop(ft, fsz);
ins2(c, load_op,
amem(D_BX, foff),
areg(D_AX));
ins1(c, A_POPQ, areg(D_BX));
ins1(c, A_POPQ, areg(D_CX));
switch (n->op) {
case TK_PLUSEQ:
ins2(c, A_ADDQ,
areg(D_CX),
areg(D_AX));
break;
case TK_MINUSEQ:
ins2(c, A_SUBQ,
areg(D_CX),
areg(D_AX));
break;
case TK_STAREQ:
ins2(c, A_IMULQ,
areg(D_CX),
areg(D_AX));
break;
case TK_AMPEQ:
ins2(c, A_ANDQ,
areg(D_CX),
areg(D_AX));
break;
case TK_PIPEEQ:
ins2(c, A_ORQ,
areg(D_CX),
areg(D_AX));
break;
case TK_CARETEQ:
ins2(c, A_XORQ,
areg(D_CX),
areg(D_AX));
break;
default: break;
}
ins2(c, store_op, areg(D_AX),
amem(D_BX, foff));
break;
}
}
}
}
/* Chained `<expr>.field = v` where <expr> evaluates to a *struct.
* cgexpr on the inner expression already returns the pointer;
* we then store at (ptr + field.offset). Without this, only the
* single-level N_IDENT base above is wired and shapes like
* `r.sym.flag = 1` (where r.sym: *T) silently emit no store —
* the read still works because the chained-N_DOT read path is
* wired below. (This was trap 1 of the cgen miscompilations.) */
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
&& n->lhs->lhs->kind != N_IDENT) {
Type *bt = n->lhs->lhs->type;
/* type_chase_named (#22); same rationale as the cgexpr-
* side pointer-to-struct field branch. */
Type *bu = type_chase_named(bt);
if (bu && bu->kind == TY_PTR && bu->sub) {
Type *inner = type_chase_named(bu->sub);
if (inner && inner->kind == TY_STRUCT) {
Tfield *f = NULL;
for (Tfield *fl = inner->fields; fl; fl = fl->next)
if (strcmp(fl->name, n->lhs->str) == 0)
{ f = fl; break; }
if (f != NULL) {
Type *ft = f->type;
/* Transitive chase (#5-F1 fold):
* 2-level alias slice/str field
* skipped the 3-word arm
* (reviewer-F1 p1 probe). */
Type *fu = type_chase_named(ft);
int fsz = (int)(ft ? ft->size : 8);
int store_op = fldstoreop(ft, fsz);
int foff = (int)f->offset;
if (n->op == TK_ASSIGN) {
/* tagged leaf (#38a): eval the *struct
* base into BX, then the shared widener
* (it spills BX across its internal
* cgexpr) — same base-then-widen order
* as the single-dot via_ptr arm. The
* scalar tail below stored ONE sized
* word at the field offset: the rhs
* landed in the TAG slot (ken b8:
* `o.p.min = 8: size` left `is size`
* false). */
if (fu && fu->kind == TY_TAGGED) {
cgexpr(c, n->lhs->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX),
areg(D_BX));
cg_widen_tagged_store(c,
&locals, fu, n->rhs,
D_BX, foff,
(int)fu->size);
break;
}
int c_isf32 = 0;
if (fld_isfloat(ft, &c_isf32)) {
/* f64/f32 chained-store: cgexpr rhs
* left the value in X0. Spill to stack
* so cgexpr on the inner pointer can
* use AX, then reload into X0 and
* MOVSD/MOVSS into the slot. */
int mov = c_isf32 ? A_MOVSS : A_MOVSD;
cgexpr(c, n->rhs, locals);
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, mov, areg(D_X0),
amem(D_SP, 0));
cgexpr(c, n->lhs->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX),
areg(D_BX));
ins2(c, mov, amem(D_SP, 0),
areg(D_X0));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
ins2(c, mov, areg(D_X0),
amem(D_BX, foff));
break;
}
if (fu && (fu->kind == TY_STR
|| fu->kind == TY_SLICE)) {
/* str/slice: rhs leaves AX=ptr,
* BX=len, CX=cap (#1/Phase 3). Spill
* all three across the base-expr eval
* (it may clobber any reg), stage the
* *struct ptr in DX off the str
* AX/BX/CX convention (mirrors s.f=v),
* then store the full triple at
* foff+0/+8/+16. */
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_CX));
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, n->lhs->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
ins1(c, A_POPQ, areg(D_AX));
ins1(c, A_POPQ, areg(D_BX));
ins1(c, A_POPQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_DX, foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, foff + 16));
} else {
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, n->lhs->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX),
areg(D_BX));
ins1(c, A_POPQ, areg(D_AX));
ins2(c, store_op, areg(D_AX),
amem(D_BX, foff));
}
break;
}
/* compound op: AX=rhs → push; eval ptr → push;
* load old field → AX; pop ptr→BX, rhs→CX;
* combine; store. #133-expanded: all 10 integer
* compound ops wired; SLASHEQ/PERCENTEQ via
* CQO+IDIV (signed) or zero-DX+DIV (unsigned);
* LSHIFTEQ via SHLQ on CX; RSHIFTEQ via SARQ
* (signed) or SHRQ (unsigned) on CX per #136.
* Float / str / slice / tagged element compound
* hard-errors LOUD (rule-7). */
{
int compound_isf32 = 0;
if (fld_isfloat(ft, &compound_isf32))
fatal("chained-ptr-field compound on "
"float element not wired "
"(#133/rule-7); field='%s'",
n->lhs->str);
Type *fchk = type_chase_named(ft);
if (fchk && fchk->kind == TY_STR)
fatal("chained-ptr-field compound on "
"str element not wired "
"(#133/rule-7); field='%s'",
n->lhs->str);
if (fchk && fchk->kind == TY_SLICE)
fatal("chained-ptr-field compound on "
"slice element not wired "
"(#133/rule-7); field='%s'",
n->lhs->str);
if (fchk && fchk->kind == TY_TAGGED)
fatal("chained-ptr-field compound on "
"tagged element not wired "
"(#133/rule-7); field='%s'",
n->lhs->str);
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, n->lhs->lhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
int load_op = fldloadop(ft, fsz);
ins2(c, load_op, amem(D_AX, foff),
areg(D_AX));
ins1(c, A_POPQ, areg(D_BX));
ins1(c, A_POPQ, areg(D_CX));
int unsignd = type_isunsigned(ft);
switch (n->op) {
case TK_PLUSEQ:
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
break;
case TK_MINUSEQ:
ins2(c, A_SUBQ, areg(D_CX), areg(D_AX));
break;
case TK_STAREQ:
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
break;
case TK_AMPEQ:
ins2(c, A_ANDQ, areg(D_CX), areg(D_AX));
break;
case TK_PIPEEQ:
ins2(c, A_ORQ, areg(D_CX), areg(D_AX));
break;
case TK_CARETEQ:
ins2(c, A_XORQ, areg(D_CX), areg(D_AX));
break;
case TK_SLASHEQ:
if (unsignd)
ins2(c, A_MOVQ, aimm(0),
areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ : A_IDIVQ,
areg(D_CX));
break;
case TK_PERCENTEQ:
if (unsignd)
ins2(c, A_MOVQ, aimm(0),
areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ : A_IDIVQ,
areg(D_CX));
ins2(c, A_MOVQ, areg(D_DX),
areg(D_AX));
break;
case TK_LSHIFTEQ:
ins2(c, A_SHLQ, areg(D_CX),
areg(D_AX));
break;
case TK_RSHIFTEQ:
ins2(c, unsignd ? A_SHRQ : A_SARQ,
areg(D_CX), areg(D_AX));
break;
default:
fatal("chained-ptr-field compound: "
"unknown op tk=%d (#133/rule-7); "
"field='%s'", n->op,
n->lhs->str);
}
ins2(c, store_op, areg(D_AX),
amem(D_BX, foff));
}
break;
}
}
}
}
/* Chained `<chain>.field = v` where <chain> spans value-struct
* dots ending at a root ident — `o.i.a = 10`, `v.a.b.c = …`.
* Also handles a slice/str pseudo-field leaf (`b.buf.len = 5`):
* spine walks down to the slice/str header, then the +0/+8/+16
* delta selects ptr/len/cap. Sibling of the chained-pointer-
* field branch above; without this the LHS is silently dropped
* (the existing 1-deep branch only fires for `ident.field = …`).
* Only plain `=` is wired — compound on a chained value-struct
* field is rare and stays unhandled. */
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
&& n->lhs->lhs->kind == N_DOT && n->op == TK_ASSIGN) {
struct { Type *pu; const char *name; } steps[16];
int nsteps = 0;
int ptr_root = 0;
Node *cur = n->lhs;
int abort = 0;
while (cur && cur->kind == N_DOT && cur->lhs) {
Type *pt = cur->lhs->type;
/* Transitive chase (#71): an alias-typed hop left pu
* TY_NAMED -> abort -> the generic cgplaceaddr spine
* (PUSHQ/LEAQ/ADDQ/POPQ) while wwstage emits the direct
* offset store. Runtime-correct both; chasing converges
* cs onto the fast direct arm = wwstage's asm. */
Type *pu = type_chase_named(pt);
if (!pu) { abort = 1; break; }
if (cur == n->lhs && (pu->kind == TY_SLICE
|| pu->kind == TY_STR)) {
/* leaf pseudo-field on slice/str header */
} else if (pu->kind == TY_STRUCT) {
/* value-struct hop */
} else if (pu->kind == TY_PTR && pu->sub
&& cur->lhs->kind == N_IDENT) {
/* `*T` root: dereference at emit time;
* walk through pointee struct fields.
* Last-hop only (root is a bare ident). */
Type *sub = type_chase_named(pu->sub);
if (sub && sub->kind == TY_STRUCT) {
pu = sub;
ptr_root = 1;
} else {
abort = 1;
break;
}
} else {
abort = 1;
break;
}
if (nsteps >= 16) { abort = 1; break; }
steps[nsteps].pu = pu;
steps[nsteps].name = cur->str;
nsteps++;
cur = cur->lhs;
}
if (!abort && cur && cur->kind == N_IDENT
&& nsteps > 0) {
int total_off = 0;
Type *leaf_type = NULL;
int slice_delta = -1;
int ok = 1;
for (int i = nsteps - 1; i >= 0; i--) {
Type *pu = steps[i].pu;
if (pu->kind == TY_SLICE
|| pu->kind == TY_STR) {
if (strcmp(steps[i].name, "ptr") == 0)
slice_delta = 0;
else if (strcmp(steps[i].name, "len") == 0)
slice_delta = 8;
else if (strcmp(steps[i].name, "cap") == 0)
slice_delta = 16;
else { ok = 0; break; }
} else {
Tfield *f = NULL;
for (Tfield *fl = pu->fields; fl; fl = fl->next)
if (strcmp(fl->name, steps[i].name) == 0)
{ f = fl; break; }
if (!f) { ok = 0; break; }
total_off += (int)f->offset;
leaf_type = f->type;
}
}
if (ok) {
int root_off = localfind(locals, cur->str);
int base_disp = root_off;
int is_global = 0;
int root_resolved = (root_off != 0);
if (!root_resolved && let_islet(cur->str)) {
root_resolved = 1;
is_global = 1;
}
if (root_resolved) {
/* `*T` root and global both store via CX as
* the base register; only the loader differs
* (LEAQ name(SB) vs MOVQ off(BP)). Compute it
* AFTER cgexpr(rhs) so AX/BX/X0 stay intact. */
int via_cx = is_global || ptr_root;
if (slice_delta >= 0) {
/* slice/str pseudo-field store. .ptr writes
* 8 bytes; .len / .cap write 8 bytes each
* (matches the existing N_IDENT pseudo-
* field branch). */
cgexpr(c, n->rhs, locals);
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_CX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_CX, total_off + slice_delta));
} else {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, base_disp + total_off + slice_delta));
}
break;
}
/* Transitive chase (#5-F1 fold): the walk
* HOPS chase (#71) but the LEAF gate
* single-peeled — 2-level alias slice leaf
* fell to the scalar tail (reviewer-F1 s4
* probe). */
Type *fu = type_chase_named(leaf_type);
int fsz = (int)(leaf_type
? leaf_type->size : 8);
int store_op = fldstoreop(leaf_type, fsz);
/* tagged leaf (#38a): full slot rewrite via
* the shared widener — the single-dot
* tagged-field arm (after_dot_assign)
* verbatim. The scalar tail below stored ONE
* sized word at the field offset: the rhs
* landed in the TAG slot and the payload
* kept its old bytes (ken x5d:
* `o.r.min = 8: size` left `is size`
* false). Only plain `=` reaches this
* walker (TK_ASSIGN gate above). */
if (fu && fu->kind == TY_TAGGED) {
int tsz = (int)fu->size;
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_BX));
cg_widen_tagged_store(c,
&locals, fu, n->rhs,
D_BX, total_off, tsz);
} else {
cg_widen_tagged_store(c,
&locals, fu, n->rhs,
D_BP,
base_disp + total_off,
tsz);
}
break;
}
if (fu && (fu->kind == TY_STR
|| fu->kind == TY_SLICE)) {
/* str/slice: store ptr/len/cap. cgexpr
* leaves CX=cap, so the via_cx base goes in
* DX (not CX) to avoid clobbering it — same
* as the single-dot str field store
* (#1/Phase 3). */
cgexpr(c, n->rhs, locals);
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_DX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, total_off + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_DX, total_off + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, total_off + 16));
} else {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, base_disp + total_off + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, base_disp + total_off + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, base_disp + total_off + 16));
}
break;
}
/* TY_STRUCT terminal in the chained-DOT walker:
* three rhs shapes — mirror of the single-dot
* branch.
* - N_IDENT: word-copy from rhs local slot.
* - N_CALL (added with #5): cgexpr → AX/DX/CX
* per #4's cgreturn ABI; sized stores per
* declared field size. cgreturn touches only
* AX/DX/CX so via_cx loads the dst addr into
* BX (not CX) after the call to keep CX as
* the third value word.
* - N_STRUCTLIT (added with #5): field-by-field
* store; via_cx reloads BX before each store
* so cgexpr can clobber AX/BX between fields.
*/
if (fu && fu->kind == TY_STRUCT
&& fsz <= 24
&& n->rhs && n->rhs->kind == N_CALL
&& (fsz % 8 == 0 || fsz % 8 == 1
|| fsz % 8 == 2 || fsz % 8 == 4)) {
cgexpr(c, n->rhs, locals);
int regs[3] = { D_AX, D_DX, D_CX };
int full = fsz / 8;
int tail = fsz % 8;
int base_reg, base_off;
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_BX));
base_reg = D_BX;
base_off = total_off;
} else {
base_reg = D_BP;
base_off = base_disp + total_off;
}
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(base_reg,
base_off + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW
: A_MOVB;
ins2(c, op, areg(regs[full]),
amem(base_reg,
base_off + full * 8));
}
break;
}
if (fu && fu->kind == TY_STRUCT
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
/* Delegate to the shared structlit fill
* helper. For via_cx (ptr_root | is_global),
* helper reloads BX before zero-fill loop +
* each field store. For local through chain,
* helper stores direct off BP. AND nested
* struct-typed structlit values recurse
* instead of silently dropping trailing
* bytes (#18 fix). */
int dst_mode = ptr_root ? DST_PTR_LOCAL
: is_global ? DST_GLOBAL : DST_BP;
int dst_disp = (dst_mode == DST_BP)
? (base_disp + total_off) : total_off;
cg_structlit_fill(c, &locals, fu,
n->rhs, dst_mode, base_disp,
is_global ? cur->str : NULL,
dst_disp);
break;
}
if (fu && fu->kind == TY_STRUCT
&& n->rhs && n->rhs->kind == N_IDENT
&& localfind(locals, n->rhs->str) != 0) {
int soff = localfind(locals, n->rhs->str);
int ssz = fsz;
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_CX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_CX));
}
int k = 0;
while (k + 8 <= ssz) {
ins2(c, A_MOVQ,
amem(D_BP, soff + k),
areg(D_AX));
if (via_cx)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_CX, total_off + k));
else
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, base_disp + total_off + k));
k += 8;
}
if (k < ssz) {
int tail = ssz - k;
int lop = (tail == 4) ? A_MOVL
: (tail == 1 ? A_MOVB : A_MOVQ);
ins2(c, lop,
amem(D_BP, soff + k),
areg(D_AX));
if (via_cx)
ins2(c, lop, areg(D_AX),
amem(D_CX, total_off + k));
else
ins2(c, lop, areg(D_AX),
amem(D_BP, base_disp + total_off + k));
}
break;
}
int sf32 = 0;
if (fld_isfloat(leaf_type, &sf32)) {
int mov = sf32 ? A_MOVSS : A_MOVSD;
cgexpr(c, n->rhs, locals);
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_CX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_CX));
ins2(c, mov, areg(D_X0),
amem(D_CX, total_off));
} else {
ins2(c, mov, areg(D_X0),
amem(D_BP, base_disp + total_off));
}
break;
}
cgexpr(c, n->rhs, locals);
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_CX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_CX));
ins2(c, store_op, areg(D_AX),
amem(D_CX, total_off));
} else {
ins2(c, store_op, areg(D_AX),
amem(D_BP, base_disp + total_off));
}
break;
}
}
}
}
/* float assignment to a local or top-level global. Globals
* route through LEAQ+indirect (no D_EXTERN SSE in w6a).
* Compound (`acc += d` etc.) loads slot into X1, combines
* into X1 (Plan 9 syntax: OP src, dst), stores X1 back —
* w6a's ADDSD/SUBSD/MULSD/DIVSD are register-register only,
* so we can't use a direct mem-form like the integer ADDQ. */
if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) {
cgexpr(c, n->rhs, locals); /* X0 */
int mvop = op_for(n, A_MOVSD, A_MOVSS);
int addop = op_for(n, A_ADDSD, A_ADDSS);
int subop = op_for(n, A_SUBSD, A_SUBSS);
int mulop = op_for(n, A_MULSD, A_MULSS);
int divop = op_for(n, A_DIVSD, A_DIVSS);
int off = localfind(locals, n->lhs->str);
int isglobal = (off == 0) && let_islet(n->lhs->str);
/* Loud twin of the IDENT-tail unresolved-name stop
* below (C1): wwstage resolves the name BEFORE its
* float dispatch, so a silent break here would make
* the stages disagree on the build verdict. */
if (off == 0 && !isglobal)
fatal("unsupported assign target: "
"unresolved identifier '%s'",
n->lhs->str);
if (n->op == TK_ASSIGN) {
if (off != 0) {
ins2(c, mvop, areg(D_X0), amem(D_BP, off));
} else {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
ins2(c, mvop, areg(D_X0), amem(D_CX, 0));
}
break;
}
/* Compound: X1 = load; X1 OP= X0; store X1. */
int fop = -1;
switch (n->op) {
case TK_PLUSEQ: fop = addop; break;
case TK_MINUSEQ: fop = subop; break;
case TK_STAREQ: fop = mulop; break;
case TK_SLASHEQ: fop = divop; break;
default: break;
}
if (off != 0) {
if (fop < 0) {
/* Unsupported compound (e.g., %= on float):
* fall back to plain store of rhs. */
ins2(c, mvop, areg(D_X0),
amem(D_BP, off));
break;
}
ins2(c, mvop, amem(D_BP, off), areg(D_X1));
ins2(c, fop, areg(D_X0), areg(D_X1));
ins2(c, mvop, areg(D_X1), amem(D_BP, off));
} else {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
if (fop < 0) {
ins2(c, mvop, areg(D_X0),
amem(D_CX, 0));
break;
}
ins2(c, mvop, amem(D_CX, 0), areg(D_X1));
ins2(c, fop, areg(D_X0), areg(D_X1));
ins2(c, mvop, areg(D_X1), amem(D_CX, 0));
}
break;
}
/* arr[i] = v store. Base may be a simple ident (array/slice/
* ptr local) or a more complex expression like s.ptr where
* s: *[]u8. We compute the base address, scale the index by
* elem size, and store with the right size. */
if (n->lhs->kind == N_INDEX && n->lhs->lhs && !place_slit) {
Node *base = n->lhs->lhs;
Type *bt = base->type;
Type *u = type_chase_named(bt);
int is_arr = u && u->kind == TY_ARRAY;
int is_sl = u && u->kind == TY_SLICE;
int is_ptr = u && u->kind == TY_PTR;
/* For `*[N]T` drill through to the array so esz reflects
* T, not sizeof(array). Base load still uses u (MOVQ
* because is_ptr stays true). */
Type *eff = idx_eff(bt);
int esz = (eff && eff->sub) ? (int)eff->sub->size : 1;
int elem_is_str = eff && eff->sub && type_isstr(eff->sub);
int elem_is_slice = eff && eff->sub && type_isslice(eff->sub);
Type *esub = eff ? eff->sub : NULL;
Type *esubu = (esub && esub->kind == TY_NAMED)
? esub->under : esub;
int elem_tagged = esubu && esubu->kind == TY_TAGGED;
/* Tagged-union element: route widening through a
* scratch slot, then copy slot bytes to &arr[i].
* Materialising into the scratch first lets us reuse
* the full cg_widen_tagged_store machinery — scalar /
* str / struct / subset payloads, tag remap, nullable
* fold — without duplicating it. The scratch lives in
* the function frame; no cleanup needed. */
if ((is_arr || is_sl || is_ptr) && elem_tagged) {
int ssz = esz;
int scr = cg_tagscr_slot(c, &locals, ssz);
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < ssz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + k));
cg_widen_tagged_store(c, &locals, esubu,
n->rhs, D_BP, scr, ssz);
/* Compute &arr[i] → BX. */
cgexpr(c, n->lhs->rhs, locals);
if (ssz > 1) {
ins2(c, A_MOVQ, aimm(ssz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
if (base->kind == N_IDENT && is_arr) {
int boff = localfind(locals, base->str);
ins2(c, A_LEAQ, amem(D_BP, boff),
areg(D_BX));
} else if (base->kind == N_IDENT) {
int boff = localfind(locals, base->str);
ins2(c, A_MOVQ, amem(D_BP, boff),
areg(D_BX));
} else if (cg_dotbase_addr(c, base, D_BX, locals)) {
/* #259: N_DOT base resolved inline to the
* field address; cgexpr fallback would
* auto-deref + load the array field as a
* VALUE (the broken shape). dst BX keeps the
* scaled index live in AX (spill contract). */
} else {
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, base, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
ins1(c, A_POPQ, areg(D_AX));
}
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
/* Copy scratch slot → dest. */
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ, amem(D_BP, scr + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, k));
}
break;
}
/* #234: over-cap sret STORE into an indexed lvalue —
* `arr[i] = wide();` STORE-twin of the Fold-B sret RECEIVE
* (a937d67). cg_sret_dest_off is a STATIC BP-relative
* offset, so only a CONSTANT index into a LOCAL value array
* yields a static dest slot (boff + idx*esz) the callee can
* sret straight into. Every other indexed form — runtime
* index, slice/ptr base, global base — needs the runtime
* RDI-pointer dest variant deferred to #234-tail and HARD-
* STOPS loud (rule 7 — never the truncating store below). */
if (n->op == TK_ASSIGN && n->rhs
&& n->rhs->kind == N_CALL
&& esub && cg_sret_retsize(esub) > 0) {
int cidx = (n->lhs->rhs
&& n->lhs->rhs->kind == N_INTLIT)
? (int)n->lhs->rhs->uval : -1;
int sboff = (base->kind == N_IDENT)
? localfind(locals, base->str) : 0;
if (!is_arr || cidx < 0 || sboff == 0)
fatal("#234-tail: over-cap tuple sret "
"store to non-local dest "
"unsupported");
cg_sret_dest_off = sboff + cidx * esz;
cgexpr(c, n->rhs, locals);
cg_sret_dest_off = 0;
break;
}
/* #270-1b: aggregate (struct/array/tuple >8B) element
* STORE `a[i] = val`. The scalar store path below copies
* only the first 8 bytes (fldstoreop MOVQ) — a silent
* truncation. Compute &a[i] (dest) and the rhs SOURCE
* address, then word-copy esz bytes: the WRITE-twin of the
* #268 let-init copy loop. Source shapes mirror that loop
* (ident local/global, N_DOT field via cg_dotchain_addr,
* `*p` deref); struct-lit sources divert at the place_slit
* gate above (#20), array-lit dies loud (task #32), and a
* by-value call result still falls to the scalar tail —
* RAX-only store, task #31-G. */
if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN
&& esubu && (esubu->kind == TY_STRUCT
|| esubu->kind == TY_ARRAY
|| esubu->kind == TY_TUPLE)
&& esz > 8
&& ((n->rhs->kind == N_IDENT)
|| (n->rhs->kind == N_DOT)
|| (n->rhs->kind == N_UN
&& n->rhs->op == TK_STAR))) {
/* dest &a[i] → BX */
cgexpr(c, n->lhs->rhs, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */
if (base->kind == N_IDENT) {
int off = localfind(locals, base->str);
int isglobal = (off == 0)
&& let_islet(base->str);
if (isglobal && is_arr)
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
else if (isglobal)
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_BX));
else if (is_arr)
ins2(c, A_LEAQ,
amem(D_BP, off), areg(D_BX));
else
ins2(c, A_MOVQ,
amem(D_BP, off), areg(D_BX));
} else if (cg_dotbase_addr(c, base, D_BX, locals)) {
/* N_DOT array-field base resolved inline. */
} else {
cgexpr(c, base, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
}
ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
ins1(c, A_PUSHQ, areg(D_BX)); /* spill dest */
/* rhs source address → SI */
if (n->rhs->kind == N_UN
&& n->rhs->op == TK_STAR) {
cgexpr(c, n->rhs->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_SI));
} else if (n->rhs->kind == N_IDENT) {
int soff = localfind(locals,
n->rhs->str);
if (soff != 0)
ins2(c, A_LEAQ,
amem(D_BP, soff),
areg(D_SI));
else
ins2(c, A_LEAQ,
masym(c, n->rhs->str),
areg(D_SI));
} else {
cg_dotchain_addr(c, n->rhs, D_SI, locals);
}
ins1(c, A_POPQ, areg(D_BX)); /* dest */
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, k));
}
if (k + 4 <= esz) {
ins2(c, A_MOVL, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BX, k));
k += 4;
}
if (k + 2 <= esz) {
ins2(c, A_MOVW, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVW, areg(D_AX),
amem(D_BX, k));
k += 2;
}
if (k + 1 <= esz) {
ins2(c, A_MOVB, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BX, k));
k += 1;
}
break;
}
if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len,CX=cap if str) */
/* str/slice: stash cap+len so all three store
* (#1/Phase 3). */
if (elem_is_str || elem_is_slice) {
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
}
/* Float element: spill X0 (not AX — AX is junk
* for floats) across the idx/base eval. A call-
* index (`a[geti()]=v`) clobbers X0 and would
* otherwise lose the value. Mirrors the *p=v
* float deref store at cgen.c:4187 (#125). */
int sp_isfloat = type_isfloat(esub);
int sp_mov = sp_isfloat
? (type_isf32(esub) ? A_MOVSS : A_MOVSD) : 0;
if (sp_isfloat) {
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, sp_mov, areg(D_X0), amem(D_SP, 0));
} else {
ins1(c, A_PUSHQ, areg(D_AX));
}
cgexpr(c, n->lhs->rhs, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */
/* base address → BX. Top-level array → LEAQ
* name(SB); top-level ptr → MOVQ name(SB); locals
* route off BP. */
if (base->kind == N_IDENT) {
int off = localfind(locals, base->str);
int isglobal = (off == 0) &&
let_islet(base->str);
if (isglobal && is_arr) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
} else if (isglobal) {
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_BX));
} else if (is_arr) {
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_BX));
}
} else if (cg_dotbase_addr(c, base, D_BX, locals)) {
/* #135 site: N_DOT base resolved inline to
* the field address; cgexpr fallback below
* would auto-deref + load the field as a
* VALUE (the broken shape). */
} else {
cgexpr(c, base, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
}
ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
/* Reload value: float reloads X0 from the spill
* slot; non-float pops AX. Twin of the value-spill
* site above (#125). */
if (sp_isfloat) {
ins2(c, sp_mov, amem(D_SP, 0), areg(D_X0));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
} else {
ins1(c, A_POPQ, areg(D_AX)); /* value (ptr if str) */
}
if (elem_is_str || elem_is_slice) {
/* str/slice: store ptr/len/cap (#1/Phase 3, #7). */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
ins1(c, A_POPQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
ins1(c, A_POPQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16));
break;
}
/* float element → store FROM X0 (MOVSS/MOVSD): cgexpr
* leaves a float value in X0, and for f32 the #104
* CVTSD2SS narrowing only touches X0 — the AX path
* below would store the raw double low-bits (garbage
* for f32). Float-ness from esub, mirroring the read
* side at cgen.c:6423 (#122). #125: the value-spill
* pair above keeps X0 live across the idx/base eval
* so this MOVSS/MOVSD is correct even on call-index
* shapes. */
if (type_isfloat(esub)) {
int mov = type_isf32(esub) ? A_MOVSS : A_MOVSD;
ins2(c, mov, areg(D_X0), amem(D_BX, 0));
break;
}
int store_op = fldstoreop(esub, esz);
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
break;
}
/* Compound assign on an indexed scalar element
* (`arr[i] OP= v`). Pre-#133 this branch had no TK_ASSIGN
* gate above and silently DEMOTED compound ops to plain
* stores (no load, no op). Mirror the chained-pointer-
* field compound template at cgen.c:3281-3317: same
* address computation as the ASSIGN body above, then
* load_op (BX)→AX, pop rhs→CX, combine, store_op.
* #133-expanded: all 10 integer compound ops wired;
* float/str/slice/tagged element compound HARD-ERRORS
* loud (rule-7, replaces prior silent fall-through).
* #136: signed RSHIFTEQ now uses A_SARQ (arithmetic
* shift). */
if ((is_arr || is_sl || is_ptr) && n->op != TK_ASSIGN) {
if (elem_is_str)
fatal("indexed-lvalue compound on "
"str element not wired "
"(#133/rule-7)");
if (elem_is_slice)
fatal("indexed-lvalue compound on "
"slice element not wired "
"(#133/rule-7)");
if (elem_tagged)
fatal("indexed-lvalue compound on "
"tagged element not wired "
"(#133/rule-7)");
if (esub && type_isfloat(esub))
fatal("indexed-lvalue compound on "
"float element not wired "
"(#133/rule-7)");
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, n->lhs->rhs, locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX));
if (base->kind == N_IDENT) {
int off = localfind(locals, base->str);
int isglobal = (off == 0) &&
let_islet(base->str);
if (isglobal && is_arr) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
} else if (isglobal) {
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_BX));
} else if (is_arr) {
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_BX));
}
} else if (cg_dotbase_addr(c, base, D_BX, locals)) {
/* #135 site: N_DOT base resolved inline to the
* field address. */
} else {
cgexpr(c, base, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
}
ins1(c, A_POPQ, areg(D_AX));
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
int load_op = fldloadop(esub, esz);
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
ins1(c, A_POPQ, areg(D_CX));
int unsignd_c = esub && type_isunsigned(esub);
switch (n->op) {
case TK_PLUSEQ:
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
break;
case TK_MINUSEQ:
ins2(c, A_SUBQ, areg(D_CX), areg(D_AX));
break;
case TK_STAREQ:
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
break;
case TK_AMPEQ:
ins2(c, A_ANDQ, areg(D_CX), areg(D_AX));
break;
case TK_PIPEEQ:
ins2(c, A_ORQ, areg(D_CX), areg(D_AX));
break;
case TK_CARETEQ:
ins2(c, A_XORQ, areg(D_CX), areg(D_AX));
break;
case TK_SLASHEQ:
if (unsignd_c)
ins2(c, A_MOVQ, aimm(0),
areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd_c ? A_DIVQ : A_IDIVQ,
areg(D_CX));
break;
case TK_PERCENTEQ:
if (unsignd_c)
ins2(c, A_MOVQ, aimm(0),
areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd_c ? A_DIVQ : A_IDIVQ,
areg(D_CX));
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
case TK_LSHIFTEQ:
ins2(c, A_SHLQ, areg(D_CX),
areg(D_AX));
break;
case TK_RSHIFTEQ:
ins2(c, unsignd_c ? A_SHRQ : A_SARQ,
areg(D_CX), areg(D_AX));
break;
default:
fatal("indexed-lvalue compound: "
"unknown op tk=%d (#133/rule-7)",
n->op);
}
int store_op_c = fldstoreop(esub, esz);
ins2(c, store_op_c, areg(D_AX), amem(D_BX, 0));
break;
}
}
/* Plain `r = expr;` where r is a tagged-union local.
* Delegates to cg_widen_tagged_store: covers nullable fold,
* tagged→tagged (with tag remap), struct payload (ident or
* literal), str payload, and scalar payload.
*
* #38b: an sret-classified tagged CALL result is in memory,
* not the cursor — an exact-type reassign falls through to
* the generic sret receive below; a widening receive needs
* mem-to-mem tag-remap (#40, unwired). */
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
&& n->lhs->type) {
Type *lt = n->lhs->type;
Type *lu = type_chase_named(lt);
if (lu && lu->kind == TY_TAGGED) {
int rhs_sret_call = n->rhs
&& n->rhs->kind == N_CALL
&& cg_sret_retsize(n->rhs->type) > 0;
if (!rhs_sret_call) {
int off = localfind(locals,
n->lhs->str);
if (off == 0) break;
cg_widen_tagged_store(c, &locals, lu,
n->rhs, D_BP, off, (int)lu->size);
break;
}
Type *ru = type_chase_named(n->rhs->type);
if (!(ru == lu || type_eq(n->rhs->type, lt)))
fatal("#40: sret-class call result "
"cannot be widened into a tagged "
"slot (mem-to-mem widen unwired)");
if (localfind(locals, n->lhs->str) == 0)
fatal("#38b: sret receive into a "
"tagged GLOBAL lvalue unwired");
}
}
/* #49 (#31-A fold): an aggregate pointee diverts the whole
* deref-assign to the resolver aggregate arm below — the
* scalar tail here stored ONE word of `*p = s` (#31-A);
* tuple-lit (#31-E) and call (#31-G) rhs now die loud there
* instead of silently truncating. str/slice pointees keep
* their 3-word arm here (byte-id-pinned). */
int deref_agg = 0;
if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR
&& n->op == TK_ASSIGN) {
Type *du = type_chase_named(n->lhs->type);
if (du && (du->kind == TY_STRUCT
|| du->kind == TY_ARRAY
|| du->kind == TY_TUPLE))
deref_agg = 1;
}
/* Deref-target assignment `*p = v;`. The size of the store is
* determined by the type *p points at; the pointer expression
* is evaluated after the value so we don't need to spill BX. */
if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR
&& n->op == TK_ASSIGN && !place_slit && !deref_agg) {
Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL;
Type *pu = type_chase_named(pt);
Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL;
vt = type_chase_named(vt);
/* `*p = v` for *f64 / *f32: cgexpr leaves the value in X0,
* not AX. Spill X0 to the stack, evaluate the pointer
* (clobbers AX/BX freely), then reload X0 and MOVSD/MOVSS
* through the pointer. */
int deref_isf32 = 0;
if (vt && fld_isfloat(vt, &deref_isf32)) {
int mov = deref_isf32 ? A_MOVSS : A_MOVSD;
cgexpr(c, n->rhs, locals);
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
cgexpr(c, n->lhs->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
ins2(c, mov, amem(D_SP, 0), areg(D_X0));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
ins2(c, mov, areg(D_X0), amem(D_BX, 0));
break;
}
cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len, CX=cap if str/slice) */
ins1(c, A_PUSHQ, areg(D_AX));
if (vt && (vt->kind == TY_STR || vt->kind == TY_SLICE)) {
/* str IS []u8 and a slice is the same 3-word
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8):
* stash len + cap across the pointer eval, which
* clobbers BX/CX (#1/Phase 3; slice arm #79). */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
}
cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
if (vt && (vt->kind == TY_STR || vt->kind == TY_SLICE)) {
ins1(c, A_POPQ, areg(D_CX)); /* cap */
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16));
ins1(c, A_POPQ, areg(D_CX)); /* len */
ins1(c, A_POPQ, areg(D_AX)); /* ptr */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
} else {
ins1(c, A_POPQ, areg(D_AX));
int sz = vt ? (int)vt->size : 8;
int store_op = fldstoreop(vt, sz);
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
}
break;
}
/* `*p OP= v` — compound assign through a pointer deref. The
* plain-assign branch above only fires for TK_ASSIGN; without
* this, compound ops fall through the switch and emit nothing
* (silent no-op). Evaluate rhs → save, evaluate ptr → BX, load
* *BX (sized + extended), combine with rhs in CX, sized store
* back. Scalar deref targets only — float and aggregate deref
* compounds (rare) still fall through. */
if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR
&& n->op != TK_ASSIGN) {
Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL;
Type *pu = type_chase_named(pt);
Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL;
vt = type_chase_named(vt);
int sz = vt ? (int)vt->size : 8;
int load_op = fldloadop(vt, sz);
int store_op = fldstoreop(vt, sz);
int handled = (sz == 1 || sz == 2 || sz == 4 || sz == 8);
if (handled) {
cgexpr(c, n->rhs, locals); /* AX = rhs */
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, n->lhs->lhs, locals); /* AX = ptr */
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
ins1(c, A_POPQ, areg(D_CX));
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); break;
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_CX), areg(D_AX)); break;
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); break;
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_CX), areg(D_AX)); break;
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_CX), areg(D_AX)); break;
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break;
case TK_LSHIFTEQ: ins2(c, A_SHLQ, areg(D_CX), areg(D_AX)); break;
case TK_RSHIFTEQ: {
/* #136: signed RSHIFTEQ → SARQ. */
int unsignd = (vt && type_isunsigned(vt))
|| (n->rhs && type_isunsigned(n->rhs->type));
ins2(c, unsignd ? A_SHRQ : A_SARQ,
areg(D_CX), areg(D_AX));
break;
}
case TK_SLASHEQ:
case TK_PERCENTEQ: {
int unsignd = (vt && type_isunsigned(vt))
|| (n->rhs && type_isunsigned(n->rhs->type));
if (unsignd)
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_CX));
if (n->op == TK_PERCENTEQ)
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
}
default:
/* unknown compound: legacy fallback —
* store rhs only. */
ins2(c, A_MOVQ, areg(D_CX), areg(D_AX));
break;
}
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
break;
}
}
/* `name = expr;` reassignment of a str/slice/struct local or
* top-level let. */
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
&& n->lhs->type) {
Type *lt = n->lhs->type;
Type *lu = type_chase_named(lt);
/* str/slice local/let: str IS []u8, so both store the full
* 3-word {ptr,len,cap} from (AX,BX,CX) at off+0/+8/+16
* (local) or via &name(SB) → DI scratch (global — CX holds
* the cap, and the asm has no `name+8(SB)` operand form, so
* a different address register is needed) (#1/Phase 3). */
if (lu && (lu->kind == TY_SLICE || lu->kind == TY_STR)) {
int off = localfind(locals, n->lhs->str);
if (off != 0) {
cgexpr(c, n->rhs, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
break;
}
if (let_islet(n->lhs->str)) {
cgexpr(c, n->rhs, locals);
ins2(c, A_MOVQ, areg(D_CX), areg(D_DI));
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, 8));
ins2(c, A_MOVQ, areg(D_DI), amem(D_CX, 16));
break;
}
break;
}
/* sret receive (#23 / #10 Fold B): `s = f();` where s's
* own slot IS the caller-prealloc dest; the callee writes
* through hidden RDI. Mirrors the cglet branch above and
* keys on cg_sret_retsize (the shared sret SSoT), NOT a
* kind — so an over-cap tuple reassign materialises its
* whole slot exactly like a >24B struct. */
if (cg_sret_retsize(lt) > 0
&& n->rhs && n->rhs->kind == N_CALL
&& n->op == TK_ASSIGN) {
int off = localfind(locals, n->lhs->str);
if (off != 0) {
cg_sret_dest_off = off;
cgexpr(c, n->rhs, locals);
cg_sret_dest_off = 0;
break;
}
/* #220: `g = f();` where g is a GLOBAL struct >24B.
* No BP slot to use as the sret dest, so route RDI
* to g's symbol address. Mirrors the str/slice
* global arm above (let_islet + LEAQ masym). The
* scalar fall-through below would emit a truncated
* 8-byte `MOVQ AX, g(SB)` and drop the struct body.
* Kept aggregate-only (struct + #272 array): a
* tuple-typed global reassign has no sret-to-symbol
* path in wwstage either, so leaving it to fall
* through keeps the stages aligned (rule-10). */
if (lu && (lu->kind == TY_STRUCT
|| lu->kind == TY_ARRAY)
&& let_islet(n->lhs->str)) {
cg_sret_dest_sym = n->lhs->str;
cgexpr(c, n->rhs, locals);
cg_sret_dest_sym = NULL;
break;
}
}
/* #49: aggregate (struct/array/tuple) IDENT
* reassignment — `s = expr;`. Literal and call rhs
* keep their dedicated receive arms; every OTHER rhs
* is an addressable source and funnels through the
* ONE mem-to-mem copy (aggarg_srcaddr → SI, dst
* address → BX, cg_aggcopy — the let-init copy's
* assign-position twin). Pre-#49 any shape that
* missed an arm fell to the scalar tail below and
* word0-copied: `b = a` lost every byte past 8 (ken
* f49_min; latent because lib style is let-init).
* The block never falls through to the scalar tail
* (rule 7). Keyed on the FULL alias chase (the #22
* type_chase_named precedent), NOT the region's
* single-peel `lu` — `type b = a; type a = struct`
* left a TY_NAMED after one peel, missing the arm
* (the wwstage twin full-chases the stamped tinfo;
* a single peel here would silently diverge). */
Type *au = type_chase_named(lt);
if (au && (au->kind == TY_STRUCT || au->kind == TY_ARRAY
|| au->kind == TY_TUPLE)) {
int off = localfind(locals, n->lhs->str);
int sz = (int)au->size;
int isglob = off == 0 && let_islet(n->lhs->str);
if (off == 0 && !isglob)
fatal("unsupported assign target: "
"unresolved identifier '%s'",
n->lhs->str);
if (n->rhs && n->rhs->kind == N_STRUCTLIT) {
if (off != 0) {
/* Delegate to the shared BP-relative
* structlit fill helper. Handles
* TK_ELLIPSIS autofill, tagged fields,
* float/scalar stores, AND nested
* struct-typed structlit values via
* recursion (#17 silent-zero fix).
* #31-B: the pre-#49 ≤24B gate is
* lifted — the fill walks fields at
* any size; the wwstage twin never
* gated, so a >24B literal reassign
* was cs-zero/ww-filled (rule-10). */
cg_structlit_fill_bp(c, &locals, au,
n->rhs, off);
break;
}
/* Global structlit reassign rides the
* DST_GLOBAL fill (the N_DOT global arms'
* machinery); pre-#49 it fell to the
* scalar tail and zeroed word0 only. */
cg_structlit_fill(c, &locals, au, n->rhs,
DST_GLOBAL, 0, n->lhs->str, 0);
break;
}
if (n->rhs && n->rhs->kind == N_CALL) {
if (off != 0 && au->kind != TY_TUPLE
&& sz <= 24
&& (sz % 8 == 0 || sz % 8 == 1
|| sz % 8 == 2
|| sz % 8 == 4)) {
cgexpr(c, n->rhs, locals);
int regs[3] = { D_AX, D_DX, D_CX };
int full = sz / 8;
int tail = sz % 8;
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(D_BP, off + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW
: A_MOVB;
ins2(c, op, areg(regs[full]),
amem(D_BP, off + full * 8));
}
break;
}
if (isglob && au->kind == TY_ARRAY
&& sz <= 24) {
/* #272: `g = f();` where g is a GLOBAL
* aggregate ≤24B. The callee leaves the result
* in AX/DX/CX (#272 reg-return); the scalar IDENT
* fall-through below would store only MOVQ AX,
* g(SB) = the first word. The asm has no `g+8(SB)`
* operand form, so LEAQ the symbol into DI and
* store the full+tail words. Mirrors the str/slice
* global arm above and the #220 sret-to-symbol path.
* #276: this arm is TY_ARRAY-only — a ≤24B STRUCT
* global receive can be float-class (X0/X1, not
* AX/DX/CX) so it has no receive here; pre-#49
* it fell through symmetric-silent, now it dies
* loud below. No consumer. Arrays are never
* float-class, so AX/DX/CX is always correct
* for this arm. */
cgexpr(c, n->rhs, locals);
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_DI));
int regs[3] = { D_AX, D_DX, D_CX };
int full = sz / 8;
int tail = sz % 8;
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(D_DI, i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW
: A_MOVB;
ins2(c, op, areg(regs[full]),
amem(D_DI, full * 8));
}
break;
}
fatal("assign: aggregate call receive "
"shape unwired (task #49/#276/"
"rule-7)");
}
if (aggarg_srcaddr(c, n->rhs, D_SI, locals)) {
if (off != 0)
ins2(c, A_LEAQ, amem(D_BP, off),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, n->lhs->str),
areg(D_BX));
cg_aggcopy(c, sz);
break;
}
fatal("assign: aggregate rhs shape unwired "
"(task #49/rule-7)");
}
}
/* F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the
* enumerated arms above matched — today the deref-rooted
* spine `(*p)[i].f = v` / `OP= v`. Base-address derivation
* routes through cgplaceaddr; the load/store emission stays
* here. Any N_DOT shape the resolver can't address dies
* LOUD below: the pre-C1 dispatch tail silently emitted
* NOTHING (rhs unevaluated) for every such shape.
* #20 (task): N_INDEX and N_UN(STAR) lvalues enroll too —
* only the struct-lit-rhs diversion above reaches here
* (every other indexed/deref shape broke out of its legacy
* arm), and the C1.25 aggregate branch fills via
* @placescr. */
if (n->lhs && (n->lhs->kind == N_DOT
|| n->lhs->kind == N_INDEX
|| (n->lhs->kind == N_UN && n->lhs->op == TK_STAR))) {
Type *ft = n->lhs->type;
Type *fu = type_chase_named(ft);
int fsz = (int)(ft ? ft->size : 8);
int pa_isf32 = 0;
if (fld_isfloat(ft, &pa_isf32))
fatal("assign-resolver: float field not "
"wired (rule-7)");
if (fu && fu->kind == TY_TAGGED)
fatal("assign-resolver: tagged field not "
"wired (rule-7)");
/* C1.25 (#23): aggregate field STORE through the
* resolver — run_thread's 40B capture store
* `(*ts)[i].root_capture = capture{...}`. Dest address
* from cgplaceaddr (BX), source address in SI per rhs
* shape, then the #270-1b word-copy tail (SI)→(BX).
* Pre-C1 this was a SILENT no-op; C1 made it loud;
* this wires it (loud-first, wire-next). Compound on
* an aggregate is meaningless and stays loud. */
if (fu && (fu->kind == TY_STRUCT
|| fu->kind == TY_ARRAY
|| fu->kind == TY_TUPLE)) {
if (n->op != TK_ASSIGN)
fatal("assign-resolver: compound on "
"aggregate field not wired "
"(rule-7)");
if (n->rhs && n->rhs->kind == N_CALL) {
/* sret-class needs a runtime-RDI dest
* (the #234-tail deferral); the ≤24B
* reg-return receive is task #24. */
if (cg_sret_retsize(ft) > 0)
fatal("assign-resolver: sret "
"call into aggregate field "
"unwired (#234-tail/"
"rule-7)");
fatal("assign-resolver: call result "
"into aggregate field unwired "
"(task #24/rule-7)");
}
int placed = 0;
if (n->rhs && n->rhs->kind == N_STRUCTLIT
&& fu->kind == TY_STRUCT) {
/* @placescr — FRESH slot PER USE (the
* @slicescr discipline, NOT the cached
* @tagscr table: a cached slot is the
* #31 multi-live corruption trap; rob
* ruling). Funnel contract, #44
* discipline: this arm is the ONLY
* @placescr alloc site. Fill handles
* nested literals (#18), tagged
* fields, TK_ELLIPSIS autofill; the
* value sits in memory, so the
* resolver below may clobber AX/CX
* freely. */
int scr = local_alloc(c, &locals,
"@placescr", fsz, cg_frame);
cg_structlit_fill_bp(c, &locals, fu,
n->rhs, scr);
placed = cgplaceaddr(c, n->lhs, D_BX,
locals);
if (placed)
ins2(c, A_LEAQ,
amem(D_BP, scr),
areg(D_SI));
} else {
/* Addressable source — ident / global
* / N_DOT chain / deref — via the
* closed #265/#268 dispatch. Its
* N_INDEX arm clobbers BX, so the dest
* spills around it (the #270-1b
* order). Literal arrays/tuples have
* no storage address and stay loud. */
placed = cgplaceaddr(c, n->lhs, D_BX,
locals);
if (placed) {
ins1(c, A_PUSHQ, areg(D_BX));
if (!aggarg_srcaddr(c, n->rhs,
D_SI, locals))
fatal("assign-resolver"
": aggregate rhs "
"shape unwired "
"(rule-7)");
ins1(c, A_POPQ, areg(D_BX));
}
}
if (!placed)
fatal("unsupported assign target "
"shape");
cg_aggcopy(c, fsz);
break;
}
if (fu && (fu->kind == TY_STR
|| fu->kind == TY_SLICE)) {
if (n->op != TK_ASSIGN)
fatal("assign-resolver: compound on "
"str/slice field not wired "
"(rule-7)");
/* str IS []u8: store the whole {ptr,len,cap}
* triple from (AX,BX,CX); the place address
* goes in DX so the three pops survive
* (#1/Phase 3). */
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_CX));
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_AX));
if (cgplaceaddr(c, n->lhs, D_DX, locals)) {
ins1(c, A_POPQ, areg(D_AX));
ins1(c, A_POPQ, areg(D_BX));
ins1(c, A_POPQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_DX, 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, 16));
break;
}
} else if (n->op == TK_ASSIGN) {
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
if (cgplaceaddr(c, n->lhs, D_BX, locals)) {
ins1(c, A_POPQ, areg(D_AX));
ins2(c, fldstoreop(ft, fsz),
areg(D_AX), amem(D_BX, 0));
break;
}
} else {
/* Compound: AX=old, CX=rhs, BX=addr — the
* same register roles as the chained-ptr-
* field compound template above. */
cgexpr(c, n->rhs, locals);
ins1(c, A_PUSHQ, areg(D_AX));
if (cgplaceaddr(c, n->lhs, D_BX, locals)) {
ins2(c, fldloadop(ft, fsz),
amem(D_BX, 0), areg(D_AX));
ins1(c, A_POPQ, areg(D_CX));
int unsignd = type_isunsigned(ft);
switch (n->op) {
case TK_PLUSEQ:
ins2(c, A_ADDQ, areg(D_CX),
areg(D_AX));
break;
case TK_MINUSEQ:
ins2(c, A_SUBQ, areg(D_CX),
areg(D_AX));
break;
case TK_STAREQ:
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
break;
case TK_AMPEQ:
ins2(c, A_ANDQ, areg(D_CX),
areg(D_AX));
break;
case TK_PIPEEQ:
ins2(c, A_ORQ, areg(D_CX),
areg(D_AX));
break;
case TK_CARETEQ:
ins2(c, A_XORQ, areg(D_CX),
areg(D_AX));
break;
case TK_SLASHEQ:
if (unsignd)
ins2(c, A_MOVQ,
aimm(0),
areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ
: A_IDIVQ, areg(D_CX));
break;
case TK_PERCENTEQ:
if (unsignd)
ins2(c, A_MOVQ,
aimm(0),
areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ
: A_IDIVQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_DX),
areg(D_AX));
break;
case TK_LSHIFTEQ:
ins2(c, A_SHLQ, areg(D_CX),
areg(D_AX));
break;
case TK_RSHIFTEQ:
ins2(c, unsignd ? A_SHRQ
: A_SARQ, areg(D_CX),
areg(D_AX));
break;
default:
fatal("assign-resolver: "
"unknown compound op "
"(rule-7)");
}
ins2(c, fldstoreop(ft, fsz),
areg(D_AX), amem(D_BX, 0));
break;
}
}
fatal("unsupported assign target shape");
}
if (n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
if (off == 0) {
/* Top-level let target — RIP-relative store
* (or load→combine→store for compound). A
* name that is neither a local nor a let
* dies LOUD: the pre-C1 break dropped the
* whole statement silently (no symbol was
* ever referenced, so not even a link error
* surfaced). */
if (!let_islet(n->lhs->str))
fatal("unsupported assign target: "
"unresolved identifier '%s'",
n->lhs->str);
cgexpr(c, n->rhs, locals);
if (n->op == TK_ASSIGN) {
ins2(c, A_MOVQ, areg(D_AX),
masym(c, n->lhs->str));
break;
}
/* Compound: BX = load; combine with AX; store
* BX. The asm has no RIP-relative ADDQ/SUBQ
* mem-form, so we use the explicit load→
* combine→store sequence uniformly. Narrow
* lets go through LEAQ + indirect load with
* localloadop so a prior `*(&letname): *iN`
* deref-store doesn't leave stale upper bytes
* in the read. */
int glop = localloadop(n->lhs->type);
if (glop == A_MOVQ) {
ins2(c, A_MOVQ, masym(c, n->lhs->str),
areg(D_BX));
} else {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
ins2(c, glop, amem(D_CX, 0),
areg(D_BX));
}
int did_compound = 1;
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); break;
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break;
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break;
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break;
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_AX), areg(D_BX)); break;
case TK_LSHIFTEQ:
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_SHLQ, areg(D_CX), areg(D_BX));
break;
case TK_RSHIFTEQ: {
/* #136: signed RSHIFTEQ → SARQ. */
int unsignd_r = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, unsignd_r ? A_SHRQ : A_SARQ,
areg(D_CX), areg(D_BX));
break;
}
case TK_SLASHEQ:
case TK_PERCENTEQ: {
/* Sister site of the IDENT-local path
* below. Park rhs (AX) in CX, slot value
* (BX) into AX, CQO sign-extend (or
* MOVQ $0, DX zero-extend), IDIVQ (or
* DIVQ) CX, ferry AX (quotient) or DX
* (remainder) back to BX for the shared
* store-BX tail. */
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
if (unsignd)
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_CX));
if (n->op == TK_SLASHEQ)
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
else
ins2(c, A_MOVQ, areg(D_DX), areg(D_BX));
break;
}
default:
/* unknown compound: legacy fallback —
* store rhs only. */
did_compound = 0;
ins2(c, A_MOVQ, areg(D_AX),
masym(c, n->lhs->str));
break;
}
if (did_compound)
ins2(c, A_MOVQ, areg(D_BX),
masym(c, n->lhs->str));
break;
}
cgexpr(c, n->rhs, locals);
if (n->op == TK_ASSIGN) {
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
break;
}
/* Compound: load → combine into BX → store. The two
* direct mem-form combines (ADDQ/SUBQ) are kept for
* the simple cases; the rest go through the generic
* register form. Signed-narrow slots take the explicit
* load-combine-store path so the load can sign-extend
* through localloadop — ADDQ/SUBQ on amem would read
* the raw 8B, which is wrong when the slot was last
* written by a 4B deref-store. */
int lop = localloadop(n->lhs->type);
if (lop == A_MOVQ && n->op == TK_PLUSEQ) {
ins2(c, A_ADDQ, areg(D_AX), amem(D_BP, off));
break;
}
if (lop == A_MOVQ && n->op == TK_MINUSEQ) {
ins2(c, A_SUBQ, areg(D_AX), amem(D_BP, off));
break;
}
ins2(c, lop, amem(D_BP, off), areg(D_BX));
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); break;
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break;
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break;
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break;
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_AX), areg(D_BX)); break;
case TK_LSHIFTEQ:
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_SHLQ, areg(D_CX), areg(D_BX));
break;
case TK_RSHIFTEQ: {
/* #136: signed RSHIFTEQ → SARQ. */
int unsignd_r = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, unsignd_r ? A_SHRQ : A_SARQ,
areg(D_CX), areg(D_BX));
break;
}
case TK_SLASHEQ:
case TK_PERCENTEQ: {
/* IDIV/DIV needs dividend in RDX:RAX, divisor
* in a GPR. Park rhs (currently AX) in CX, move
* slot value (BX) into AX, sign- or zero-extend
* into RDX:RAX, divide, then ferry the quotient
* (AX) or remainder (DX) back into BX for the
* shared store-BX-to-slot tail below. Post-#16:
* CQO is now in the assembler. */
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
if (unsignd)
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
else
ins0(c, A_CQO);
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_CX));
if (n->op == TK_SLASHEQ)
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
else
ins2(c, A_MOVQ, areg(D_DX), areg(D_BX));
break;
}
default:
/* unknown: just store rhs (legacy fallback) */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
goto skip_assign_store;
}
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off));
skip_assign_store: ;
}
/* C1 residual (task #22): a non-DOT lvalue no arm above
* matched still falls out SILENT here — the known member is
* the str-base element store family (`s[i] = v`: cstage
* drops, wwstage emits MOVB; pre-existing gate-blind
* divergence) plus tuple-member writes. The tail goes loud
* for the remaining kinds with #22, after the family gets a
* symmetric verdict. */
break;
}
case N_CALL: {
/* abort([msg]) — call rt_abort. Empty msg becomes (NULL, 0).
* Only fires when the checker tagged the callee as a builtin
* (lhs->type == ty_err); a user-declared `abort` in scope is
* resolved through the regular call path. */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
n->lhs->type == ty_err &&
strcmp(n->lhs->str, "abort") == 0) {
if (n->list) {
cgexpr(c, n->list, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
} else {
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
}
ins1(c, A_CALL, asym("rt_abort"));
break;
}
/* assert(cond[, msg]) — if !cond, call rt_abort. Compiles to:
* CMPQ $0, AX
* JNE skip
* <abort body>
* skip: */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
n->lhs->type == ty_err &&
strcmp(n->lhs->str, "assert") == 0 && n->list) {
cgexpr(c, n->list, locals);
char *skip = mklabel(c, "as");
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JNE, abranch(skip));
Node *msg = n->list->next;
if (msg) {
cgexpr(c, msg, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
} else {
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
}
ins1(c, A_CALL, asym("rt_abort"));
label(c, skip);
break;
}
/* Hare-style builtins: len(x) and append(s, v). */
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "len") == 0 && n->list) {
Node *a = n->list;
/* loop-peel: the wwstage mirror already chases
* multi-level TY_NAMED; cstage single-peeled, so a
* 2-level alias fell to the old silent fallback. With
* the loud tail below that asymmetry would surface as
* cs-rejects / ww-accepts — same predicate both
* stages. */
Type *u = type_chase_named(a->type);
int hdrish = u && (u->kind == TY_SLICE
|| u->kind == TY_STR);
int lendone = 0;
if (hdrish && a->kind == N_IDENT) {
int off = localfind(locals, a->str);
if (off == 0 && let_islet(a->str)) {
/* #231: str/slice GLOBAL — the .len word
* lives at the global's address+8, not a
* BP-relative slot (off==0 → MOVQ 8(BP)
* read a bogus stack slot). Route through
* the post-#1 value mangle so a private
* same-module same-leaf global isn't
* mis-resolved. */
ins2(c, A_LEAQ,
mahint(c, a->str, c->cur_mod),
areg(D_CX));
ins2(c, A_MOVQ, amem(D_CX, 8),
areg(D_AX));
lendone = 1;
} else if (off != 0) {
ins2(c, A_MOVQ, amem(D_BP, off + 8),
areg(D_AX));
lendone = 1;
}
/* off==0 non-let ident (e.g. a DATA-backed
* def): the old arm emitted MOVQ 8(BP) —
* garbage. Falls to the resolver route. */
} else if (hdrish
&& a->kind == N_DOT && a->lhs
&& a->lhs->kind == N_IDENT && a->str) {
/* #235: len() of a tuple-element slice/str
* (`len(t.N)`). Kept as an enumerated arm: tuples
* are not resolver-addressable (cgplaceaddr has no
* TY_TUPLE hop — that gap is #238). Load the
* element's .len word directly at
* BP + element_off + 8, mirroring the N_IDENT
* slice arm above and the tuple-field-offset walk
* (cgen.c N_DOT TY_TUPLE). */
Type *bt = a->lhs->type;
Type *bu = (bt && bt->kind == TY_NAMED)
? bt->under : bt;
if (bu && bu->kind == TY_TUPLE) {
int idx = 0;
for (const char *q = a->str; *q; q++)
idx = idx * 10 + (*q - '0');
Tparam *tp = bu->params;
int foff = 0;
/* C-t0/#22: slot stride (tuple_eslot),
* twin of the N_DOT TY_TUPLE walk. */
while (idx > 0 && tp) {
foff += tuple_eslot(tp->type);
tp = tp->next;
idx--;
}
int off = localfind(locals, a->lhs->str);
/* C-t3 (#48): GLOBAL tuple base —
* pre-fix localfind's 0 read the .len
* word at foff+8(BP), stack garbage,
* SILENT. Twin of the N_DOT TY_TUPLE
* global arm. */
if (off == 0
&& let_islet(a->lhs->str)) {
ins2(c, A_LEAQ,
masym(c, a->lhs->str),
areg(D_CX));
ins2(c, A_MOVQ,
amem(D_CX, foff + 8),
areg(D_AX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, off + foff + 8),
areg(D_AX));
}
lendone = 1;
}
/* struct-field N_DOT (`len(s.field)`): the old
* inner fallback returned .ptr as the length.
* Falls to the resolver route. */
} else if (hdrish && a->kind == N_INDEX) {
/* #19: len() of an INDEXED str/slice element
* (`len(xs[i])`). The N_INDEX str/slice load leaves
* AX=.ptr, BX=.len, CX=.cap (cgslicehdr) — the bare
* cgexpr fallback returned AX (the ptr) AS the
* length. Shuffle BX (the len word) into AX, the
* same MOVQ BX,AX shape as the #14 .len pseudo-field
* fix. Same family as #18 (shared cstage==wwstage gap,
* not a rule-10 divergence). */
cgexpr(c, a, locals);
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
lendone = 1;
} else if (u && u->kind == TY_ARRAY) {
ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX));
lendone = 1;
}
/* #10 (F2) + #41 (FA2/FB1): ONE uniform header-place
* route for every other slice/str place — the arm
* enumeration above leaked four siblings
* (#235 → #19 → F2 → FA2/FB1) because each new operand
* shape fell to a bare cgexpr fallback that returned
* the slice DATA POINTER as the length. Resolve the
* operand's header address (cgplaceaddr — deref /
* index / dot spines) and read the .len word at +8;
* non-place operands (call result, slicing expr,
* string literal — all previously the same silent
* ptr-garbage) die LOUD per rule 7. */
if (!lendone && hdrish
&& cgplaceaddr(c, a, D_BX, locals)) {
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_AX));
lendone = 1;
}
if (!lendone)
fatal("#10/#41: len() operand shape not "
"place-resolvable (rule-7)");
break;
}
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "free") == 0 && n->list &&
n->list->next == NULL) {
/* free(x) is a no-op: ww has no free by design
* (rt/alloc.s:30 — the bump allocator cannot reclaim
* a mid-chunk pointer; process exit does). The old
* CALL ffi_resolve("free") was an undefined reference
* unless an @symbol decl happened to be in scope (#27).
* The operand is still evaluated — Hare's free(expr)
* evaluates expr — so Hare code ports verbatim with
* its side effects intact. */
cgexpr(c, n->list, locals);
break;
}
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "delete") == 0 && n->list &&
n->list->next == NULL) {
/* delete(xs[i]) — single-element slice removal, the
* delete-half of #35. Shift [i+1..len) down one
* stride, len -= 1, cap unchanged. The move is a
* same-type whole-stride byte copy: src and dst are
* elements of the SAME slice, so no boxing/coercion
* exists for any element kind (str/slice 24B header,
* tagged box, struct body) — one word-copy loop
* serves all kinds, unlike append's value-store
* dispatch (#34) which boxes from a foreign source.
* Ascending j keeps src (j+1) ahead of dst (j), the
* safe memmove-down direction. Bounds are implicit
* (no index check, matching the rest of cgen).
*
* ; AX = i (cgexpr)
* ; PUSHQ AX ; 8(SP) = j counter
* ; LEAQ/MOVQ off(BP), AX ; &hdr (ident / *p base)
* ; PUSHQ AX ; (SP) = &hdr
* ; del_l:
* ; MOVQ (SP), DX ; MOVQ 8(SP), CX
* ; MOVQ 8(DX), BX ; SUBQ $1, BX
* ; CMPQ BX, CX ; JGE del_e ; j >= len-1
* ; [IMULQ esz, CX]
* ; MOVQ (DX), BX ; ADDQ CX, BX ; BX = dst
* ; word-copy esz bytes esz(BX) -> (BX)
* ; ADDQ $1, 8(SP) ; JMP del_l
* ; del_e:
* ; MOVQ (SP), DX ; SUBQ $1, 8(DX)
* ; ADDQ $16, SP */
Node *d = n->list; /* N_INDEX or N_SLICE,
* checker-validated */
Node *base = d->lhs;
Type *su = type_chase_named(base->type);
Type *esub = (su && su->sub)
? type_chase_named(su->sub) : NULL;
int esz = esub ? (int)esub->size : 0;
if (esz <= 0)
fatal("#35: delete() element size unresolved "
"(rule-7)");
if (d->kind == N_SLICE) {
/* delete(xs[lo:hi]) — range slice removal
* (fold-5a prereq P2; harec check.c:1994
* EXPR_SLICE). Shift [hi..len) down count =
* hi-lo strides, len -= count, cap
* unchanged; lo defaults 0, hi defaults
* len. delete(xs[:]) never enters the copy
* loop (lo+count == len at entry) and
* zeroes len. The per-element move is the
* single-element arm's same-slice
* whole-stride word copy with a DYNAMIC
* src offset (count*esz, via a src
* register) instead of the constant
* one-stride. Ascending j keeps src >=
* dst, the safe memmove-down direction.
* Bounds are implicit (no range check,
* matching the single-element arm and the
* rest of cgen).
*
* ; &hdr -> AX (ident / *p / xs[g])
* ; PUSHQ AX ; 16(SP)=&hdr
* ; lo -> AX ($0 default) ; PUSHQ AX
* ; ; 8(SP)=j
* ; count = hi - lo (hi: cgexpr or len)
* ; PUSHQ AX ; (SP)=count
* ; rdl_l:
* ; MOVQ 16(SP), DX ; MOVQ 8(SP), CX
* ; MOVQ (SP), AX ; ADDQ CX, AX
* ; MOVQ 8(DX), BX
* ; CMPQ BX, AX ; JGE rdl_e
* ; ; j+count>=len
* ; [IMULQ esz, CX]
* ; MOVQ (DX), BX ; ADDQ CX, BX ; dst
* ; MOVQ (SP), CX ; [IMULQ esz, CX]
* ; ADDQ BX, CX ; src
* ; word-copy esz bytes (CX) -> (BX)
* ; ADDQ $1, 8(SP) ; JMP rdl_l
* ; rdl_e:
* ; MOVQ 16(SP), DX ; MOVQ (SP), AX
* ; MOVQ 8(DX), BX ; SUBQ AX, BX
* ; MOVQ BX, 8(DX) ; len-=count
* ; ADDQ $24, SP */
int hdr_lea = 0;
int hdr_off = 0;
int hdr_ok = 0;
int hdr_idx = 0;
int osz = 0;
if (base->kind == N_IDENT &&
localfind(locals, base->str) != 0) {
hdr_lea = 1;
hdr_off = localfind(locals, base->str);
hdr_ok = 1;
}
/* (*p)[lo:hi]: header behind a local
* ptr-to-slice — the single-element arm's
* regex_shape twin. */
if (!hdr_ok && base->kind == N_UN &&
base->op == TK_STAR && base->lhs &&
base->lhs->kind == N_IDENT &&
localfind(locals, base->lhs->str) != 0) {
hdr_off = localfind(locals,
base->lhs->str);
hdr_ok = 1;
}
/* xs[g][lo:hi]: the header IS element g of
* an outer local slice — the fold-5a
* consumer shape (ref/hare/regex/regex.ha:333
* delete(jump_idxs[group_level][..])).
* Outer stride = the inner header type's
* own table size (su). */
if (!hdr_ok && base->kind == N_INDEX &&
base->lhs &&
base->lhs->kind == N_IDENT &&
localfind(locals, base->lhs->str) != 0) {
hdr_idx = 1;
hdr_off = localfind(locals,
base->lhs->str);
osz = su ? (int)su->size : 0;
if (osz <= 0)
fatal("#35: delete() outer "
"element size unresolved "
"(rule-7)");
hdr_ok = 1;
}
if (!hdr_ok)
fatal("#35: delete() range base "
"shape unsupported (rule-7: "
"local slice ident, "
"deref-of-local, or indexed "
"local slice only)");
if (hdr_idx) {
cgexpr(c, base->rhs, locals);
if (osz > 1) {
ins2(c, A_MOVQ, aimm(osz),
areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
}
ins2(c, A_MOVQ,
amem(D_BP, hdr_off), areg(D_CX));
ins2(c, A_ADDQ, areg(D_CX),
areg(D_AX));
} else if (hdr_lea)
ins2(c, A_LEAQ,
amem(D_BP, hdr_off), areg(D_AX));
else
ins2(c, A_MOVQ,
amem(D_BP, hdr_off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
if (d->rhs)
cgexpr(c, d->rhs, locals);
else
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
if (d->cond)
cgexpr(c, d->cond, locals);
else {
ins2(c, A_MOVQ, amem(D_SP, 8),
areg(D_CX));
ins2(c, A_MOVQ, amem(D_CX, 8),
areg(D_AX));
}
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
ins2(c, A_SUBQ, areg(D_CX), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
char *rll = mklabel(c, "rdl_l");
char *rle = mklabel(c, "rdl_e");
label(c, rll);
ins2(c, A_MOVQ, amem(D_SP, 16), areg(D_DX));
ins2(c, A_MOVQ, amem(D_SP, 8), areg(D_CX));
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_AX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_BX));
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
ins1(c, A_JGE, abranch(rle));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz),
areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX),
areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz),
areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX),
areg(D_CX));
}
ins2(c, A_ADDQ, areg(D_BX), areg(D_CX));
int rk = 0;
for (; rk + 8 <= esz; rk += 8) {
ins2(c, A_MOVQ, amem(D_CX, rk),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, rk));
}
if (rk + 4 <= esz) {
ins2(c, A_MOVL, amem(D_CX, rk),
areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BX, rk));
rk += 4;
}
if (rk + 2 <= esz) {
ins2(c, A_MOVW, amem(D_CX, rk),
areg(D_AX));
ins2(c, A_MOVW, areg(D_AX),
amem(D_BX, rk));
rk += 2;
}
if (rk + 1 <= esz) {
ins2(c, A_MOVB, amem(D_CX, rk),
areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BX, rk));
rk += 1;
}
ins2(c, A_ADDQ, aimm(1), amem(D_SP, 8));
ins1(c, A_JMP, abranch(rll));
label(c, rle);
ins2(c, A_MOVQ, amem(D_SP, 16), areg(D_DX));
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_AX));
ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_BX));
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, 8));
ins2(c, A_ADDQ, aimm(24), areg(D_SP));
break;
}
int hdr_lea = 0;
int hdr_off = 0;
int hdr_ok = 0;
if (base->kind == N_IDENT &&
localfind(locals, base->str) != 0) {
hdr_lea = 1;
hdr_off = localfind(locals, base->str);
hdr_ok = 1;
}
/* (*p)[i]: the header lives behind a local
* ptr-to-slice — the regex fold-2b delete_thread
* shape (threads: *[]thread). */
if (!hdr_ok && base->kind == N_UN &&
base->op == TK_STAR && base->lhs &&
base->lhs->kind == N_IDENT &&
localfind(locals, base->lhs->str) != 0) {
hdr_off = localfind(locals, base->lhs->str);
hdr_ok = 1;
}
if (!hdr_ok)
fatal("#35: delete() base shape unsupported "
"(rule-7: local slice ident or "
"deref-of-local only)");
cgexpr(c, d->rhs, locals); /* AX = i */
ins1(c, A_PUSHQ, areg(D_AX));
if (hdr_lea)
ins2(c, A_LEAQ, amem(D_BP, hdr_off), areg(D_AX));
else
ins2(c, A_MOVQ, amem(D_BP, hdr_off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
char *dll = mklabel(c, "del_l");
char *dle = mklabel(c, "del_e");
label(c, dll);
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_DX));
ins2(c, A_MOVQ, amem(D_SP, 8), areg(D_CX));
ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_BX));
ins2(c, A_SUBQ, aimm(1), areg(D_BX));
ins2(c, A_CMPQ, areg(D_BX), areg(D_CX));
ins1(c, A_JGE, abranch(dle));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
int dk = 0;
for (; dk + 8 <= esz; dk += 8) {
ins2(c, A_MOVQ, amem(D_BX, esz + dk), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, dk));
}
if (dk + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BX, esz + dk), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_BX, dk));
dk += 4;
}
if (dk + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BX, esz + dk), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_BX, dk));
dk += 2;
}
if (dk + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BX, esz + dk), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_BX, dk));
dk += 1;
}
ins2(c, A_ADDQ, aimm(1), amem(D_SP, 8));
ins1(c, A_JMP, abranch(dll));
label(c, dle);
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_DX));
ins2(c, A_SUBQ, aimm(1), amem(D_DX, 8));
ins2(c, A_ADDQ, aimm(16), areg(D_SP));
break;
}
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "insert") == 0 && n->list &&
n->list->next && n->list->next->next == NULL) {
/* insert(xs[idx], v) — delete()'s twin, the
* insert-half of #35: insert v BEFORE idx, idx==len
* is a legal end-insert. Lowered as a DESUGAR to
* append(xs, v) + a rotate-right of [idx, len):
* the append arm below contributes grow (rt_ensure)
* and the whole #34 value-store dispatch (scalar /
* str-slice header / tagged widen / struct fill)
* verbatim — one boxing choke-point, byte-id by
* construction — landing v at slot len-1; the
* rotate then moves it home through an esz frame
* scratch. The rotate is delete's shift loop in
* reverse (descending j keeps src j behind dst j+1,
* the safe memmove-up direction) and, like
* delete's, is a same-slice whole-stride raw byte
* move — no boxing exists for any element kind.
* idx evaluates BEFORE the grow (Hare's
* left-to-right operand order: insert(xs[len(xs)],
* v) sees the pre-grow len); v's evaluation point
* inherits append's per-kind rules. Bounds are
* implicit (no index check, matching delete).
*
* ; AX = idx (cgexpr) ; PUSHQ AX
* ; ...append(xs, v) body (grow + store at end,
* ; push-balanced)...
* ; LEAQ/MOVQ off(BP), AX ; &hdr (ident / *p)
* ; PUSHQ AX ; (SP)=&hdr 8(SP)=idx
* ; save elem[len-1] -> @insscr (word copy)
* ; MOVQ 8(DX), AX ; SUBQ $2, AX ; PUSHQ AX
* ; ; (SP)=j=len-2
* ; ins_l:
* ; MOVQ (SP), CX ; MOVQ 16(SP), DX
* ; CMPQ DX, CX ; JL ins_e ; j < idx
* ; [IMULQ esz, CX]
* ; MOVQ 8(SP), DX ; MOVQ (DX), BX ; ADDQ CX, BX
* ; word-copy esz bytes (BX) -> esz(BX)
* ; SUBQ $1, (SP) ; JMP ins_l
* ; ins_e:
* ; store @insscr -> elem[idx] (word copy)
* ; ADDQ $24, SP */
Node *d = n->list; /* N_INDEX, checker-validated */
Node *base = d->lhs;
Node *v = d->next;
Type *su = type_chase_named(base->type);
Type *esub = (su && su->sub)
? type_chase_named(su->sub) : NULL;
int esz = esub ? (int)esub->size : 0;
if (esz <= 0)
fatal("#35: insert() element size unresolved "
"(rule-7)");
int hdr_lea = 0;
int hdr_off = 0;
int hdr_ok = 0;
if (base->kind == N_IDENT &&
localfind(locals, base->str) != 0) {
hdr_lea = 1;
hdr_off = localfind(locals, base->str);
hdr_ok = 1;
}
/* (*p)[i]: header behind a local ptr-to-slice —
* delete's regex_shape twin. */
if (!hdr_ok && base->kind == N_UN &&
base->op == TK_STAR && base->lhs &&
base->lhs->kind == N_IDENT &&
localfind(locals, base->lhs->str) != 0) {
hdr_off = localfind(locals, base->lhs->str);
hdr_ok = 1;
}
if (!hdr_ok)
fatal("#35: insert() base shape unsupported "
"(rule-7: local slice ident or "
"deref-of-local only)");
/* Fresh esz-sized slot per SITE (esz varies; an
* @-name dedup would mis-share across element
* types). */
int ins_scr = local_alloc(c, &locals, "@insscr",
esz, cg_frame);
cgexpr(c, d->rhs, locals); /* AX = idx */
ins1(c, A_PUSHQ, areg(D_AX));
/* Desugar in place and re-dispatch into the append
* arm: cgen is single-pass, base is an lhs node
* (never on a sibling chain), and the checker has
* already validated this call — the mutation is
* dead after this emission. */
n->lhs->str = "append";
n->list = base;
base->next = v;
cgexpr(c, n, locals);
if (hdr_lea)
ins2(c, A_LEAQ, amem(D_BP, hdr_off), areg(D_AX));
else
ins2(c, A_MOVQ, amem(D_BP, hdr_off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), areg(D_DX));
ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
int ik = 0;
for (; ik + 8 <= esz; ik += 8) {
ins2(c, A_MOVQ, amem(D_BX, ik), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, ins_scr + ik));
}
if (ik + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BX, ik), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_BP, ins_scr + ik));
ik += 4;
}
if (ik + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BX, ik), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_BP, ins_scr + ik));
ik += 2;
}
if (ik + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BX, ik), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_BP, ins_scr + ik));
ik += 1;
}
ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_AX));
ins2(c, A_SUBQ, aimm(2), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
char *ill = mklabel(c, "ins_l");
char *ile = mklabel(c, "ins_e");
label(c, ill);
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
ins2(c, A_MOVQ, amem(D_SP, 16), areg(D_DX));
ins2(c, A_CMPQ, areg(D_DX), areg(D_CX));
ins1(c, A_JL, abranch(ile));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_SP, 8), areg(D_DX));
ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
ik = 0;
for (; ik + 8 <= esz; ik += 8) {
ins2(c, A_MOVQ, amem(D_BX, ik), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, esz + ik));
}
if (ik + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BX, ik), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_BX, esz + ik));
ik += 4;
}
if (ik + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BX, ik), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_BX, esz + ik));
ik += 2;
}
if (ik + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BX, ik), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_BX, esz + ik));
ik += 1;
}
ins2(c, A_SUBQ, aimm(1), amem(D_SP, 0));
ins1(c, A_JMP, abranch(ill));
label(c, ile);
ins2(c, A_MOVQ, amem(D_SP, 16), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_SP, 8), areg(D_DX));
ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
ik = 0;
for (; ik + 8 <= esz; ik += 8) {
ins2(c, A_MOVQ, amem(D_BP, ins_scr + ik), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, ik));
}
if (ik + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BP, ins_scr + ik), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_BX, ik));
ik += 4;
}
if (ik + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BP, ins_scr + ik), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_BX, ik));
ik += 2;
}
if (ik + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BP, ins_scr + ik), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_BX, ik));
ik += 1;
}
ins2(c, A_ADDQ, aimm(24), areg(D_SP));
break;
}
if (n->lhs && n->lhs->kind == N_IDENT &&
n->lhs->type == ty_err &&
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 &&
n->list) {
/* alloc(value): heap-init a fresh *T with the value's
* bytes. Size comes from the value's static type.
* `n->lhs->type == ty_err` gate (mirrors assert above)
* — check.c only stamps ty_err when no user-scoped
* `alloc` shadows the builtin (task #23).
*
* Result is the graduated `(*T | nomem)` tagged-pointer
* ABI (AX=tag, DX=ptr) — task #30. rt_malloc returns 0
* on OOM (rt/alloc.s); we branch on AX, building tag=1
* (nomem, DX=0) on null and tag=0 (success, DX=ptr)
* after the value-init stores complete. Callers wrap
* with `!` / `?` to consume the union. */
Node *v = n->list;
Type *t = v->type;
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
Type *def = type_default(t);
int sz = def ? (int)def->size : 8;
if (sz == 0) sz = 8;
char *alloc_ok = mklabel(c, "alloc_ok");
char *alloc_done = mklabel(c, "alloc_done");
ins2(c, A_MOVQ, aimm(sz), areg(D_DI));
ins1(c, A_CALL, asym(ffi_resolve("malloc")));
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JNE, abranch(alloc_ok));
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
ins1(c, A_JMP, abranch(alloc_done));
label(c, alloc_ok);
ins1(c, A_PUSHQ, areg(D_AX)); /* save ptr */
if (v->kind == N_STRUCTLIT && u && u->kind == TY_STRUCT) {
for (Node *f = v->list; f; f = f->next) {
u64 foff = 0;
int fsz = 8;
Type *ftype = NULL;
for (Tfield *fl = u->fields; fl; fl = fl->next) {
if (strcmp(fl->name, f->str) == 0) {
foff = fl->offset;
fsz = (int)(fl->type ? fl->type->size : 8);
ftype = fl->type;
break;
}
}
cgexpr(c, f->lhs, locals); /* AX or (AX,BX) or X0 */
int f_isf32 = 0;
if (fld_isfloat(ftype, &f_isf32)) {
int mov = f_isf32 ? A_MOVSS : A_MOVSD;
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
ins2(c, mov, areg(D_X0),
amem(D_BX, (int)foff));
continue;
}
/* str IS []u8: cgexpr leaves (AX=ptr, BX=len,
* CX=cap). Route the heap base through DX so all
* three survive — CX now holds cap, BX holds len
* (#1/Phase 3). */
Type *fu = type_chase_named(ftype);
if (fu && fu->kind == TY_STR) {
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, (int)foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_DX, (int)foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, (int)foff + 16));
continue;
}
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
int op = A_MOVQ;
if (fsz == 1) op = A_MOVB;
else if (fsz == 4) op = A_MOVL;
ins2(c, op, areg(D_AX), amem(D_BX, (int)foff));
}
} else {
cgexpr(c, v, locals); /* AX = value */
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
int op = A_MOVQ;
if (sz == 1) op = A_MOVB;
else if (sz == 4) op = A_MOVL;
ins2(c, op, areg(D_AX), amem(D_BX, 0));
}
ins1(c, A_POPQ, areg(D_DX)); /* DX = success ptr */
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
label(c, alloc_done);
break;
}
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
strcmp(n->lhs->str, "append") == 0 && n->list &&
n->list->next) {
/* append(s, v) lowering — Hare's rt::ensure model.
* ; AX = value
* ; PUSHQ AX ; save
* ; ADDQ $1, sn_off+8(BP) ; s.len += 1
* ; LEAQ sn_off(BP), DI ; arg1 = &s
* ; MOVQ esz, SI ; arg2 = membsz
* ; CALL rt_ensure(SB) ; may realloc s.ptr
* ; MOVQ sn_off+8(BP), CX ; CX = new len
* ; SUBQ $1, CX ; slot index
* ; [IMULQ esz, CX] ; byte offset (esz>1)
* ; MOVQ sn_off(BP), BX ; reread s.ptr
* ; ADDQ CX, BX ; BX = target
* ; POPQ AX ; v
* ; MOV* AX, (BX) ; store (MOVB / MOVQ)
*
* Spread form `append(s, items...)` runs this same body
* in a counted loop over items. */
Node *sn = n->list;
Type *st = sn->type;
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
int esz = (su && su->sub) ? (int)su->sub->size : 1;
Type *esub = su ? su->sub : NULL;
int sn_off = (sn->kind == N_IDENT)
? localfind(locals, sn->str) : 0;
/* FA1 (#15): a non-ident-local target used to silently
* 0-default sn_off, making 0(BP)/8(BP) the "header" —
* rt_ensure then corrupted the caller frame. Non-direct
* targets resolve through cgplaceaddr below; a shape it
* can't address is loud. */
int sn_direct = sn_off != 0;
int store_op = fldstoreop(esub, esz);
/* #34 element-kind store dispatch: the scalar 1-word
* store below silently gutted every wide element
* (str/slice 24B header, tagged box, struct body).
* Mirrors the #270/#12/#20 array-literal element
* dispatch (cg_arrlit_fill_bp). */
Type *esubu = type_chase_named(esub);
int el_str = type_isstr(esub);
int el_slice = type_isslice(esub);
int el_tagged = esubu && esubu->kind == TY_TAGGED;
int el_struct = esubu && esubu->kind == TY_STRUCT;
int el_wide = el_str || el_slice || el_tagged ||
el_struct;
if (!el_wide && esz > 8)
fatal("#34: append() element kind "
"unsupported (rule-7)");
int sn_scr = 0;
if (!sn_direct) {
if (!cgplaceaddr(c, sn, D_BX, locals))
fatal("#15: append() target place "
"unsupported (rule-7)");
/* Fresh slot per SITE, not a per-fn cache: a
* nested append-through-pointer inside a value
* expression (match-yield arm) spills its own
* resolve; a shared slot would hand the outer
* grow/slot reloads the inner target's header.
* local_alloc never dedups by name. */
sn_scr = local_alloc(c, &locals,
"@apphdrscr", 8, cg_frame);
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, sn_scr));
}
for (Node *vn = sn->next; vn; vn = vn->next) {
/* #34 review: a spread whose source is not a
* local ident used to fall PAST the spread arm
* into the single-value stores with the
* N_SPREAD node (cstage garbage store; wwstage
* silently SKIPPED it — divergent). */
if (vn->kind == N_SPREAD) {
/* #35: only a {ptr,len,cap}-headered
* source reads as a header below; a
* [N]T array place IS its storage —
* the ident path used to read its
* first 16 data bytes as ptr/len,
* silently. Loud until wired (task
* #27); str shares the slice header
* layout. */
Type *itu = vn->lhs
? type_chase_named(vn->lhs->type) : NULL;
if (itu == NULL || (itu->kind != TY_SLICE
&& itu->kind != TY_STR))
fatal("#35: append() spread source "
"shape unsupported (rule-7)");
int it_off = 0;
int it_scr = 0;
if (vn->lhs->kind == N_IDENT)
it_off = localfind(locals, vn->lhs->str);
if (it_off == 0) {
/* #35: place-chain source (deref
* spine, indexed chain, global
* ident — the regex.ha:569/820
* dup shapes) resolves its header
* ADDRESS through cgplaceaddr
* ONCE, pre-grow: the chain's
* rvalues run exactly once (the
* #49 split's pre-grow half) and
* every iteration re-reads
* .ptr/.len THROUGH the spilled
* header post-grow (the live
* re-derivation half). A header
* reached through a buffer the
* grow reallocs keeps Hare's
* stale-base hole — see the #49
* comment below. Rvalue sources
* (CALL, slicing exprs) have no
* place — loud, task #27. Fresh
* spill slot per SITE for the
* same nesting reason as
* @apphdrscr above. */
if (!cgplaceaddr(c, vn->lhs, D_BX, locals))
fatal("#35: append() spread source "
"shape unsupported (rule-7)");
it_scr = local_alloc(c, &locals,
"@appsprscr", 8, cg_frame);
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, it_scr));
}
int load_op = fldloadop(esub, esz);
/* push counter (i) on stack */
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, A_MOVQ, aimm(0), amem(D_SP, 0));
char *ll = mklabel(c, "spr_l");
char *le = mklabel(c, "spr_e");
label(c, ll);
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
if (it_scr != 0) {
ins2(c, A_MOVQ, amem(D_BP, it_scr), areg(D_DX));
ins2(c, A_MOVQ, amem(D_DX, 8), areg(D_DX));
} else
ins2(c, A_MOVQ, amem(D_BP, it_off + 8), areg(D_DX));
ins2(c, A_CMPQ, areg(D_DX), areg(D_CX));
ins1(c, A_JGE, abranch(le));
if (el_wide) {
/* #34: a spread element is already a
* fully-formed T in the source slice
* (tag included), so a whole-width
* word-copy is the store — no boxing.
* Grow FIRST: rt_ensure may realloc,
* so both addresses are recomputed
* from the slice headers after the
* call (i reloads from the counter
* slot; CX was clobbered). */
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
if (it_scr != 0) {
ins2(c, A_MOVQ, amem(D_BP, it_scr), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX));
} else
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_DX);
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, k));
}
if (k + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_DX, k));
k += 4;
}
if (k + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_DX, k));
k += 2;
}
if (k + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_DX, k));
k += 1;
}
ins2(c, A_ADDQ, aimm(1), amem(D_SP, 0));
ins1(c, A_JMP, abranch(ll));
label(c, le);
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
continue;
}
/* AX = items.ptr[i] */
if (it_scr != 0) {
ins2(c, A_MOVQ, amem(D_BP, it_scr), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX));
} else
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
/* ensure + store one element */
ins1(c, A_PUSHQ, areg(D_AX));
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
ins1(c, A_POPQ, areg(D_AX));
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
/* loop tail */
ins2(c, A_ADDQ, aimm(1), amem(D_SP, 0));
ins1(c, A_JMP, abranch(ll));
label(c, le);
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
continue;
}
if (el_str || el_slice) {
/* #34: 24B {ptr,len,cap} header. cgexpr
* leaves AX/BX/CX; all three must survive
* rt_ensure. dst lands in DX, NOT BX — the
* pops put the element .len back in BX
* (the #24 register discipline). */
cgexpr(c, vn, locals);
ins1(c, A_PUSHQ, areg(D_AX));
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_CX));
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_DX);
ins1(c, A_POPQ, areg(D_CX));
ins1(c, A_POPQ, areg(D_BX));
ins1(c, A_POPQ, areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, 16));
continue;
}
if (el_tagged || el_struct) {
/* #34: no register form survives rt_ensure
* for these. struct: grow FIRST, then fill
* through the dst pointer (literal fill /
* ident word-copy). tagged: #50 — the #12
* widen choke-point cgexprs the value
* internally, so boxing must run PRE-grow
* (Hare's argument order: a `xs.len` read
* in v sees the pre-append len, like the
* scalar arm); box into a frame scratch,
* grow, raw-copy the finished box in. */
/* #49 (#35's single-element sibling): a
* place-chain source (indexed field
* `threads[i].root_capture` regex.ha:819,
* deref spine, computed index) SPLITS
* around the grow per the #49 ruling:
* the chain's rvalues (deref-root
* pointer expr, index expr) evaluate
* exactly once PRE-grow — an index
* reading the slice header sees the
* pre-append len, Hare's argument
* order — and only the BASE re-derives
* POST-grow from the live storage, so
* a self-append source re-roots in the
* post-realloc buffer. harec resolves
* an aggregate source address wholly
* PRE-grow (gen.c: gen_load returns
* the address for STORAGE_STRUCT,
* gen_store copies after rt.ensure) —
* a use-after-free under a reclaiming
* allocator; per #263 we align to the
* runtime-correct side, not the
* reference. A pointer ALIASING the
* grown buffer keeps Hare's own
* stale-base hole (sound today only
* because rt/malloc.ww never
* reclaims). Spec not vendored
* (ref/hare/docs = man pages only),
* spec-silence assumed — re-verify if
* the spec is ever vendored.
* Bounded shapes: root (local/global
* ident | deref) + at most one index
* + trailing direct fields; all else
* stays on the #34 fatal (incl. CALL
* rvalues, the #42-style bound). */
int aplace = 0, afld = 0, aidx_esz = 0;
int abase_slice = 0, aroot_off = 0;
Node *aroot = NULL, *aidx = NULL;
if (el_struct && vn->kind != N_STRUCTLIT
&& vn->kind != N_IDENT) {
Node *ch = vn;
int aok = 1;
while (aok && ch->kind == N_DOT) {
Node *ab = ch->lhs;
Type *abu = ab ? type_chase_named(ab->type) : NULL;
Tfield *af = NULL;
if (abu && abu->kind == TY_STRUCT)
for (Tfield *fl = abu->fields; fl; fl = fl->next)
if (strcmp(fl->name, ch->str) == 0) { af = fl; break; }
if (af == NULL) { aok = 0; break; }
afld += (int)af->offset;
ch = ab;
}
if (aok && ch->kind == N_INDEX) {
Node *ab = ch->lhs;
Type *abu = ab ? type_chase_named(ab->type) : NULL;
Type *aet = type_chase_named(ch->type);
if (ab == NULL || abu == NULL || aet == NULL
|| (abu->kind != TY_SLICE && abu->kind != TY_ARRAY)) {
aok = 0;
} else {
abase_slice = abu->kind == TY_SLICE;
aidx_esz = (int)aet->size;
aidx = ch->rhs;
ch = ab;
}
}
if (aok) {
if (ch->kind == N_IDENT) {
aroot_off = localfind(locals, ch->str);
if (aroot_off == 0
&& !let_islet(ch->str)
&& !def_isstructdef(ch->str)
&& !def_isarraydef(ch->str))
aok = 0;
} else if (!(ch->kind == N_UN && ch->op == TK_STAR)) {
aok = 0;
}
}
if (!aok)
fatal("#34: append() struct element source "
"shape unsupported (rule-7)");
aroot = ch;
if (aroot->kind == N_UN) {
if (cg_appendsroot == 0)
cg_appendsroot = local_alloc(c,
&locals, "@appendsroot", 8,
cg_frame);
cgexpr(c, aroot->lhs, locals);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, cg_appendsroot));
}
if (aidx != NULL) {
if (cg_appendsoff == 0)
cg_appendsoff = local_alloc(c,
&locals, "@appendsoff", 8,
cg_frame);
cgexpr(c, aidx, locals);
if (aidx_esz > 1) {
ins2(c, A_MOVQ, aimm(aidx_esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, cg_appendsoff));
}
aplace = 1;
}
if (el_tagged) {
/* Fresh slot per SITE, not the
* shared per-size scratch: the
* box must stay live across
* rt_ensure, and a nested
* append inside the value
* expression would clobber a
* dedup'd slot (the @apphdrscr
* rationale; #25/#31). */
int tg_scr = local_alloc(c,
&locals, "@apptagscr", esz,
cg_frame);
ins2(c, A_XORQ, areg(D_AX),
areg(D_AX));
for (int zk = 0; zk < esz; zk += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, tg_scr + zk));
cg_widen_tagged_store(c, &locals,
esub, vn, D_BP, tg_scr, esz);
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
for (int ck = 0; ck < esz; ck += 8) {
ins2(c, A_MOVQ,
amem(D_BP, tg_scr + ck),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, ck));
}
continue;
}
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
if (vn->kind == N_STRUCTLIT) {
/* #59 (#50's eval-order kin):
* the literal's field exprs
* still eval POST-grow here —
* filed, not folded. */
if (cg_appendscr == 0)
cg_appendscr = local_alloc(c,
&locals, "@appendscr", 8,
cg_frame);
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, cg_appendscr));
cg_structlit_fill(c, &locals, esubu,
vn, DST_PTR_LOCAL, cg_appendscr,
NULL, 0);
continue;
}
if (vn->kind == N_IDENT) {
int soff = localfind(locals, vn->str);
if (soff == 0)
fatal("#34: append() struct "
"element source ident is "
"not a local (rule-7)");
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, k));
}
if (k + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_BX, k));
k += 4;
}
if (k + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_BX, k));
k += 2;
}
if (k + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_BX, k));
k += 1;
}
continue;
}
if (aplace) {
/* phase 2: dst slot to
* @appendscr, base from the
* live storage, stashed
* offsets back on top. */
if (cg_appendscr == 0)
cg_appendscr = local_alloc(c,
&locals, "@appendscr", 8,
cg_frame);
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, cg_appendscr));
if (aroot->kind == N_IDENT) {
if (aroot_off != 0)
ins2(c, A_LEAQ,
amem(D_BP, aroot_off),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, aroot->str),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, cg_appendsroot),
areg(D_BX));
}
if (aidx != NULL) {
if (abase_slice)
ins2(c, A_MOVQ,
amem(D_BX, 0),
areg(D_BX));
ins2(c, A_MOVQ,
amem(D_BP, cg_appendsoff),
areg(D_AX));
ins2(c, A_ADDQ, areg(D_AX),
areg(D_BX));
}
if (afld != 0)
ins2(c, A_ADDQ, aimm(afld),
areg(D_BX));
ins2(c, A_MOVQ,
amem(D_BP, cg_appendscr),
areg(D_DX));
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, k));
}
if (k + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_DX, k));
k += 4;
}
if (k + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_DX, k));
k += 2;
}
if (k + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_DX, k));
k += 1;
}
continue;
}
fatal("#34: append() struct element source "
"shape unsupported (rule-7)");
}
cgexpr(c, vn, locals); /* val → AX */
ins1(c, A_PUSHQ, areg(D_AX));
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
ins1(c, A_POPQ, areg(D_AX));
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
}
break;
}
/* up to 6 integer + 8 float args via SysV registers.
* str args occupy two integer eightbytes (ptr, len). The
* arg-buffer cap accommodates Hare-style variadic gather
* (`fmt.println(a, b, c, ...)`) where N args of element
* type T fold into a single []T slice slot below. */
int argcount = 0;
Node *args[64] = {0};
for (Node *a = n->list; a; a = a->next)
if (argcount < 64) args[argcount++] = a;
/* Resolve callee fn-type so we can match each arg against
* its declared parameter type — needed to detect implicit
* widening of a concrete variant into a tagged-union slot. */
Type *callee_t = n->lhs ? n->lhs->type : NULL;
Type *cu = type_chase_named(callee_t);
Tparam *callee_params = (cu && cu->kind == TY_FN) ?
cu->params : NULL;
/* Hare-style variadic last param: gather N tail args into a
* stack-resident []T or forward an `xs...` spread, then
* splice in a single slice arg so the downstream widen/push/
* pop machinery sees one 24B slice slot for the variadic.
*
* Forward shape: `f(... , xs...)` becomes `f(... , xs)`.
* Gather shape: `f(... , e0, e1, eN)` materialises e0..eN
* into a frame-resident `[N]T` (widening each element when T
* is a tagged union), writes a 24B slice descriptor
* {ptr=&data, len=N, cap=N}, and replaces the tail args with
* an N_IDENT pointing at the descriptor. Empty form
* (`f(...)` with no variadic args) writes {0, 0, 0}. */
{
int nfixed = 0;
Tparam *var_p = NULL;
for (Tparam *p = callee_params; p; p = p->next) {
if (p->variadic) { var_p = p; break; }
nfixed++;
}
if (var_p != NULL) {
int nvar = argcount - nfixed;
if (nvar < 0) nvar = 0;
int forwarding = (nvar == 1 && args[nfixed] &&
args[nfixed]->kind == N_SPREAD);
if (forwarding) {
args[nfixed] = args[nfixed]->lhs;
argcount = nfixed + 1;
} else {
Type *vst = var_p->type;
Type *vsu = type_chase_named(vst);
Type *velem = (vsu && vsu->kind == TY_SLICE)
? vsu->sub : NULL;
int esz = (velem && velem->size)
? (int)velem->size : 8;
/* Allocate dname BEFORE sname so the
* descriptor lives below the element
* buffer, matching wwstage's emit-time
* order (rule 10). */
int doff = 0;
if (nvar > 0) {
const char *dname = mklabel(c, "vararg_d");
doff = localoff(c, &locals,
dname, nvar * esz, cg_frame);
}
const char *slname = mklabel(c, "vararg_sl");
/* #60: route slice-descriptor width through
* vsu->size so a future slice-header bump
* propagates (mirrors wwstage cgcall vararg
* gather using tyslicesize()). */
int sloff = localoff(c, &locals,
slname, (int)vsu->size, cg_frame);
if (nvar > 0) {
/* #38b: a >48B tagged variadic ELEMENT
* would need the memory convention
* inside the vararg gather buffer —
* unwired (rule 7). */
if (velem &&
tagged_memarg_size(velem) > 0)
fatal("#38b: >48B tagged "
"variadic element "
"unwired");
int v_is_tagged = velem &&
tagged_arg_size(velem) > 0;
int v_is_str = type_isstr(velem);
int v_is_slice = type_isslice(velem);
for (int j = 0; j < nvar; j++) {
Node *a = args[nfixed + j];
int slot = doff + j * esz;
if (v_is_tagged) {
cg_widen_tagged_store(c,
&locals, velem,
a, D_BP, slot, esz);
continue;
}
cgexpr(c, a, locals);
/* str / slice element: cgexpr
* returns the full descriptor in
* AX/(BX)/(CX); a bare MOVQ AX
* stores .ptr only and the
* trailing fields read stack
* garbage at the callee. */
if (v_is_str) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, slot));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, slot + 8));
continue;
}
if (v_is_slice) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, slot));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, slot + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, slot + 16));
continue;
}
int op = A_MOVQ;
if (esz == 1) op = A_MOVB;
else if (esz == 4) op = A_MOVL;
ins2(c, op, areg(D_AX),
amem(D_BP, slot));
}
}
if (nvar > 0)
ins2(c, A_LEAQ,
amem(D_BP, doff),
areg(D_AX));
else
ins2(c, A_XORQ, areg(D_AX),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, sloff + 0));
ins2(c, A_MOVQ, aimm(nvar),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, sloff + 8));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, sloff + 16));
Node *sn = newnode(c->a, N_IDENT, n->pos);
sn->str = slname;
sn->strlen = 0;
sn->type = vst;
args[nfixed] = sn;
argcount = nfixed + 1;
}
}
}
/* Family C (#35): peel tagged→tagged casts FIRST so the
* widen decision below keys on the operand's type — an
* identity cast (`take((v: un))`) reduces to the ident
* fast path, a widening cast (`take((v: wider))`) trips
* widen[i] and re-boxes with the remap. cgexpr on the
* cast node itself collapses to one word (silent word0
* push pre-#35). */
for (int i = 0; i < argcount; i++)
args[i] = cg_tagged_castpeel(args[i]);
/* widen[i]: param is tagged and arg needs re-layout.
* - arg is a concrete variant (str/struct/scalar) — wrap
* in the param's slot shape.
* - arg is itself a tagged union of a subset/different
* variant set — copy the slot words and remap the tag.
* Identical types pass through unchanged. */
int widen[64] = {0};
int widen_sz[64] = {0};
Type *widen_param[64] = {0};
int memarg[64] = {0};
{
Tparam *p = callee_params;
for (int i = 0; i < argcount; i++) {
if (p == NULL) break;
Type *at = args[i] ? args[i]->type : NULL;
int psz = tagged_arg_size(p->type);
if (psz > 0) {
Type *pu = type_chase_named(p->type);
Type *au = type_chase_named(at);
int same = (pu == au) || type_eq(p->type, at);
if (!same) {
widen[i] = 1;
widen_sz[i] = psz;
widen_param[i] = p->type;
}
}
/* #38b: MEMORY-class param (>48B tagged) —
* same widen detection, memory transport. */
int msz = tagged_memarg_size(p->type);
if (msz > 0) {
memarg[i] = msz;
Type *pu = type_chase_named(p->type);
Type *au = type_chase_named(at);
int same = (pu == au) || type_eq(p->type, at);
if (!same) {
widen[i] = 1;
widen_sz[i] = msz;
widen_param[i] = p->type;
}
}
p = p->next;
}
/* #38b: exact-type >48B tagged arg with no declared
* param to key off (fn-ptr callee, param-list
* mismatch) — MEMORY-class by the arg's own stamped
* type. */
for (int i = 0; i < argcount; i++)
if (!memarg[i] && args[i])
memarg[i] =
tagged_memarg_size(args[i]->type);
}
/* #38b MEMORY-class pre-pass: stage every >48B tagged arg on
* the stack BELOW all register-class words (rightmost-first,
* so the leftmost mem arg lands at the lowest address = the
* callee's first positive-BP cursor slot at 16(BP)). The pop
* loop below drains a strict prefix of the stack, so the mem
* copies are never popped; the caller-cleanup ADDQ reclaims
* them with the spill slots after CALL. Layout per
* ref/qbe/amd64/sysv.c:411-426 (stack blit, left-to-right). */
int memslots = 0;
for (int i = argcount - 1; i >= 0; i--) {
if (!memarg[i]) continue;
int msz = memarg[i];
if (widen[i]) {
cg_widen_tagged_push(c, &locals,
widen_param[i], args[i], widen_sz[i]);
memslots += widen_sz[i] / 8;
continue;
}
if (args[i]->kind == N_CALL)
fatal("#38b: sret-class tagged call result "
"as a >48B by-value arg unwired "
"(#40-family follow-up)");
if (args[i]->kind == N_IDENT) {
int off = localfind(locals, args[i]->str);
if (off != 0) {
for (int k = msz/8 - 1; k >= 0; k--) {
ins2(c, A_MOVQ,
amem(D_BP, off + k*8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
memslots += msz / 8;
continue;
}
}
if (aggarg_srcaddr(c, args[i], D_SI, locals)) {
for (int k = msz/8 - 1; k >= 0; k--) {
ins2(c, A_MOVQ, amem(D_SI, k*8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
memslots += msz / 8;
continue;
}
/* #40/FB3: a place the enumerated arms miss —
* slice element, deref-spine element — resolves
* through the F6 resolver. AFTER aggarg_srcaddr
* 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, args[i], D_SI, locals)) {
for (int k = msz/8 - 1; k >= 0; k--) {
ins2(c, A_MOVQ, amem(D_SI, k*8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
memslots += msz / 8;
continue;
}
fatal("#38b: >48B tagged arg from unsupported source "
"kind %d (rvalue and unresolvable-place sources "
"unwired)", args[i]->kind);
}
/* eval right-to-left, push to stack. Each N_IDENT fast-path
* is guarded by !widen[i] so the tagged-union widening (which
* needs to synthesise tag + payload + pad) takes precedence
* over the verbatim slice/struct/tagged-ident loads below. */
for (int i = argcount - 1; i >= 0; i--) {
if (memarg[i]) /* #38b: staged by the mem pre-pass */
continue;
if (!widen[i] && node_isslice(args[i]) && args[i]->kind == N_IDENT) {
int off = localfind(locals, args[i]->str);
/* push cap, len, ptr (top) so pops give ptr,len,cap */
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
continue;
}
if (!widen[i] && args[i]->kind == N_SLICE) {
Node *base = args[i]->lhs;
Node *lo = args[i]->rhs;
Node *hi = args[i]->cond;
Type *bt = base ? base->type : NULL;
Type *bu = type_chase_named(bt);
/* esz from the type table for an N_IDENT base
* (#76) or an N_DOT array/slice-field base
* (#257: scale by the field's element width via
* the checker-stamped base->type, not esz=1 --
* silently wrong for non-u8). Other non-ident
* bases stay esz=1 (unscaled). */
int esz = (base && (base->kind == N_IDENT
|| base->kind == N_DOT
|| base->kind == N_ARRLIT)
&& bu && bu->sub)
? (int)bu->sub->size : 1;
/* base addr → push */
if (base->kind == N_IDENT) {
int boff = localfind(locals, base->str);
int isglobal = (boff == 0) &&
let_islet(base->str);
if (isglobal && bu && bu->kind == TY_ARRAY) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_AX));
} else if (isglobal) {
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_AX));
} else if (bu && bu->kind == TY_ARRAY) {
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
} else {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
}
} else if (cg_dotbase_addr(c, base, D_AX, locals)) {
/* #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 (correct header/ptr load). */
} else {
cgexpr(c, base, locals);
}
ins1(c, A_PUSHQ, areg(D_AX));
/* hi (default base length) → push */
if (hi) cgexpr(c, hi, locals);
else if (bu && bu->kind == TY_ARRAY)
cgexpr_int(c, (long long)bu->alen);
else if (base->kind == N_IDENT && bu &&
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
int boff = localfind(locals, base->str);
int isglobal = (boff == 0) &&
let_islet(base->str);
if (isglobal) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_CX));
ins2(c, A_MOVQ,
amem(D_CX, 8), areg(D_AX));
} else {
ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX));
}
} else {
cgexpr_int(c, 0);
}
ins1(c, A_PUSHQ, areg(D_AX));
/* lo (default 0) → AX */
if (lo) cgexpr(c, lo, locals);
else cgexpr_int(c, 0);
ins1(c, A_POPQ, areg(D_BX)); /* hi */
ins1(c, A_POPQ, areg(D_CX)); /* base */
/* len = hi - lo (DX) */
ins2(c, A_MOVQ, areg(D_BX), areg(D_DX));
ins2(c, A_SUBQ, areg(D_AX), areg(D_DX));
/* ptr = base + lo*esz (#76; ensure.ha:30
* membsz-unit). BX=lo*esz; AX=lo PRESERVED
* for cap. BX (dead hi) reloaded by cap below. */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_BX));
ins2(c, A_ADDQ, areg(D_BX), areg(D_CX));
} else {
ins2(c, A_ADDQ, areg(D_AX), areg(D_CX));
}
/* push cap, len, ptr (top). cap = base_cap - lo
* (#20); AX=lo, BX free. */
if (cg_base_cap(c, base, bu, locals, D_BX)) {
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
ins1(c, A_PUSHQ, areg(D_BX)); /* cap */
} else {
ins1(c, A_PUSHQ, areg(D_DX)); /* cap = len */
}
ins1(c, A_PUSHQ, areg(D_DX)); /* len */
ins1(c, A_PUSHQ, areg(D_CX)); /* ptr */
continue;
}
if (!widen[i] && node_isstructarg(args[i]) && args[i]->kind == N_IDENT) {
/* load qword(s) directly from the struct's slot */
int off = localfind(locals, args[i]->str);
int sz = struct_arg_size(args[i]->type);
if (sz > 8) {
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
continue;
}
if (!widen[i] && node_istaggedarg(args[i]) && args[i]->kind == N_IDENT) {
/* Tagged-union: push each 8B word from the slot.
* High word goes first so the popper drains them
* in low→high order into the arg-register class. */
int off = localfind(locals, args[i]->str);
int sz = tagged_arg_size(args[i]->type);
int nwords = sz / 8;
for (int k = nwords - 1; k >= 0; k--) {
ins2(c, A_MOVQ, amem(D_BP, off + k*8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
continue;
}
/* #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: materialise the source's 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 multi-word drain. */
if (!widen[i] && node_isaggarg(args[i])
&& !(node_isstructarg(args[i])
&& args[i]->kind == N_IDENT)) {
int aggsz = aggarg_size(args[i]->type);
int nwords = (aggsz + 7) / 8;
/* A float-bearing ≤16B struct from a non-ident
* source would need the #165 SSE eightbyte
* transport the GP push/drain here can't model —
* loud-stop rather than silently GP-pass it (a
* ≤16B struct with any float field; the wwstage
* tinfo mirror uses the same predicate). */
{
Type *st = type_chase_named(args[i]->type);
if (st && st->kind == TY_STRUCT
&& st->size <= 16) {
int f32;
for (Tfield *f = st->fields; f;
f = f->next)
if (fld_isfloat(f->type,
&f32))
fatal("#271/#165: "
"float-bearing "
"struct arg from a "
"non-ident source "
"needs SSE eightbyte "
"transport (out of "
"scope)");
}
}
if (args[i]->kind == N_CALL) {
if (cg_sret_retsize(args[i]->type) > 0) {
if (cg_aggargscr == 0) {
cg_aggargscr =
local_alloc(c, &locals,
"@aggargscr", aggsz,
cg_frame);
cg_aggargscr_sz = aggsz;
} else if (aggsz >
cg_aggargscr_sz) {
fatal("cgcall: @aggargscr "
"cached sz %d, need %d "
"(#271 pinned offset "
"can't grow)",
cg_aggargscr_sz,
aggsz);
}
cg_sret_dest_off = cg_aggargscr;
cgexpr(c, args[i], locals);
cg_sret_dest_off = 0;
for (int k = nwords - 1; k >= 0;
k--) {
ins2(c, A_MOVQ,
amem(D_BP,
cg_aggargscr + k*8),
areg(D_AX));
ins1(c, A_PUSHQ,
areg(D_AX));
}
} else {
/* ≤24B: producer left AX=word0,
* DX=word1, CX=word2. Push
* high→low so the pop drains
* word0 first. */
int rr[3] = { D_AX, D_DX, D_CX };
cgexpr(c, args[i], locals);
for (int k = nwords - 1; k >= 0;
k--)
ins1(c, A_PUSHQ,
areg(rr[k]));
}
continue;
}
if (!aggarg_srcaddr(c, args[i], D_SI, locals))
fatal("#271: aggregate arg from "
"unsupported source kind %d",
args[i]->kind);
for (int k = nwords - 1; k >= 0; k--) {
ins2(c, A_MOVQ, amem(D_SI, k*8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
continue;
}
if (widen[i]) {
/* Concrete → tagged-union widening at the call
* site. Mirrors the let/assign/return widening:
* lay out the value in the parameter's slot
* shape, then push high→low so pop drains tag
* first.
*
* Branches by source shape:
* - nullable (sz==8): pointer IS the disc.
* - str: tag@+0, ptr@+8, len@+16.
* - struct ident: copy struct words then
* prepend tag, zero-pad to slot size.
* - struct literal: materialise via a stack
* scratch slot — store each field at its
* struct-relative offset (with the +8 tag
* shift), zero-fill, then push from slot.
* - tagged source: load src slot words, remap
* the tag word via cg_widen_tag_remap, pad
* to wider dst slot, push.
* - scalar: tag@+0, value@+8, optional pad. */
cg_widen_tagged_push(c, &locals, widen_param[i],
args[i], widen_sz[i]);
continue;
}
cgexpr(c, args[i], locals);
Type *tuparg_push = node_tuplearg(args[i]);
/* #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. */
if (tuparg_push == NULL) {
Type *targ = type_chase_named(args[i]->type);
if (targ && targ->kind == TY_TUPLE)
fatal("#32: tuple arg from unsupported "
"source shape %d (call/ident/"
"literal/unwrap only; rule 7)",
args[i]->kind);
}
if (node_isfloat(args[i])) {
/* f32 spills 4B (MOVSS), f64 8B (MOVSD): the SysV
* float class drives the width per ref/qbe
* amd64/emit.c:524 (slot-copy single→movss). The
* slot is 8B either way; the pop reads the same
* width back. #143. */
int fmov = op_for(args[i], A_MOVSD, A_MOVSS);
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, fmov, areg(D_X0), amem(D_SP, 0));
} else if (node_isstr(args[i])) {
/* str IS []u8: cgexpr left (AX=ptr, BX=len,
* CX=cap). Push the triple, same as slice
* (#1/Phase 3). */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
} else if (node_isslice(args[i])) {
/* Slice-typed arg without a fast path above
* (e.g. `s: []u8` cast): cgexpr left
* (AX=ptr, BX=len, CX=cap). Push the triple. */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
} else if (node_istaggedarg(args[i])) {
/* Tagged-return ABI: AX=tag, DX=val0,
* CX=val1, R8=val2. Push high-to-low so pop
* drains tag first (into arg-reg[0]), then
* values into arg-reg[1..]. Nullable (sz=8):
* AX holds the pointer, no value-word
* registers — push just AX. */
/* #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. */
if (args[i]->kind == N_CALL
&& cg_sret_retsize(args[i]->type) > 0)
fatal("#38b: >32B tagged call result "
"as a call argument unwired "
"(#40-family follow-up)");
/* #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. */
if (cg_tagged_memread(args[i])) {
int msz = tagged_arg_size(
args[i]->type);
for (int k = msz - 8; k >= 0; k -= 8) {
ins2(c, A_MOVQ, amem(D_AX, k),
areg(D_DX));
ins1(c, A_PUSHQ, areg(D_DX));
}
} else {
int sz = tagged_arg_size(
args[i]->type);
if (sz > 24)
ins1(c, A_PUSHQ, areg(D_R8));
if (sz > 16)
ins1(c, A_PUSHQ, areg(D_CX));
if (sz > 8)
ins1(c, A_PUSHQ, areg(D_DX));
ins1(c, A_PUSHQ, areg(D_AX));
}
} else if (tuparg_push) {
/* #163: tuple ARG (param twin of #164's return).
* cgexpr above left the tuple in the return-ABI
* cursor; restage it into @tupargscr by SysV class
* (tuple_store, the #164 helper), then push the slot
* words high→low so the pop drains slot+0 first into
* the ARG cursor. The frame slot decouples the
* return-class regs (AX/DX/CX/R8 + X0/X1) from the
* overlapping arg-class regs (DI/SI/.. + X0..X7). */
int gpcur = 0, ssecur = 0, eoff = 0, ef32;
int gptot = 0, sstot = 0, tsz = 0;
for (Tparam *p = tuparg_push->params; p; p = p->next) {
/* 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). Loud
* until a consumer motivates wiring. */
Type *cu = type_chase_named(p->type);
if (cu && (cu->kind == TY_TUPLE
|| cu->kind == TY_STRUCT
|| cu->kind == TY_ARRAY
|| cu->kind == TY_TAGGED))
fatal("#32: tuple arg element "
"kind unsupported (nested "
"tuple/struct/array/tagged; "
"rule 7)");
if (fld_isfloat(p->type, &ef32))
sstot++;
else
gptot += tuple_eslot(p->type) / 8;
/* slot stride per element (sum == tuple slot
* size); matches the wwstage slotsize() walk so
* the @tupargscr width + reverse-push count agree
* byte-for-byte. */
tsz += tuple_eslot(p->type);
}
/* The producing call already satisfied #164's
* return caps; guard anyway (tuple_store indexes
* tuple_rseq[4] / tuple_sse_seq[2]). */
if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP)
fatal("tuple arg exceeds return-cursor ABI "
"capacity; see #163/#164");
if (cg_tupargscr == 0) {
cg_tupargscr = local_alloc(c, &locals,
"@tupargscr", tsz, cg_frame);
cg_tupargscr_sz = tsz;
} else if (tsz > cg_tupargscr_sz) {
fatal("cgcall: @tupargscr cached sz %d, "
"need %d (pinned offset can't grow; "
"#163)", cg_tupargscr_sz, tsz);
}
for (Tparam *p = tuparg_push->params; p; p = p->next) {
int isflt = fld_isfloat(p->type, &ef32);
tuple_store(c, p->type, gpcur, ssecur,
cg_tupargscr + eoff);
if (isflt)
ssecur++;
else
gpcur += tuple_eslot(p->type) / 8;
eoff += tuple_eslot(p->type);
}
for (int w = tsz - 8; w >= 0; w -= 8) {
ins2(c, A_MOVQ,
amem(D_BP, cg_tupargscr + w),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
} else {
ins1(c, A_PUSHQ, areg(D_AX));
}
}
/* sret discipline (#23): callee returns plain TY_STRUCT
* > 24B. Reserve RDI for the hidden dest-pointer arg by
* starting the int-arg cursor at 1 and emit the LEAQ AFTER
* the pop loop (so the pops don't clobber RDI). The dest
* slot is either the receiver's own slot (cg_sret_dest_off,
* propagated from N_LET / N_ASSIGN ident receive) or a
* per-fn @sretscr discard slot. Sized at the receive site
* or here for discards.
*
* Stack alignment is unaffected because pushargsrev/pops
* left RDI free — we never popped a user arg into it. */
int sret_call_sz = 0;
int sret_call_off = 0;
const char *sret_dest_sym = NULL; /* #220 */
{
Type *ret = (cu && cu->kind == TY_FN)
? cu->ret : NULL;
sret_call_sz = cg_sret_retsize(ret);
}
if (sret_call_sz > 0 && cg_sret_dest_sym != NULL) {
/* #220: GLOBAL dest — RDI gets LEAQ name(SB) below; no
* @sretscr slot needed (the callee writes the struct
* straight into g's storage). */
sret_dest_sym = cg_sret_dest_sym;
cg_sret_dest_sym = NULL;
} else if (sret_call_sz > 0) {
/* @sretscr is only needed when the result is dropped
* (no `let x = f();` receiver wired the call's dest into
* cg_sret_dest_off). Allocate first-use per #15/#26c
* size-strategy convergence — wwstage's scanlocals pre-
* pass that used to reserve this slot unconditionally is
* gone; cstage matches by skipping the allocation when a
* dest is already wired. fatal() on a later sret CALL
* needing a bigger slot (rule 7 — pinned offset can't
* grow in place). */
if (cg_sret_dest_off != 0) {
sret_call_off = cg_sret_dest_off;
cg_sret_dest_off = 0;
} else {
if (cg_sretscr_off == 0) {
cg_sretscr_off = local_alloc(c,
&locals, "@sretscr",
sret_call_sz, cg_frame);
cg_sretscr_sz = sret_call_sz;
} else if (sret_call_sz > cg_sretscr_sz) {
fatal("cgcall: @sretscr cached sz "
"%d, need %d (per-fn slot growth "
"post-#15 — pinned offset can't "
"grow in place)",
cg_sretscr_sz, sret_call_sz);
}
sret_call_off = cg_sretscr_off;
}
}
/* pop forward into the right register class. Args that
* don't fit in regs stay on the stack and are reached by
* the callee via positive offsets from BP. The caller is
* responsible for cleaning them up after CALL. */
int ii = (sret_call_sz > 0) ? 1 : 0, fi = 0, stackslots = 0;
Type *tu;
for (int i = 0; i < argcount; i++) {
if (memarg[i]) /* #38b: stays on the stack */
continue;
if (widen[i]) {
/* Pop widened tagged slot into arg-register
* class — sized by the parameter's tagged slot,
* not the arg's static type. */
int eb = widen_sz[i] / 8;
for (int k = 0; k < eb; k++) {
if (ii < 6)
ins1(c, A_POPQ,
areg(sysv_argregs[ii++]));
else
stackslots++;
}
continue;
}
if (node_isfloat(args[i])) {
if (fi < 8) {
/* Reload the spilled f32/f64 at its class
* width — MOVSS for f32, MOVSD for f64 —
* matching the push above (#143). */
int fmov = op_for(args[i], A_MOVSD,
A_MOVSS);
ins2(c, fmov, amem(D_SP, 0),
areg(sysv_fargregs[fi]));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
fi++;
} else {
stackslots++; /* leave on stack */
}
} else if (node_isstr(args[i])) {
/* str IS []u8: 3-word arg, same as slice (#1/Phase 3). */
for (int k = 0; k < 3; k++) {
if (ii < 6)
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
else
stackslots++;
}
} else if (node_isslice(args[i])) {
for (int k = 0; k < 3; k++) {
if (ii < 6)
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
else
stackslots++;
}
} else if (node_isstructarg(args[i])) {
int sclass[2], snb;
/* SSE-drain only for an ident arg: the struct push
* stages raw slot words for an N_IDENT only (non-
* ident struct args are a pre-existing >8B-push gap,
* out of scope). Gating here keeps cstage byte-id
* with wwstage, whose type lookup is ident-keyed. */
if (args[i]->kind == N_IDENT
&& (snb = struct_float_class(args[i]->type,
sclass)) > 0) {
/* #165: float-bearing struct arg — drain by
* SysV eightbyte class: a lone-f64 eightbyte
* MOVSD off (SP) into the next XMM (X0..X7), a
* pure-INT eightbyte POPQ into the next INTEGER
* arg reg (DI/SI/..). The struct-ident push
* staged raw words (class-independent); only the
* drain differs. Gated to qualifying floats;
* all-int + f32-packed keep the all-GP pop
* below. Reg overflow loud-stops (rule 7), the
* partial-spill stitch out of scope (#163 twin). */
for (int e = 0; e < snb; e++) {
if (sclass[e]) {
if (fi >= 8)
fatal("float struct arg "
"eightbyte overflows SSE "
"arg regs (X0..X7); stitch "
"out of scope, see #165");
ins2(c, A_MOVSD,
amem(D_SP, 0),
areg(sysv_fargregs[fi]));
ins2(c, A_ADDQ, aimm(8),
areg(D_SP));
fi++;
} else {
if (ii >= 6)
fatal("float struct arg "
"eightbyte overflows "
"integer arg regs (DI/SI/"
"DX/CX/R8/R9); stitch out "
"of scope, see #165");
ins1(c, A_POPQ,
areg(sysv_argregs[ii++]));
}
}
} else {
int sz = struct_arg_size(args[i]->type);
int eb = (sz > 8) ? 2 : 1;
for (int k = 0; k < eb; k++) {
if (ii < 6)
ins1(c, A_POPQ,
areg(sysv_argregs[ii++]));
else
stackslots++;
}
}
} else if (node_isaggarg(args[i])
&& !node_isstructarg(args[i])) {
/* #271: array / >16B-struct aggregate arg —
* drain its ceil(sz/8) staged words into the
* INTEGER arg cursor (overflow spills to the
* stack, reached by the callee via positive BP
* offsets). The ≤16B struct case stays in
* node_isstructarg above (SSE class path
* intact). */
int aggsz = aggarg_size(args[i]->type);
int nw = (aggsz + 7) / 8;
for (int k = 0; k < nw; k++) {
if (ii < 6)
ins1(c, A_POPQ,
areg(sysv_argregs[ii++]));
else
stackslots++;
}
} else if (node_istaggedarg(args[i])) {
int sz = tagged_arg_size(args[i]->type);
int eb = sz / 8;
for (int k = 0; k < eb; k++) {
if (ii < 6)
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
else
stackslots++;
}
} else if ((tu = node_tuplearg(args[i])) != NULL) {
/* #163: drain the tuple's staged words (pushed
* slot+0 first) into the SysV arg cursor by SysV
* class — a float MOVSD/MOVSS off (SP) into the
* next XMM (X0..X7), everything else POPQ into the
* next INTEGER arg reg (DI/SI/..); a slice/str its
* 3-word {ptr,len,cap}. Reg overflow loud-stops
* (rule 7): the partial-spill stitch is out of
* scope (twin of #164's cap). */
int ef32;
for (Tparam *p = tu->params; p; p = p->next) {
if (fld_isfloat(p->type, &ef32)) {
if (fi >= 8)
fatal("tuple arg float "
"element overflows SSE "
"arg regs (X0..X7); "
"stitch out of scope, "
"see #163");
ins2(c, ef32 ? A_MOVSS : A_MOVSD,
amem(D_SP, 0),
areg(sysv_fargregs[fi]));
ins2(c, A_ADDQ, aimm(8),
areg(D_SP));
fi++;
continue;
}
int eb = tuple_eslot(p->type) / 8;
if (ii + eb > 6)
fatal("tuple arg element "
"overflows integer arg regs "
"(DI/SI/DX/CX/R8/R9); stitch "
"out of scope, see #163");
for (int k = 0; k < eb; k++)
ins1(c, A_POPQ,
areg(sysv_argregs[ii++]));
}
} else {
if (ii < 6) {
ins1(c, A_POPQ, areg(sysv_argregs[ii]));
ii++;
} else {
stackslots++;
}
}
}
/* #38b: MEMORY-class args and register-overflow spill words
* cannot coexist — the callee's positive-BP cursor walks
* params in declaration order, but the caller's residual
* region puts spilled register-class words below every mem
* copy. Loud-stop (rule 7); the callee prologue holds the
* mirror check. The merged count feeds the caller-cleanup
* ADDQ after CALL. */
if (memslots > 0 && stackslots > 0)
fatal("#38b: >48B tagged arg mixed with register-"
"overflow stack args unwired");
stackslots += memslots;
/* sret hidden first-arg (#23): load &dest into RDI AFTER
* all user-arg pops have finished — the pop loop started
* its int-arg cursor at 1, so RDI was never written.
*
* Forwarding (task #9 follow-up): when outer's `return f();`
* forwards through an sret callee, source RDI from outer's
* saved @sretarg — inner writes directly into outer's
* caller-prealloc dest. No temporary in outer's frame.
* Post-#15 @sretscr is skipped entirely on the forwarding
* branch (no allocation, no frame growth) — earlier scan-
* lockstep reservation is gone. */
if (sret_call_sz > 0) {
if (cg_sret_forward) {
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_DI));
cg_sret_forward = 0;
} else if (sret_dest_sym != NULL) {
/* #220: sret into a GLOBAL — RDI = &g(SB). */
ins2(c, A_LEAQ, masym(c, sret_dest_sym),
areg(D_DI));
} else {
ins2(c, A_LEAQ,
amem(D_BP, sret_call_off),
areg(D_DI));
}
}
/* SysV: variadic callees require AL to hold the count of
* XMM regs used in the variable portion. We don't pass
* floats yet, so AL=0 covers every case we emit. */
if (cu && cu->kind == TY_FN && cu->variadic)
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
if (n->lhs->kind == N_IDENT) {
/* If the callee names a local variable holding a
* function pointer, load it and call indirect. Without
* this check `CALL fp(SB)` is emitted as if `fp` were
* a global symbol — the linker rightly fails. Hare /
* QBE handles this by treating any non-`$symbol` value
* as an indirect target; we get the same effect by
* reusing the cgexpr path. */
int loff = localfind(locals, n->lhs->str);
if (loff != 0) {
ins2(c, A_MOVQ,
amem(D_BP, loff), areg(D_AX));
ins1(c, A_CALL, areg(D_AX));
} else {
/* Bare `f()` — same-module by ww's resolver
* rules. Hint with c->cur_mod so the right
* fn wins when the leaf collides with another
* module's exported same-leaf fn. */
ins1(c, A_CALL,
mafn(c, n->lhs->str, c->cur_mod));
}
} else if (n->lhs->kind == N_DOT && n->lhs->lhs &&
n->lhs->lhs->kind == N_IDENT) {
/* `m.fn()` is module-qualified iff the ident has no
* concrete type (SK_USE leaves it ty_err). For a real
* type — typically a struct or *struct holding a
* function pointer — we load the field and indirect. */
Type *bt = n->lhs->lhs->type;
if (bt == NULL || bt == ty_err) {
/* `m.fn()` — explicit module qualifier. Pass
* the bareword as the hint so cross-module
* same-leaf exports resolve correctly. */
ins1(c, A_CALL,
mafn(c, n->lhs->str, n->lhs->lhs->str));
} else {
cgexpr(c, n->lhs, locals); /* AX = fn ptr */
ins1(c, A_CALL, areg(D_AX));
}
} else {
cgexpr(c, n->lhs, locals);
ins1(c, A_CALL, areg(D_AX));
}
/* SysV: caller cleans stack args. */
if (stackslots > 0)
ins2(c, A_ADDQ, aimm(stackslots * 8), areg(D_SP));
/* str IS []u8: callee returns AX=ptr, BX=len, CX=cap —
* same as a slice, no receive-side shuffle (#1/Phase 3). */
break;
}
case N_MATCH: {
/* match on a tagged-union scrutinee. Read tag and value from
* the slot. Dispatch by the resolved variant index of each
* case's type pattern — case order is independent of variant
* declaration order. A case with no pattern (`case =>`) is a
* default arm; its body always runs.
*
* Slot layout: [+0]=tag, [+8]=value0, [+16]=value1. The third
* word is only meaningful for variants whose payload is >8B
* (e.g. str). Bindings sized 16B (str) copy two words.
*
* Nullable folded `(*T | void)`: slot is one 8B word holding
* the pointer; null IS the void variant. Discriminator =
* value, not a separate tag. */
/* Family C (#35): identity-cast peel — see the `is` twin. */
Node *s = cg_tagged_idcastpeel(n->lhs);
Type *st = s ? s->type : NULL;
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
int is_nullable = type_isnullable(st);
int slot_size = (su && su->kind == TY_TAGGED) ? (int)su->size : 16;
int sl_off;
if (s->kind == N_IDENT) {
sl_off = localfind(locals, s->str);
} else if (s->kind == N_DOT && s->lhs && s->lhs->kind == N_IDENT
&& s->lhs->type) {
/* `match (p.field)` — point sl_off at the field's slot
* inside the parent struct. The slot layout (tag at +0,
* value words at +8/+16) is contiguous within the struct,
* so no spill is needed. */
Type *bt = s->lhs->type;
Type *bu = type_chase_named(bt);
Tfield *f = NULL;
if (bu && bu->kind == TY_STRUCT) {
for (Tfield *fl = bu->fields; fl; fl = fl->next) {
if (strcmp(fl->name, s->str) == 0) {
f = fl; break;
}
}
}
if (f) {
int boff = localfind(locals, s->lhs->str);
sl_off = boff + (int)f->offset;
} else {
/* fall back to spill — `match (h.e)` where
* h is *struct. cgexpr → cgdot now leaves the
* AX=tag, DX=val0, CX=val1[, R8=val2] shape
* (task #28), so spill all words the variant
* may carry. Pre-#28 only AX landed and the
* dispatch fired on a stale slot. */
sl_off = localoff(c, &locals, "@match_spill",
slot_size, cg_frame);
if (cg_tagged_memread(s)) {
/* #37: >32B box — cgexpr left its
* ADDRESS in AX; copy the whole box
* from memory (the cursor can't
* carry it). */
cgexpr(c, s, locals);
for (int k = 0; k < slot_size; k += 8) {
ins2(c, A_MOVQ, amem(D_AX, k),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + k));
}
} else {
cgexpr(c, s, locals);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, sl_off + 0));
if (!is_nullable) {
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + 8));
if (slot_size > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, sl_off + 16));
if (slot_size > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, sl_off + 24));
}
}
}
} else {
/* Spill non-ident scrutinees (e.g. `match (foo()?)`) into
* a scratch slot so we can index out the tag/value. The
* call ABI for tagged returns is AX=tag, DX=value0,
* CX=value1, R8=value2 — copy each word into the slot.
* Nullable returns are single-word: AX is the pointer;
* spill only that. */
sl_off = localoff(c, &locals, "@match_spill", slot_size,
cg_frame);
if (s->kind == N_CALL && cg_sret_retsize(st) > 0) {
/* #38b: sret-classified tagged call — pass the
* scrut slot itself as the sret dest and skip
* the cursor spill; downstream tag dispatch /
* case-let binds already read the slot from
* memory. */
cg_sret_dest_off = sl_off;
cgexpr(c, s, locals);
cg_sret_dest_off = 0;
} else if (cg_tagged_memread(s)) {
/* #37: >32B box read (insts[pc], t.N) —
* cgexpr left its ADDRESS in AX; copy the
* whole box from memory. */
cgexpr(c, s, locals);
for (int k = 0; k < slot_size; k += 8) {
ins2(c, A_MOVQ, amem(D_AX, k),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + k));
}
} else {
/* #37 (rule 7): a >32B box from a kind with no
* mem-read convention (cast, ...) would spill the
* cursor it never filled — loud, not garbage. */
if (!is_nullable && slot_size > TUPLE_GPCAP * 8)
fatal("#37: >32B tagged match scrutinee from "
"a non-mem-based source (kind %d) unwired "
"(rule 7)", s->kind);
/* Family C catch-all (rule 7): a widening tagged
* cast scrutinee has no cursor — loud. */
if (s->kind == N_CAST && !is_nullable
&& su && su->kind == TY_TAGGED)
fatal("#35: tagged cast source shape unwired "
"at match (rule 7)");
cgexpr(c, s, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
if (!is_nullable) {
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + 8));
if (slot_size > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, sl_off + 16));
if (slot_size > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, sl_off + 24));
}
}
}
char *end = mklabel(c, "match_end");
/* Push the end label as the yield target for arm bodies. */
if (nyields < YIELD_MAX) {
yield_target[nyields++] = end;
}
for (Node *cs = n->list; cs; cs = cs->next) {
char *next = mklabel(c, "match_next");
/* Per-arm scope: save the locals head, restore it
* after the body runs. Mirrors check.c's saved/restore
* around cstmt — the case bind (and any lets inside
* the arm) shouldn't leak past the arm, where a
* matching outer name would otherwise resolve to the
* shadow instead of the original. */
Local *arm_locals_saved = locals;
if (cs->type != NULL) {
int tag = cg_tag_for_variant(su, cs->type);
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
if (is_nullable) {
/* discriminator = pointer-vs-null.
* *T variant: skip if ptr == 0.
* void variant: skip if ptr != 0. */
int ptr_tag = nullable_ptr_tag(su);
int want_ptr = (tag == ptr_tag);
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
if (want_ptr)
ins1(c, A_JE, abranch(next));
else
ins1(c, A_JNE, abranch(next));
} else if (cs->list != NULL) {
/* Multi-pattern `case T1 | T2 | ... =>`:
* if the tag matches any of the alts,
* jump to body; otherwise to the next
* case. */
char *body = mklabel(c, "match_body");
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag),
areg(D_AX));
ins1(c, A_JE, abranch(body));
for (Node *alt = cs->list; alt;
alt = alt->next) {
int atag = cg_tag_for_variant(
su, alt->type);
ins2(c, A_CMPQ,
aimm(atag < 0 ? 0 : atag),
areg(D_AX));
ins1(c, A_JE, abranch(body));
}
ins1(c, A_JMP, abranch(next));
label(c, body);
} else {
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag),
areg(D_AX));
ins1(c, A_JNE, abranch(next));
}
}
if (cs->str && cs->str[0] && cs->type) {
Type *bt = cs->type;
Type *bu = (bt && bt->kind == TY_NAMED)
? bt->under : bt;
if (is_nullable) {
/* Bind *T or void to a local. The
* value IS the slot's pointer word; no
* payload to copy. void binding is
* unusable (size 0), so only emit for
* the *T variant. local_alloc (not
* localoff): the bind must NEVER reuse
* an outer same-named slot. */
if (bu && bu->kind == TY_PTR) {
int voff = local_alloc(c, &locals,
cs->str, 8, cg_frame);
ins2(c, A_MOVQ,
amem(D_BP, sl_off + 0),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, voff));
}
} else {
/* #43: route through Type.size SSoT rather
* than re-asserting 16/24 for str/slice. */
int bsz = 8;
if (bu) bsz = (int)bu->size;
if (bsz <= 0) bsz = 8;
/* local_alloc to dodge name-collision
* dedup — a 16B str bind shadowing an
* 8B outer would otherwise overflow
* into the saved BP. */
int voff = local_alloc(c, &locals, cs->str,
bsz, cg_frame);
int nwords = (bsz + 7) / 8;
for (int w = 0; w < nwords; w++) {
ins2(c, A_MOVQ,
amem(D_BP, sl_off + 8 + 8*w),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, voff + 8*w));
}
}
}
cgstmt(c, cs->body, &locals, cg_frame);
locals = arm_locals_saved;
ins1(c, A_JMP, abranch(end));
label(c, next);
}
label(c, end);
if (nyields > 0) nyields--;
break;
}
case N_TRYPROP: {
/* Evaluate tagged value: AX=tag, DX=value0[, CX=value1].
* If the tag matches an error variant, propagate as the
* current function's return (with a tag remap to the
* enclosing fn's variant order). On success, unwrap to the
* success-variant ABI: ≤8B values in AX; str values in
* (AX=ptr, BX=len).
*
* Nullable: AX is the pointer; *T variant is the success
* (any non-null), void variant is the error (null). The
* enclosing fn's null encoding is the same — RET with AX=0
* if propagating; otherwise leave AX as-is on success. */
/* #38b residuals (rule 7): the cursor read below cannot see
* an sret-classified call result (AX = dest pointer), and the
* propagate-RET below cannot speak an sret-classified
* enclosing return (the caller reads memory, not the
* cursor). Both are unwired follow-ups of #40's family. */
if (n->lhs && n->lhs->kind == N_CALL
&& cg_sret_retsize(n->lhs->type) > 0)
fatal("#38b: `?` on an sret-class call result "
"unwired (mem-based unwrap is a #40-family "
"follow-up)");
if (cg_sret_retsize(cg_ret_type) > 0)
fatal("#38b: `?` propagation into a >32B tagged "
"return unwired (sret error-propagate is a "
"#40-family follow-up)");
Type *u = n->lhs ? n->lhs->type : NULL;
if (u && u->kind == TY_NAMED) u = u->under;
/* Family C (#35/#46): non-call sources don't fill the
* AX/DX/CX/R8 cursor the unwrap below reads — an IDENT
* loads it from its slot, a mem-based read (deref at any
* size, >32B INDEX/DOT) from the box address cgexpr left
* in AX. Both were silent word0 unwraps pre-#35. >32B
* stays loud (the cursor can't carry it; #40 family). */
if (u && u->kind == TY_TAGGED && !u->nullable
&& (int)u->size > TUPLE_GPCAP * 8
&& n->lhs->kind != N_CALL)
fatal("#37: `?` on a >32B mem-based tagged read "
"unwired (#40-family follow-up)");
if (n->lhs && n->lhs->kind == N_IDENT
&& u && u->kind == TY_TAGGED && !u->nullable) {
int boff = localfind(locals, n->lhs->str);
int bsz = (int)u->size;
/* rule 7: a module-level tagged `g?` has no frame
* slot; the global cursor load is unwired. */
if (boff == 0)
fatal("#35: `?` on a global tagged ident "
"unwired (rule 7)");
if (bsz > 24)
ins2(c, A_MOVQ, amem(D_BP, boff + 24),
areg(D_R8));
if (bsz > 16)
ins2(c, A_MOVQ, amem(D_BP, boff + 16),
areg(D_CX));
if (bsz > 8)
ins2(c, A_MOVQ, amem(D_BP, boff + 8),
areg(D_DX));
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
} else if (cg_tagged_memread(n->lhs)) {
int bsz = (int)type_chase_named(n->lhs->type)->size;
cgexpr(c, n->lhs, locals);
if (bsz > 24)
ins2(c, A_MOVQ, amem(D_AX, 24), areg(D_R8));
if (bsz > 16)
ins2(c, A_MOVQ, amem(D_AX, 16), areg(D_CX));
if (bsz > 8)
ins2(c, A_MOVQ, amem(D_AX, 8), areg(D_DX));
ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX));
} else
cgexpr(c, n->lhs, locals);
Type *r = cg_ret_type;
if (r && r->kind == TY_NAMED) r = r->under;
if (u && u->kind == TY_TAGGED && u->nullable) {
char *cont = mklabel(c, "tryprop_ok");
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JNE, abranch(cont));
/* null = error: propagate. AX already 0; matches
* the enclosing nullable encoding if it has one. */
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
label(c, cont);
break;
}
int s_tag = cg_tagged_success_tag(u);
Type *succ_t = NULL;
if (u && u->kind == TY_TAGGED) {
int i = 0;
for (Tparam *p = u->params; p; p = p->next, i++)
if (i == s_tag) { succ_t = p->type; break; }
}
int success_is_str = type_isstr(succ_t);
char *cont = mklabel(c, "tryprop_ok");
ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX));
ins1(c, A_JE, abranch(cont));
if (u && r && r->kind == TY_TAGGED && u->params) {
/* Same-shape unions remap every variant to itself, so
* the loop emits no JMPs. Skip propret entirely then —
* wwstage doesn't emit a dead label either (CLAUDE.md
* rule 10, task #18). */
char *propret = NULL;
int i = 0;
for (Tparam *p = u->params; p; p = p->next, i++) {
if (!cg_variant_is_error(u, i)) continue;
int j = cg_tag_for_variant(r, p->type);
if (j < 0) j = 0;
if (j == i) continue;
char *skip = mklabel(c, "tryprop_skip");
ins2(c, A_CMPQ, aimm(i), areg(D_AX));
ins1(c, A_JNE, abranch(skip));
ins2(c, A_MOVQ, aimm(j), areg(D_AX));
if (propret == NULL)
propret = mklabel(c, "tryprop_ret");
ins1(c, A_JMP, abranch(propret));
label(c, skip);
}
if (propret != NULL)
label(c, propret);
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
label(c, cont);
{
/* #241: a tuple success payload is an rvalue tuple — fill
* the cursor (shift past the tag) so the destructure /
* let consumer reads every element, not just word0. */
Type *stu = type_chase_named(succ_t);
if (stu && stu->kind == TY_TUPLE) {
cg_tagged_tuple_payload_shift(c, stu);
break;
}
/* Family C (#35, unwrap source): a TAGGED success
* variant is a NESTED box (ww keeps nested unions
* un-flattened) riding the payload words intact —
* shift past the outer tag so consumers see the
* standard AX=tag cursor. The scalar MOVQ DX,AX
* below carried only the inner tag and dropped the
* payload (ken unw16). Nullable folds to one word
* and stays on the scalar move. */
if (stu && stu->kind == TY_TAGGED && !stu->nullable) {
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
if (stu->size > 8)
ins2(c, A_MOVQ, areg(D_CX),
areg(D_DX));
if (stu->size > 16)
ins2(c, A_MOVQ, areg(D_R8),
areg(D_CX));
break;
}
}
if (success_is_str) {
/* str IS []u8: success value arrives in the tagged
* ABI as DX=ptr, CX=len, R8=cap (slot 32B). Move len
* out before cap overwrites CX (#1/Phase 3). */
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_R8), areg(D_CX));
}
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
}
case N_TRYUNW: {
/* On error variant: exit(1) directly via the syscall.
* Nullable: null = error; non-null = success (AX is the
* pointer, ready to use). */
/* #38b residual (rule 7): see the N_TRYPROP twin. */
if (n->lhs && n->lhs->kind == N_CALL
&& cg_sret_retsize(n->lhs->type) > 0)
fatal("#38b: `!` on an sret-class call result "
"unwired (mem-based unwrap is a #40-family "
"follow-up)");
Type *u = n->lhs ? n->lhs->type : NULL;
if (u && u->kind == TY_NAMED) u = u->under;
/* Family C (#35/#46): see the N_TRYPROP twin — ident loads
* the cursor from its slot, a mem-based read from the box
* address; >32B non-call stays loud (#40 family). */
if (u && u->kind == TY_TAGGED && !u->nullable
&& (int)u->size > TUPLE_GPCAP * 8
&& n->lhs->kind != N_CALL)
fatal("#37: `!` on a >32B mem-based tagged read "
"unwired (#40-family follow-up)");
if (n->lhs && n->lhs->kind == N_IDENT
&& u && u->kind == TY_TAGGED && !u->nullable) {
int boff = localfind(locals, n->lhs->str);
int bsz = (int)u->size;
if (boff == 0)
fatal("#35: `!` on a global tagged ident "
"unwired (rule 7)");
if (bsz > 24)
ins2(c, A_MOVQ, amem(D_BP, boff + 24),
areg(D_R8));
if (bsz > 16)
ins2(c, A_MOVQ, amem(D_BP, boff + 16),
areg(D_CX));
if (bsz > 8)
ins2(c, A_MOVQ, amem(D_BP, boff + 8),
areg(D_DX));
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
} else if (cg_tagged_memread(n->lhs)) {
int bsz = (int)type_chase_named(n->lhs->type)->size;
cgexpr(c, n->lhs, locals);
if (bsz > 24)
ins2(c, A_MOVQ, amem(D_AX, 24), areg(D_R8));
if (bsz > 16)
ins2(c, A_MOVQ, amem(D_AX, 16), areg(D_CX));
if (bsz > 8)
ins2(c, A_MOVQ, amem(D_AX, 8), areg(D_DX));
ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX));
} else
cgexpr(c, n->lhs, locals);
if (u && u->kind == TY_TAGGED && u->nullable) {
char *cont = mklabel(c, "tryunw_ok");
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JNE, abranch(cont));
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
ins0(c, A_SYSCALL);
label(c, cont);
break;
}
int s_tag = cg_tagged_success_tag(u);
Type *succ_t = NULL;
if (u && u->kind == TY_TAGGED) {
int i = 0;
for (Tparam *p = u->params; p; p = p->next, i++)
if (i == s_tag) { succ_t = p->type; break; }
}
int success_is_str = type_isstr(succ_t);
char *cont = mklabel(c, "tryunw_ok");
ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX));
ins1(c, A_JE, abranch(cont));
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
ins0(c, A_SYSCALL);
label(c, cont);
{
/* #241: tuple success payload fills the cursor (shift past
* the tag) — same rvalue-tuple-into-cursor story. */
Type *stu = type_chase_named(succ_t);
if (stu && stu->kind == TY_TUPLE) {
cg_tagged_tuple_payload_shift(c, stu);
break;
}
/* Family C (#35): TAGGED success = nested box on the
* payload words — shift past the outer tag (see the
* N_TRYPROP twin). */
if (stu && stu->kind == TY_TAGGED && !stu->nullable) {
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
if (stu->size > 8)
ins2(c, A_MOVQ, areg(D_CX),
areg(D_DX));
if (stu->size > 16)
ins2(c, A_MOVQ, areg(D_R8),
areg(D_CX));
break;
}
}
if (success_is_str) {
/* str IS []u8: success arrives DX=ptr, CX=len, R8=cap
* (slot 32B). Move len out before cap clobbers CX
* (#1/Phase 3). */
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_R8), areg(D_CX));
}
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
}
case N_TYPETEST: {
/* `e is T` — Compare scrutinee tag against T's variant index.
* Result is bool (0/1) in AX. Nullable: discriminator is
* pointer-vs-null, not a tag. */
/* #38b residual (rule 7): an sret-class call result leaves
* AX = dest pointer, not the tag — mem-based test is a
* #40-family follow-up. */
if (n->lhs && n->lhs->kind == N_CALL
&& cg_sret_retsize(n->lhs->type) > 0)
fatal("#38b: `is` on an sret-class call result "
"unwired (#40-family follow-up)");
/* Family C (#35): identity casts are transport no-ops —
* peel so the ident emission carries; a WIDENING tagged
* cast renumbers the tag the compare below keys on and
* has no wired source arm — loud, not a mis-keyed test. */
Node *tl = cg_tagged_idcastpeel(n->lhs);
{
Type *tcu = tl ? type_chase_named(tl->type) : NULL;
if (tl && tl->kind == N_CAST && tcu
&& tcu->kind == TY_TAGGED && !tcu->nullable)
fatal("#35: tagged cast source shape unwired "
"at `is` (rule 7)");
}
cgexpr(c, tl, locals);
/* #37: a mem-based box read (>32B INDEX/DOT, any-size
* deref) leaves its ADDRESS in AX — load the tag word
* from memory before the compare. */
if (cg_tagged_memread(tl))
ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX));
Type *u = tl ? tl->type : NULL;
if (u && u->kind == TY_NAMED) u = u->under;
Type *vt = n->rhs ? n->rhs->type : NULL;
char *ne = mklabel(c, "is_ne");
char *done = mklabel(c, "is_done");
if (u && u->kind == TY_TAGGED && u->nullable) {
int tag = cg_tag_for_variant(u, vt);
int ptr_tag = nullable_ptr_tag(u);
int want_ptr = (tag == ptr_tag);
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
if (want_ptr)
ins1(c, A_JE, abranch(ne));
else
ins1(c, A_JNE, abranch(ne));
} else {
int tag = cg_tag_for_variant(u, vt);
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
ins1(c, A_JNE, abranch(ne));
}
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
ins1(c, A_JMP, abranch(done));
label(c, ne);
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
label(c, done);
break;
}
case N_TYPEASSERT: {
/* `e as T` — abort if tag != T's variant index; otherwise
* unwrap value to T's ABI: scalar/ptr variants land in AX;
* 16B str variants in AX:BX.
*
* We need both tag *and* value words. For an N_IDENT local
* the value lives at slot+8/+16 — cgexpr's single-MOVQ path
* does not load it. Mirror match's pattern: resolve a slot
* offset (existing local or a fresh @asrt_spill) and index
* out tag/value from memory.
*
* Nullable: the slot's word IS the pointer. *T variant
* asserts non-null; void variant asserts null. The value
* left in AX after the check is the pointer itself. */
/* Family C (#35): identity-cast peel — see the `is` twin. */
Node *s = cg_tagged_idcastpeel(n->lhs);
Type *st = s ? s->type : NULL;
Type *u = (st && st->kind == TY_NAMED) ? st->under : st;
Type *vt = n->type;
/* Enum ↔ integer: reinterpret-only. The value already lives
* in AX after evaluating the LHS; no tag/unwrap needed. */
{
Type *vu = (vt && vt->kind == TY_NAMED) ? vt->under : vt;
if ((u && u->kind == TY_ENUM) ||
(vu && vu->kind == TY_ENUM)) {
cgexpr(c, s, locals);
break;
}
}
int slot_size = (u && u->kind == TY_TAGGED) ? (int)u->size : 16;
/* #38b residual (rule 7): the @asrt_spill below reads the
* cursor, which an sret-class call result never fills. */
if (s && s->kind == N_CALL && cg_sret_retsize(st) > 0)
fatal("#38b: `as` on an sret-class call result "
"unwired (#40-family follow-up)");
int sl_off = 0;
if (s && s->kind == N_IDENT && s->str) {
sl_off = localfind(locals, s->str);
}
if (sl_off == 0) {
sl_off = localoff(c, &locals, "@asrt_spill",
slot_size, cg_frame);
if (cg_tagged_memread(s)) {
/* #37: >32B box read — ADDRESS in AX; copy
* the whole box from memory. */
cgexpr(c, s, locals);
for (int k = 0; k < slot_size; k += 8) {
ins2(c, A_MOVQ, amem(D_AX, k),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + k));
}
} else {
/* #37 (rule 7): >32B from a non-mem-based kind
* would spill an unfilled cursor. */
if (!(u && u->kind == TY_TAGGED && u->nullable)
&& slot_size > TUPLE_GPCAP * 8)
fatal("#37: `as` on a >32B tagged value from "
"a non-mem-based source (kind %d) unwired "
"(rule 7)", s->kind);
/* Family C catch-all (rule 7): a widening tagged
* cast source has no cursor — loud. */
if (s->kind == N_CAST && u && u->kind == TY_TAGGED
&& !u->nullable)
fatal("#35: tagged cast source shape unwired "
"at `as` (rule 7)");
cgexpr(c, s, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
if (!(u && u->kind == TY_TAGGED && u->nullable)) {
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, sl_off + 8));
if (slot_size > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, sl_off + 16));
}
}
}
char *ok = mklabel(c, "asrt_ok");
if (u && u->kind == TY_TAGGED && u->nullable) {
int tag = cg_tag_for_variant(u, vt);
int ptr_tag = nullable_ptr_tag(u);
int want_ptr = (tag == ptr_tag);
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
if (want_ptr)
ins1(c, A_JNE, abranch(ok));
else
ins1(c, A_JE, abranch(ok));
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
ins0(c, A_SYSCALL);
label(c, ok);
/* AX already holds the pointer (or 0 for void
* variant, where the result type has size 0 and
* no consumer reads it). */
break;
}
int tag = cg_tag_for_variant(u, vt);
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
ins1(c, A_JE, abranch(ok));
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
ins0(c, A_SYSCALL);
label(c, ok);
ins2(c, A_MOVQ, amem(D_BP, sl_off + 8), areg(D_AX));
if (type_isstr(vt))
ins2(c, A_MOVQ, amem(D_BP, sl_off + 16), areg(D_BX));
break;
}
case N_CAST: {
int from_f = node_isfloat(n->lhs);
int to_f = cg_isfloat(n->type);
int from_f32 = node_isf32(n->lhs);
int to_f32 = type_isf32(n->type);
cgexpr(c, n->lhs, locals); /* AX or X0 depending */
if (from_f && !to_f) {
int op = from_f32 ? A_CVTTSS2SI : A_CVTTSD2SI;
ins2(c, op, areg(D_X0), areg(D_AX));
} else if (!from_f && to_f) {
int op = to_f32 ? A_CVTSI2SS : A_CVTSI2SD;
ins2(c, op, areg(D_AX), areg(D_X0));
} else if (from_f && to_f && from_f32 != to_f32) {
int op = to_f32 ? A_CVTSD2SS : A_CVTSS2SD;
ins2(c, op, areg(D_X0), areg(D_X0));
}
/* str → []u8 (or any []T): cgexpr left (AX=ptr, BX=len).
* Slice register convention is (AX=ptr, BX=len, CX=cap);
* synthesise cap = len so downstream arg-push / let-init
* paths see the canonical triple. Without this, the cap
* register stays whatever cgexpr happened to leave there
* and the receiver reads a stale value. */
{
Type *tt = n->type;
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
Type *ft = n->lhs ? n->lhs->type : NULL;
Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft;
if (tu && tu->kind == TY_SLICE
&& fu && fu->kind == TY_STR) {
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
}
}
/* Narrowing integer cast: clamp AX to the target width so
* downstream 64-bit ops see a value within the declared
* range. Hare semantics: `expr: T` truncates to T's bit
* width (mod 2^n). Without this, `(big_u64): u32` left the
* upper 32 bits intact and CMPQ/DIVQ misread the value.
*
* Unsigned targets use MOVL/ANDQ to clear the high bits.
* Signed-narrow targets (i8/i16/i32) sign-extend via
* MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates;
* this is what lets `(0xFF80i64): i8` compare equal to
* -128i64 after a widening read-back. Symmetric on signed
* vs unsigned: both branches gate on `type_isint(tu) &&
* size<8`, then dispatch on type_isunsigned(tu). The
* recursion through TY_ENUM in type_isunsigned (task #5)
* is what lets an enum-aliased narrow (`type myflag = i8`)
* pick up the right MOVS*Q. Wwstage's cgcast keys off the
* resolved type-name through the same shape. TY_RUNE is
* unsigned (Unicode scalar) and lands on the MOVL path. */
if (!from_f && !to_f && n->type) {
Type *tt = n->type;
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
/* Identity-width identity-sign cast is a no-op at the
* machine-int level: src and dst share both width and
* signedness, so the natural slot/load already carries
* the right canonical 64-bit shape and the narrow-clamp
* is dead. Replaces b5632b1's single-site `!dst_is_enum`
* gate (task #25) which mirrored wwstage's N_TENUM
* lacuna; the lacuna is fixed there too, so this gate
* stays symmetric across both stages (#33). Source side
* uses `castsrcprim` (a structural walk matching
* wwstage's exprprimresolved exactly), NOT n->lhs->type
* — cstage's checker has richer type info than wwstage
* can derive without a checker, and the asymmetric
* coverage broke 995_self_rebuild's byte-id. The cost
* is that some casts (`.len: i32`, N_BIN result, call
* return, match-bound payload) still emit a redundant
* clamp on both stages; closing those gaps is a
* sibling task that extends wwstage's type inference.
* Incidentally fixes a silent miscompile #25's
* dst-kind-only skip left in place: u32→enum-u8 (and
* similar narrow-to-enum casts) was suppressing the
* clamp, so the upper bits of the source value leaked
* through register-chained downstream uses. Caveat:
* removing the defensive MOVL exposes any upstream
* cgen path that leaves garbage in upper RAX when
* producing a sub-word value — the contract is
* producers leave the value in canonical width-
* extended form. */
int src_w = 0, src_unsignd = 0;
castsrcprim(n->lhs, &src_w, &src_unsignd);
int dst_w = (tu && type_isint(tu)) ? (int)tu->size : 0;
int identity = dst_w > 0 && src_w == dst_w
&& src_unsignd == type_isunsigned(tu);
if (tu && type_isint(tu) && tu->size > 0
&& tu->size < 8 && !identity) {
if (type_isunsigned(tu)) {
if (tu->size == 4) {
ins2(c, A_MOVL,
areg(D_AX), areg(D_AX));
} else {
u64 mask = ((u64)1 << (tu->size * 8)) - 1;
ins2(c, A_ANDQ,
aimm((i64)mask),
areg(D_AX));
}
} else {
int op = A_MOVSXD;
if (tu->size == 1) op = A_MOVSBQ;
else if (tu->size == 2) op = A_MOVSWQ;
ins2(c, op, areg(D_AX), areg(D_AX));
}
}
/* TY_BOOL is size 1 too; clamp to a single byte so
* `(u32_val): bool` produces 0 or a low-byte value
* instead of leaking the upper bits. type_isint(bool)
* is false, so the symmetric narrow above misses it
* — this dedicated branch covers the bool case. */
if (tu && tu->kind == TY_BOOL) {
ins2(c, A_ANDQ, aimm(0xFF), areg(D_AX));
}
}
break;
}
case N_DOT: {
/* slice/str pseudo-fields: .ptr (offset 0), .len (8), .cap (16).
* Arrays don't carry a header; .len uses the static size and
* .ptr is the address of the first element. */
/* `(*p).f` read retarget: parser produces n->lhs = N_UN(STAR,
* IDENT(p)) with type T (post-deref struct). Pull the inner
* IDENT in as dot_lhs so bt resolves to *T and the pointer-
* auto-deref branch below fires (mirror of the N_ASSIGN
* N_DOT lhs retarget). v1 scope: N_IDENT inner only;
* (*expr).f follow-up task pending. Branches that gate on
* `n->lhs->kind == N_DOT/N_INDEX/...` keep checking the raw
* n->lhs since (*p) isn't either of those shapes. */
Node *dot_lhs = n->lhs;
if (dot_lhs && dot_lhs->kind == N_UN && dot_lhs->op == TK_STAR
&& dot_lhs->lhs && dot_lhs->lhs->kind == N_IDENT)
dot_lhs = dot_lhs->lhs;
Type *bt = dot_lhs ? dot_lhs->type : NULL;
/* type_chase_named (#22): `type b = a; type a = struct;` stacks
* two TY_NAMED layers; single peel left `u` still TY_NAMED,
* missing the TY_STRUCT field-walk gate below and collapsing
* `s.field` to a base-only MOVQ read (offset 0 instead of
* the field's declared offset). */
Type *u = type_chase_named(bt);
/* Module-qualified value reference: `mod.name`. The checker
* leaves SK_USE idents untyped (NULL/ty_err); detect that and
* look up the leaf in the flat (driver-concatenated) sym/def
* maps the same way a bare N_IDENT would. */
if (n->lhs && n->lhs->kind == N_IDENT
&& (bt == NULL || bt == ty_err)) {
Type *t = n->type;
Type *tu = (t && t->kind == TY_NAMED) ? t->under : t;
if (tu && tu->kind == TY_FN) {
/* `mod.fn` address-of via N_DOT — pass the
* module bareword as the disambiguation hint. */
ins2(c, A_LEAQ,
mafn(c, n->str, n->lhs->str), areg(D_AX));
break;
}
{
/* Same-module-first walk using n->lhs->str as
* the explicit module hint (sister of wwstage
* deflookuprhsmod). The TY_FN branch above
* already uses n->lhs->str via mafn for the
* cross-module qualifier disambiguation; this
* walk mirrors that polarity so `alpha.MSG`
* from a third module beats a head-of-sdefs
* beta.MSG collision (#11, sister of #4c). */
Sdef *s;
for (s = sdefs; s; s = s->next) {
if (strcmp(s->name, n->str) != 0)
continue;
if (sdef_mod_match_hint(s, n->lhs->str))
break;
}
if (s == NULL) {
for (s = sdefs; s; s = s->next)
if (strcmp(s->name, n->str) == 0)
break;
}
if (s != NULL) {
const char *lab = intern_strlit(c,
s->bytes, s->len);
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
ins2(c, A_MOVQ,
aimm((long long)s->len),
areg(D_BX));
goto dot_done;
}
}
/* Same gating as the bare-ident catch-all: lets route
* through localloadop (their slot can be the target of
* a narrow deref-store via `&letname: *iN`); defs and
* unresolved symbols stay on MOVQ so wwstage's defent-
* registry-without-tnode shape agrees byte-for-byte. */
int mqop = let_islet(n->str)
? localloadop(n->type) : A_MOVQ;
/* #229: thread the DOTTED module (the `m` in `m.x`) into
* the value mangle, not cur_mod — masym's non-preferring
* leaf lookup mis-mangled `aa.v` onto another module's
* same-leaf global (read the WRONG global). The TY_FN
* branch above already uses n->lhs->str via mafn. */
if (mqop == A_MOVQ) {
ins2(c, A_MOVQ,
mahint(c, n->str, n->lhs->str), areg(D_AX));
} else {
ins2(c, A_LEAQ,
mahint(c, n->str, n->lhs->str), areg(D_CX));
ins2(c, mqop, amem(D_CX, 0), areg(D_AX));
}
goto dot_done;
}
/* Chained N_DOT spine through value-struct fields. Handles any
* depth `root.f0.f1.…leaf` where every intermediate field is a
* value struct, plus the slice/str pseudo-field tail (`s.buf.len`)
* where the innermost field is a slice/str header. Walks inward
* collecting (parent_struct, field_name); reverses to sum field
* offsets; emits one load at (base + total_off). Placed BEFORE
* the slice/str pseudo-field branch so its else-arm (cgexpr lhs
* + shuffle BX→AX) doesn't mis-handle `b.buf.len` — cgexpr on a
* value-struct→slice chain only loads .ptr into AX, leaving BX
* stale. Sibling of the pointer-chain branch further down. */
if (n->lhs && n->lhs->kind == N_DOT) {
Type *lt0 = n->lhs->type;
/* Transitive chase (#71) — the READ twin of the chained-dot
* store walk; an alias-typed hop skipped this arm onto the
* ADDQ-per-hop address spine while wwstage folds the offsets. */
Type *lu0 = type_chase_named(lt0);
int leaf_is_pseudo = lu0 && n->str
&& (lu0->kind == TY_SLICE || lu0->kind == TY_STR)
&& (strcmp(n->str, "ptr") == 0
|| strcmp(n->str, "len") == 0
|| strcmp(n->str, "cap") == 0);
int leaf_in_struct = lu0 && lu0->kind == TY_STRUCT;
if (leaf_is_pseudo || leaf_in_struct) {
struct { Type *pu; const char *name; } steps[16];
int nsteps = 0;
int ptr_root = 0;
Node *cur = n;
int abort = 0;
while (cur && cur->kind == N_DOT && cur->lhs) {
Type *pt = cur->lhs->type;
Type *pu = type_chase_named(pt);
if (!pu) { abort = 1; break; }
if (cur == n && (pu->kind == TY_SLICE
|| pu->kind == TY_STR)) {
/* leaf pseudo on slice/str header */
} else if (pu->kind == TY_STRUCT) {
/* value-struct hop */
} else if (pu->kind == TY_PTR && pu->sub
&& cur->lhs->kind == N_IDENT) {
/* `*T` root: dereference once at emit
* time, then walk offsets through the
* pointee. Only at the last hop (root
* is a bare ident) — `*T`-field mid-
* chain keeps its cgexpr-based pointer-
* field branch further down. */
Type *sub = type_chase_named(pu->sub);
if (sub && sub->kind == TY_STRUCT) {
pu = sub;
ptr_root = 1;
} else {
abort = 1;
break;
}
} else {
abort = 1;
break;
}
if (nsteps >= 16) { abort = 1; break; }
steps[nsteps].pu = pu;
steps[nsteps].name = cur->str;
nsteps++;
cur = cur->lhs;
}
if (!abort && cur && cur->kind == N_IDENT
&& nsteps > 0) {
int total_off = 0;
Type *leaf_type = NULL;
int slice_delta = -1;
int ok = 1;
for (int i = nsteps - 1; i >= 0; i--) {
Type *pu = steps[i].pu;
if (pu->kind == TY_SLICE
|| pu->kind == TY_STR) {
if (strcmp(steps[i].name, "ptr") == 0)
slice_delta = 0;
else if (strcmp(steps[i].name, "len") == 0)
slice_delta = 8;
else if (strcmp(steps[i].name, "cap") == 0)
slice_delta = 16;
else { ok = 0; break; }
} else {
Tfield *f = NULL;
for (Tfield *fl = pu->fields; fl; fl = fl->next)
if (strcmp(fl->name, steps[i].name) == 0)
{ f = fl; break; }
if (!f) { ok = 0; break; }
total_off += (int)f->offset;
leaf_type = f->type;
}
}
if (ok) {
int root_off = localfind(locals, cur->str);
int base_reg = D_BP;
int base_disp = root_off;
int root_resolved = (root_off != 0);
/* #129 A.2: struct-typed defs (def_isstructdef)
* now have DATA storage and need the same
* LEAQ-and-offset shape as struct lets. */
if (!root_resolved && (let_islet(cur->str)
|| def_isstructdef(cur->str))) {
ins2(c, A_LEAQ,
masym(c, cur->str), areg(D_CX));
base_reg = D_CX;
base_disp = 0;
root_resolved = 1;
}
if (root_resolved && ptr_root) {
/* `*T` root: load the pointer value
* once; field accesses then index at
* total_off off the pointer. */
if (base_reg == D_BP) {
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_CX));
} else {
ins2(c, A_MOVQ,
amem(D_CX, 0), areg(D_CX));
}
base_reg = D_CX;
base_disp = 0;
}
if (root_resolved) {
if (slice_delta >= 0) {
ins2(c, A_MOVQ,
amem(base_reg,
base_disp + total_off + slice_delta),
areg(D_AX));
goto dot_done;
}
/* Transitive chase (#5-F1 fold):
* read twin of the store-walk
* leaf gate (reviewer-F1 r2b
* clobber probe). */
Type *fu = type_chase_named(leaf_type);
/* tagged leaf (#38a): load the box into
* the tagged cursor (AX=tag, DX=val0,
* R8=val2 before CX=val1 — base_reg may
* be CX), the single-dot tagged-field arm
* verbatim. Pre-#38a the fldloadop tail
* pulled ONE word (the tag): is-tests
* passed by tag-luck while as/match/let
* consumers read stale payload registers
* (ken x5c: o.r.min as size added DX).
* >32B box: ADDRESS in AX (the #37
* cg_tagged_memread convention). */
if (fu && fu->kind == TY_TAGGED) {
int fo = base_disp + total_off;
if ((int)fu->size
> TUPLE_GPCAP * 8) {
ins2(c, A_LEAQ,
amem(base_reg, fo),
areg(D_AX));
goto dot_done;
}
ins2(c, A_MOVQ,
amem(base_reg, fo + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(base_reg, fo + 8),
areg(D_DX));
if (fu->size > 24)
ins2(c, A_MOVQ,
amem(base_reg, fo + 24),
areg(D_R8));
if (fu->size > 16)
ins2(c, A_MOVQ,
amem(base_reg, fo + 16),
areg(D_CX));
goto dot_done;
}
if (fu && fu->kind == TY_STR) {
ins2(c, A_MOVQ,
amem(base_reg,
base_disp + total_off + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(base_reg,
base_disp + total_off + 8),
areg(D_BX));
goto dot_done;
}
if (fu && fu->kind == TY_SLICE) {
/* Slice leaf: load all three header
* words into (AX=ptr, BX=len, CX=cap)
* so the value follows the canonical
* slice-rhs convention. base_reg may
* be CX (global / `*T` root); load
* .cap LAST so the base survives the
* earlier reads. */
ins2(c, A_MOVQ,
amem(base_reg,
base_disp + total_off + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(base_reg,
base_disp + total_off + 8),
areg(D_BX));
ins2(c, A_MOVQ,
amem(base_reg,
base_disp + total_off + 16),
areg(D_CX));
goto dot_done;
}
int g_isf32 = 0;
if (fld_isfloat(leaf_type, &g_isf32)) {
int mov = g_isf32 ? A_MOVSS : A_MOVSD;
ins2(c, mov,
amem(base_reg,
base_disp + total_off),
areg(D_X0));
goto dot_done;
}
int fsz = (int)(leaf_type
? leaf_type->size : 8);
int op = fldloadop(leaf_type, fsz);
ins2(c, op,
amem(base_reg,
base_disp + total_off),
areg(D_AX));
goto dot_done;
}
}
}
}
}
int lenfld = (n->str && strcmp(n->str, "len") == 0);
int capfld = (n->str && strcmp(n->str, "cap") == 0);
int ptrfld = (n->str && strcmp(n->str, "ptr") == 0);
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
&& (lenfld || capfld || ptrfld)) {
if (n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
if (off == 0) {
/* Not a local — could be `def NAME: str
* = "lit"`. Sdef-backed strs aren't laid
* out in memory; emit .ptr/.len from the
* literal directly, mirroring the bare
* N_IDENT branch above. Without this we'd
* load BP+8 (return-address slot) as the
* "len". */
{
/* Same-module-first walk: two
* same-leaf `def MSG: str = ...`
* across modules would otherwise
* fold the wrong strlit's length /
* label into `MSG.len` / `MSG.ptr`
* (sister of wwstage deflookuprhs
* #4c). */
Sdef *s;
for (s = sdefs; s; s = s->next) {
if (strcmp(s->name,
n->lhs->str) != 0)
continue;
if (sdef_mod_match(c, s))
break;
}
if (s == NULL) {
for (s = sdefs; s;
s = s->next)
if (strcmp(s->name,
n->lhs->str)
== 0)
break;
}
if (s != NULL) {
if (ptrfld) {
const char *lab =
intern_strlit(c,
s->bytes,
s->len);
ins2(c, A_LEAQ,
asym(lab),
areg(D_AX));
} else {
ins2(c, A_MOVQ,
aimm((long long)
s->len),
areg(D_AX));
}
goto dot_done;
}
}
/* Top-level str/slice `let` — load
* the field through &name(SB). Same
* pattern as the bare N_IDENT load. */
if (let_islet(n->lhs->str)) {
int delta = ptrfld ? 0
: (lenfld ? 8 : 16);
ins2(c, A_LEAQ,
masym(c, n->lhs->str),
areg(D_CX));
ins2(c, A_MOVQ,
amem(D_CX, delta),
areg(D_AX));
goto dot_done;
}
}
int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
ins2(c, A_MOVQ, amem(D_BP, off + delta),
areg(D_AX));
} else {
/* Evaluate the str/slice expression — leaves
* the full (AX=ptr, BX=len, CX=cap) header
* (cgslicehdr) for an indexed element / non-ident
* base. .ptr returns AX, .len shuffles BX→AX,
* .cap shuffles CX→AX. The .cap shuffle is the
* #13 read-fix (sibling of the #20 store): pre-fix
* the else-arm handled only .len, so `t[i].cap`
* fell through returning AX=.ptr. */
cgexpr(c, n->lhs, locals);
if (lenfld)
ins2(c, A_MOVQ, areg(D_BX),
areg(D_AX));
else if (capfld)
ins2(c, A_MOVQ, areg(D_CX),
areg(D_AX));
}
break;
}
if (u && u->kind == TY_ARRAY && n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
if (lenfld) {
ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX));
break;
}
if (ptrfld) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
break;
}
}
/* tuple positional field access: t.0, t.1, ...
* C-t0/#22: slot stride (tuple_eslot — str/slice header,
* tagged box, 8B floor) — the layout every cursor transport
* site writes and the checker's TY_TUPLE size counts. The
* load below keeps the element's NATURAL width (fldloadop). */
if (u && u->kind == TY_TUPLE && n->lhs->kind == N_IDENT && n->str) {
int idx = 0;
for (const char *q = n->str; *q; q++) idx = idx * 10 + (*q - '0');
Tparam *tp = u->params;
int foff = 0;
while (idx > 0 && tp) {
foff += tuple_eslot(tp->type);
tp = tp->next;
idx--;
}
if (tp != NULL) {
int fsz = (int)(tp->type ? tp->type->size : 8);
Type *fu = type_chase_named(tp->type);
int op = fldloadop(tp->type, fsz);
int off = localfind(locals, n->lhs->str);
/* C-t3 (#48): GLOBAL tuple base — LEAQ the
* mangled symbol into CX and read at CX+foff,
* the struct-field global pattern below.
* Pre-C-t3 localfind's 0 silently read the
* stack frame. */
int base_reg = D_BP;
int base_disp = off;
if (off == 0 && let_islet(n->lhs->str)) {
ins2(c, A_LEAQ,
masym(c, n->lhs->str),
areg(D_CX));
base_reg = D_CX;
base_disp = 0;
}
/* f64/f32 tuple field must ride X0 via MOVSD/MOVSS;
* the integer fldloadop left it in AX (#103 FACE Z).
* Mirrors the struct-field float load at cgen.c:1462,
* 1838 (the #96 pattern). */
int tup_isf32 = 0;
if (fld_isfloat(tp->type, &tup_isf32)) {
int mov = tup_isf32 ? A_MOVSS : A_MOVSD;
ins2(c, mov,
amem(base_reg, base_disp + foff),
areg(D_X0));
break;
}
/* str IS []u8 — load (ptr, len, cap) into
* (AX, BX, CX), the canonical slice-header ABI,
* so chains like `t.1.len` propagate through the
* slice-rhs convention (#1/Phase 3 collapse).
* UNLIKE the field arms there is no slice-element
* sibling here, so the triple is hand-authored;
* base is BP (frame) or CX (global base — CX is
* written LAST so it survives the +0/+8 reads). */
if (fu && fu->kind == TY_STR) {
ins2(c, A_MOVQ,
amem(base_reg, base_disp + foff + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(base_reg, base_disp + foff + 8),
areg(D_BX));
ins2(c, A_MOVQ,
amem(base_reg, base_disp + foff + 16),
areg(D_CX));
break;
}
/* #22a: tagged element — load the box into the
* tagged value regs (AX=tag, DX/CX/R8=payload),
* the same cursor the is/as spill + match read.
* A CX-based global never co-occurs (tagged
* element inits are loud at the DATA emit), so
* the ascending walk can't clobber the base. */
if (fu && fu->kind == TY_TAGGED) {
int eslot = tuple_eslot(tp->type);
/* #37: a >32B box overruns the 4-reg
* cursor — leave its ADDRESS in AX
* (cg_tagged_memread, the sret-receive
* convention); consumers copy from
* memory. Replaces the #22b loud
* bound (pre-bound: cstage indexed
* past tuple_rseq = invalid asm,
* wwstage clamped to R8 = silent
* payload drop). */
if (eslot > TUPLE_GPCAP * 8) {
ins2(c, A_LEAQ,
amem(base_reg,
base_disp + foff),
areg(D_AX));
break;
}
for (int k = 0; k < eslot / 8; k++)
ins2(c, A_MOVQ,
amem(base_reg,
base_disp + foff + k * 8),
areg(tuple_rseq[k]));
break;
}
ins2(c, op, amem(base_reg, base_disp + foff),
areg(D_AX));
}
break;
}
/* real struct field: load at struct_base + field_off.
* Base is either a local frame slot (off(BP)) or a top-
* level let global (&name(SB) into CX); we resolve which
* once and then share the field-walk code. */
if (u && u->kind == TY_STRUCT && n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
int is_global = 0;
int base_reg = D_BP;
int base_disp = off;
/* #129 A.2: struct-typed defs now also resolve via
* LEAQ name(SB) (paralleling lets). Pre-A.2 the
* `def_isstructdef` arm fell through to the default
* BP-relative path with off=0, emitting `MOV (BP),`
* which reads the stack frame's first slot instead
* of the def's data section. */
if (off == 0 && (let_islet(n->lhs->str)
|| def_isstructdef(n->lhs->str))) {
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX));
is_global = 1;
base_reg = D_CX;
base_disp = 0;
}
for (Tfield *f = u->fields; f; f = f->next) {
if (strcmp(f->name, n->str) != 0) continue;
/* tagged-union field: load AX=tag, DX=val0,
* CX=val1, R8=val2 (CX last, since for globals
* CX is also the base addr; load R8 before CX
* so the base address survives the +24 read).
* Mirrors the tagged-return ABI so the let-init
* / match dispatch shapes just work. The val2
* word fires for slice-variant tagged-unions
* (slot = 8 tag + 24 slice header = 32B). */
Type *tag_fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if (tag_fu && tag_fu->kind == TY_TAGGED) {
int fo = base_disp + (int)f->offset;
/* #37: >32B box — ADDRESS in AX (the
* cg_tagged_memread convention), not
* the cursor. */
if ((int)tag_fu->size
> TUPLE_GPCAP * 8) {
ins2(c, A_LEAQ,
amem(base_reg, fo),
areg(D_AX));
(void)is_global;
break;
}
ins2(c, A_MOVQ,
amem(base_reg, fo + 0), areg(D_AX));
ins2(c, A_MOVQ,
amem(base_reg, fo + 8), areg(D_DX));
if (tag_fu->size > 24)
ins2(c, A_MOVQ,
amem(base_reg, fo + 24),
areg(D_R8));
if (tag_fu->size > 16)
ins2(c, A_MOVQ,
amem(base_reg, fo + 16),
areg(D_CX));
(void)is_global;
break;
}
/* str IS []u8 — same 3-word {ptr,len,cap} as a slice
* field: load (ptr, len, cap) into (AX, BX, CX) so the
* value flows through the slice-rhs convention. str
* folds onto the slice arm (#1/Phase 3 collapse).
* base_reg may be CX for globals; load .cap LAST so
* the base survives the earlier reads. */
/* Transitive chase (#5-F1 fold): the slice half
* single-peeled while the str half (type_isstr)
* recursed — a 2-level alias slice field read
* loaded ptr only, len/cap rode stale registers
* (reviewer-F1 r1b clobber probe). */
Type *str_fu = type_chase_named(f->type);
if ((str_fu && str_fu->kind == TY_SLICE) ||
type_isstr(f->type)) {
ins2(c, A_MOVQ,
amem(base_reg, base_disp + (int)f->offset + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(base_reg, base_disp + (int)f->offset + 8),
areg(D_BX));
ins2(c, A_MOVQ,
amem(base_reg, base_disp + (int)f->offset + 16),
areg(D_CX));
break;
}
/* f64/f32 field: route through X0 (MOVSD/MOVSS).
* Loading via MOVQ AX would put the bits in the
* integer reg, and any downstream consumer that
* reads X0 (arg pass, return, arithmetic) would see
* stale data. */
int e_isf32 = 0;
if (fld_isfloat(f->type, &e_isf32)) {
int mov = e_isf32 ? A_MOVSS : A_MOVSD;
ins2(c, mov,
amem(base_reg, base_disp + (int)f->offset),
areg(D_X0));
break;
}
int fsz = (int)(f->type ? f->type->size : 8);
int op = fldloadop(f->type, fsz);
ins2(c, op,
amem(base_reg, base_disp + (int)f->offset),
areg(D_AX));
break;
}
break;
}
/* pointer-to-slice/str field: deref and read pseudo-field.
* Used by helpers like rt_appendu8(s: *[]u8, v: u8). dot_lhs
* gates the N_IDENT check so `(*p).len` (parser N_UN(STAR,
* IDENT)) emits the same load as `p.len` after the case-top
* retarget. */
if (u && u->kind == TY_PTR && u->sub) {
Type *inner = type_chase_named(u->sub);
if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR)
&& (lenfld || capfld || ptrfld)
&& dot_lhs && dot_lhs->kind == N_IDENT) {
int off = localfind(locals, dot_lhs->str);
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_AX));
break;
}
}
/* pointer-to-struct field: deref and load. Common pattern:
* fn move(p: *point) ... { p.x += dx; ... }
* dot_lhs gates this branch so both `p.f` (n->lhs is IDENT)
* and `(*p).f` (n->lhs is N_UN(STAR, IDENT), retargeted to
* inner IDENT at case-top) emit the same load sequence.
*
* type_chase_named (#22): `type b = a;` inside the pointer
* (`*b`) leaves a single peel still at TY_NAMED. Bites the
* strings.tokenize wrapper shape — caller signature
* `next_token(s: *strings.tokenizer)` where strings.tokenizer
* aliases bytes.tokenizer. */
if (u && u->kind == TY_PTR && u->sub) {
Type *inner = type_chase_named(u->sub);
if (inner && inner->kind == TY_STRUCT
&& dot_lhs && dot_lhs->kind == N_IDENT) {
int off = localfind(locals, dot_lhs->str);
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
for (Tfield *f = inner->fields; f; f = f->next) {
if (strcmp(f->name, n->str) != 0) continue;
/* tagged-union field through *struct: BX
* already holds the *struct pointer. Load
* the four payload regs from (BX, f->offset)
* — BX is not a target (AX/DX/CX/R8), so
* load order is harmless. Mirrors the direct-
* struct branch above so consumers see the
* same tagged-return register shape
* regardless of pointer rooting. Pre-#28 fell
* through to fldloadop and dropped the
* payload words. */
Type *ptag_fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if (ptag_fu && ptag_fu->kind == TY_TAGGED) {
int fo = (int)f->offset;
/* #37: >32B box — ADDRESS in
* AX, not the cursor. */
if ((int)ptag_fu->size
> TUPLE_GPCAP * 8) {
ins2(c, A_LEAQ,
amem(D_BX, fo),
areg(D_AX));
break;
}
ins2(c, A_MOVQ,
amem(D_BX, fo + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(D_BX, fo + 8),
areg(D_DX));
if (ptag_fu->size > 16)
ins2(c, A_MOVQ,
amem(D_BX, fo + 16),
areg(D_CX));
if (ptag_fu->size > 24)
ins2(c, A_MOVQ,
amem(D_BX, fo + 24),
areg(D_R8));
break;
}
/* str IS []u8 — same 3-word {ptr,len,cap} as a
* slice field through *struct: load (ptr, len,
* cap) into (AX, BX, CX). BX holds the *struct
* pointer, so load .len LAST — the earlier loads
* still index off the original base. str folds
* onto the slice arm (#1/Phase 3 collapse). */
/* Transitive chase (#5-F1 fold): via-ptr twin
* of the BP-base read gate (reviewer-F1 r3
* clobber probe). */
Type *str_fu = type_chase_named(f->type);
if ((str_fu && str_fu->kind == TY_SLICE) ||
type_isstr(f->type)) {
ins2(c, A_MOVQ,
amem(D_BX, (int)f->offset + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(D_BX, (int)f->offset + 16),
areg(D_CX));
ins2(c, A_MOVQ,
amem(D_BX, (int)f->offset + 8),
areg(D_BX));
break;
}
/* f64/f32 field via *struct: load into X0.
* BX already holds the struct pointer from
* the MOVQ amem(D_BP,off) above. */
int f_isf32 = 0;
if (fld_isfloat(f->type, &f_isf32)) {
int mov = f_isf32 ? A_MOVSS : A_MOVSD;
ins2(c, mov,
amem(D_BX, (int)f->offset),
areg(D_X0));
break;
}
int fsz = (int)(f->type ? f->type->size : 8);
int op = fldloadop(f->type, fsz);
ins2(c, op,
amem(D_BX, (int)f->offset),
areg(D_AX));
break;
}
break;
}
}
/* Chained N_DOT through a *struct field. cgexpr lhs leaves
* AX = the inner *struct pointer; load the requested field
* with a single MOVQ. Without this, returning `o.p.val`
* silently leaves AX = o.p (the pointer) and the outer
* cast/use sees the pointer instead of the dereferenced
* field. (Surfaced building ww-w6l.) */
if (n->lhs->kind == N_DOT) {
Type *lt = n->lhs->type;
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
if (lu && lu->kind == TY_PTR && lu->sub) {
Type *inner = lu->sub;
if (inner->kind == TY_NAMED) inner = inner->under;
if (inner && inner->kind == TY_STRUCT) {
for (Tfield *f = inner->fields; f; f = f->next) {
if (strcmp(f->name, n->str) != 0) continue;
cgexpr(c, n->lhs, locals); /* AX = inner ptr */
Type *ft = f->type;
Type *fu = type_chase_named(ft);
/* tagged leaf (#38a): AX holds the *struct
* base and the tagged cursor targets AX
* (tag) — stage the base in BX, then the
* cursor load (AX=tag, DX=val0, R8=val2,
* CX=val1; >32B → ADDRESS in AX, #37). The
* fldloadop tail pulled ONE word (ken b8:
* o.p.min read stale DX as payload). */
if (fu && fu->kind == TY_TAGGED) {
int fo = (int)f->offset;
ins2(c, A_MOVQ, areg(D_AX),
areg(D_BX));
if ((int)fu->size
> TUPLE_GPCAP * 8) {
ins2(c, A_LEAQ,
amem(D_BX, fo),
areg(D_AX));
goto dot_done;
}
ins2(c, A_MOVQ,
amem(D_BX, fo + 0),
areg(D_AX));
ins2(c, A_MOVQ,
amem(D_BX, fo + 8),
areg(D_DX));
if (fu->size > 24)
ins2(c, A_MOVQ,
amem(D_BX, fo + 24),
areg(D_R8));
if (fu->size > 16)
ins2(c, A_MOVQ,
amem(D_BX, fo + 16),
areg(D_CX));
goto dot_done;
}
/* str IS []u8 — same 3-word {ptr,len,cap} as a
* slice field: load (ptr, len, cap) into
* (AX, BX, CX). AX is the *struct base, so
* load .ptr (which targets AX) LAST. str folds
* onto the slice arm (#1/Phase 3 collapse). */
if ((fu && fu->kind == TY_SLICE) ||
type_isstr(ft)) {
ins2(c, A_MOVQ,
amem(D_AX, (int)f->offset + 8),
areg(D_BX));
ins2(c, A_MOVQ,
amem(D_AX, (int)f->offset + 16),
areg(D_CX));
ins2(c, A_MOVQ,
amem(D_AX, (int)f->offset + 0),
areg(D_AX));
goto dot_done;
}
/* f64/f32 chained field: read into X0. */
int g_isf32 = 0;
if (fld_isfloat(ft, &g_isf32)) {
int mov = g_isf32 ? A_MOVSS : A_MOVSD;
ins2(c, mov,
amem(D_AX, (int)f->offset),
areg(D_X0));
goto dot_done;
}
int fsz = (int)(ft ? ft->size : 8);
int op = fldloadop(ft, fsz);
ins2(c, op, amem(D_AX, (int)f->offset),
areg(D_AX));
goto dot_done;
}
}
}
}
/* `arr[i].field` — element-then-field through a `[N]*S` /
* `[N]S` (and slice/`*[N]S`) base. One branch covers both
* shapes: compute `&arr[i]` into BX, then either deref
* (`*Struct` element) or move-to-AX (value `Struct` element),
* so the leaf load is `(field.offset)(AX)` either way.
* Bypasses cgindex deliberately — cgindex's final MOVQ
* would truncate a value-struct element to 8 bytes. Mirrors
* selfhost/cmd/wcc/cgenexpr.ww's cgdot N_INDEX-lhs branch. */
if (n->lhs && n->lhs->kind == N_INDEX && n->lhs->lhs
&& n->lhs->lhs->kind == N_IDENT) {
Node *idxbase = n->lhs->lhs;
Type *elemt = n->lhs->type;
Type *elemu = (elemt && elemt->kind == TY_NAMED)
? elemt->under : elemt;
Type *struct_t = NULL;
int viaptr = 0;
if (elemu && elemu->kind == TY_PTR) {
Type *inner = elemu->sub;
if (inner && inner->kind == TY_NAMED)
inner = inner->under;
if (inner && inner->kind == TY_STRUCT) {
struct_t = inner;
viaptr = 1;
}
} else if (elemu && elemu->kind == TY_STRUCT) {
struct_t = elemu;
}
if (struct_t) {
Tfield *f = NULL;
for (Tfield *fl = struct_t->fields; fl; fl = fl->next)
if (strcmp(fl->name, n->str) == 0)
{ f = fl; break; }
Type *bt = idxbase->type;
Type *bu = (bt && bt->kind == TY_NAMED)
? bt->under : bt;
int is_arr = bu && bu->kind == TY_ARRAY;
int is_sl = bu && bu->kind == TY_SLICE;
int is_ptr = bu && bu->kind == TY_PTR;
int off = localfind(locals, idxbase->str);
/* #21 (READ twin of #11): a module-GLOBAL base
* makes localfind return 0, so the field-offset-
* aware branch was skipped and `g[i].field` fell to
* a generic index-load that drops f->offset (reads
* element[i] at offset 0). Resolve the global the
* same way the N_INDEX arm does (let_islet ||
* def_isarraydef) and dispatch the base load by
* shape: array -> LEAQ name(SB) (the symbol IS the
* storage), slice/ptr -> MOVQ name(SB) (the symbol's
* first word IS the .ptr). */
int isglobal = (off == 0)
&& (let_islet(idxbase->str)
|| def_isarraydef(idxbase->str));
if (f != NULL && (is_arr || is_sl || is_ptr)
&& (off != 0 || isglobal)) {
int esz = (int)elemt->size;
cgexpr(c, n->lhs->rhs, locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
}
if (isglobal && is_arr)
ins2(c, A_LEAQ,
masym(c, idxbase->str),
areg(D_BX));
else if (isglobal)
ins2(c, A_MOVQ,
masym(c, idxbase->str),
areg(D_BX));
else if (is_arr)
ins2(c, A_LEAQ,
amem(D_BP, off), areg(D_BX));
else
ins2(c, A_MOVQ,
amem(D_BP, off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
if (viaptr)
ins2(c, A_MOVQ,
amem(D_BX, 0), areg(D_AX));
else
ins2(c, A_MOVQ, areg(D_BX),
areg(D_AX));
int foff = (int)f->offset;
Type *ft = f->type;
Type *fu = type_chase_named(ft);
/* #270-1a: an `[N]T`-typed field of an
* array element (`a[i].m[j]`) — leave the
* field's ADDRESS, a base for the outer
* index, NEVER deref. AX holds &a[i]; the
* field address is &a[i]+foff. The #135
* read-side for `d.m[i]`, applied to an
* array-element base. Without this an array
* field fell to fldloadop below and loaded
* its first 8 bytes as a value → garbage
* base → SEGFAULT in the outer index. */
if (fu && fu->kind == TY_ARRAY) {
if (foff != 0)
ins2(c, A_ADDQ,
aimm(foff),
areg(D_AX));
goto dot_done;
}
if (fu && (fu->kind == TY_STR
|| fu->kind == TY_SLICE)) {
/* str/slice: the 3-word {ptr,len,cap}
* slice header (#1). AX holds the
* element base, so load .ptr (which
* targets AX) LAST. Matches the
* caseB *struct slice arm and
* cgslicehdr(D_AX). */
ins2(c, A_MOVQ,
amem(D_AX, foff + 8),
areg(D_BX));
ins2(c, A_MOVQ,
amem(D_AX, foff + 16),
areg(D_CX));
ins2(c, A_MOVQ,
amem(D_AX, foff + 0),
areg(D_AX));
goto dot_done;
}
int g_isf32 = 0;
if (fld_isfloat(ft, &g_isf32)) {
int mov = g_isf32
? A_MOVSS : A_MOVSD;
ins2(c, mov,
amem(D_AX, foff),
areg(D_X0));
goto dot_done;
}
int fsz = (int)(ft ? ft->size : 8);
int op = fldloadop(ft, fsz);
ins2(c, op, amem(D_AX, foff),
areg(D_AX));
goto dot_done;
}
}
}
/* Nested module-qualified field where the chain didn't fold to
* a known shape (typical when w6c runs on a single file with
* `use mod;` but no driver concatenation — the body's enum /
* struct hasn't been seen). Emit `MOVQ <leaf>(SB), AX` so the
* linker surfaces a clean undefined-symbol error on the leaf
* — mirrors the bare-N_IDENT unresolved fallback used by
* single-segment N_DOTs. Keeps cstage / wwstage byte-aligned
* on the cgen-match isolation probes. C2 (F4): gated to
* UNTYPED chains only — pre-C2 it swallowed every unmatched
* dot-over-dot chain, turning a TYPED depth-2 read behind an
* index/deref spine (`threads[0].cap.end`) into a silent
* global read of a colliding leaf symbol (p6min10 exit 66). */
if (n->lhs && n->lhs->kind == N_DOT && n->str
&& (n->lhs->type == NULL || n->lhs->type == ty_err)) {
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
break;
}
/* Non-ident / untyped-str base pseudo-field: e.g. `"abc".len`
* / `"abc".ptr`. A string literal is TY_UNTYPED_STR, not
* TY_STR, so it misses the typed slice/str gate above and
* lands here. cgexpr leaves (AX=ptr, BX=len); `.ptr` keeps AX,
* `.len` shuffles BX→AX. Mirrors wwstage cgdot's catch-all
* (selfhost/cmd/wcc/cgenexpr.ww). #14. C2 (F4/FA3): gated to
* TY_UNTYPED_STR — pre-C2 this was the offset- and header-
* blind catch-all every unmatched typed dot fell into, so a
* nonzero-offset field behind a deref-index spine read the
* element's word 0 (`(*p)[i].slicefield` → 1-word wrong-
* offset read; offset-0 scalars worked by COINCIDENCE). */
{
Type *cu = type_chase_named(
n->lhs ? n->lhs->type : NULL);
if (cu && cu->kind == TY_UNTYPED_STR) {
cgexpr(c, n->lhs, locals);
if (lenfld)
ins2(c, A_MOVQ, areg(D_BX),
areg(D_AX));
break;
}
}
/* C2 read-resolver (F4 + FA3-cstage): a TYPED N_DOT read no
* enumerated arm matched — depth-2+ chains and slice/str/
* scalar fields behind index/deref spines. Address via
* cgplaceaddr (the C1 resolver), leaf load emitted here by
* kind. Leaf kinds with no canonical register convention in
* expr position stay LOUD; any shape the resolver can't
* address dies LOUD (rule 7) — the pre-C2 tails guessed. */
{
Type *rt = n->type;
Type *ru = type_chase_named(rt);
if (ru && ru->kind == TY_TAGGED)
fatal("read-resolver: tagged field read not "
"wired (rule-7)");
if (ru && (ru->kind == TY_STRUCT
|| ru->kind == TY_TUPLE))
fatal("read-resolver: aggregate field read "
"not wired (rule-7)");
if (!cgplaceaddr(c, n, D_BX, locals))
fatal("unsupported field-read shape");
int rd_isf32 = 0;
if (fld_isfloat(rt, &rd_isf32)) {
ins2(c, rd_isf32 ? A_MOVSS : A_MOVSD,
amem(D_BX, 0), areg(D_X0));
goto dot_done;
}
if (ru && ru->kind == TY_ARRAY) {
/* `[N]T` leaf: leave the field ADDRESS — a
* base for an outer index, never a value
* (#270-1a semantics). */
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
goto dot_done;
}
if (ru && (ru->kind == TY_STR
|| ru->kind == TY_SLICE)) {
/* str IS []u8 — 3-word {ptr,len,cap} into
* (AX, BX, CX). BX is the place base, so
* load .len (which targets BX) LAST. */
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX));
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_BX));
goto dot_done;
}
int rdsz = (int)(rt ? rt->size : 8);
ins2(c, fldloadop(rt, rdsz), amem(D_BX, 0),
areg(D_AX));
}
dot_done:
break;
}
case N_INDEX: {
/* Scaled indexing for slice/array/str/ptr-to-T.
* Element size is 1 for u8/str, otherwise type's natural size.
* For `*[N]T` drill through to the array so esz/esub reflect
* T, not sizeof(array). */
Type *bt = n->lhs ? n->lhs->type : NULL;
/* #128b: module-qualified `mod.arr[i]` — n->lhs is N_DOT and
* its type is NULL (SK_USE-bound module ident). Look up the
* imported let's type via let_var_type so esz/esub reflect
* the imported array's element width instead of falling to
* the esz=1 default (→ MOVZBQ wrong-width load). Sister of
* the dst-side cg_dotbase_addr branch that emits LEAQ for
* the base address. */
if ((bt == NULL || bt == ty_err)
&& n->lhs && n->lhs->kind == N_DOT
&& n->lhs->str
&& let_islet(n->lhs->str)) {
bt = let_var_type(n->lhs->str);
}
Type *u = type_chase_named(bt);
Type *eff = idx_eff(bt);
int esz = 1;
if (eff && eff->sub) esz = (int)eff->sub->size;
Type *esub = eff ? eff->sub : NULL;
Type *esubu = (esub && esub->kind == TY_NAMED)
? esub->under : esub;
int elem_tagged = esubu && esubu->kind == TY_TAGGED;
if (n->lhs->kind == N_IDENT && u) {
int off = localfind(locals, n->lhs->str);
/* #129 A.3: array-typed defs now have DATA storage; the
* LEAQ name(SB) base-load must fire for them too, not
* just let_islet. Parallel to A.2's def_isstructdef
* gate at the N_DOT direct-struct-ident arm. */
int isglobal = (off == 0) && (let_islet(n->lhs->str)
|| def_isarraydef(n->lhs->str));
cgexpr(c, n->rhs, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
/* base address into BX. Top-level array → LEAQ
* name(SB); top-level ptr → MOVQ name(SB) (the symbol
* holds the pointer); locals route off BP. */
if (isglobal && u->kind == TY_ARRAY) {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_BX));
} else if (isglobal) {
ins2(c, A_MOVQ, masym(c, n->lhs->str),
areg(D_BX));
} else if (u->kind == TY_ARRAY) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX));
} else {
/* slice/str/ptr: ptr field is at off+0 */
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
}
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
/* #156 (PREREQ-1 read-half): element is itself an array
* ([N][M]T → element [M]T). This index yields the sub-
* array's ADDRESS, not a loaded value — the outer index
* adds its own offset and only the final scalar element
* dereferences. Sister of #135 (N_DOT-base-on-[N]T-field
* needs ADDRESS). BX holds base+idx*esz; move it to AX (the
* value-result reg). Gated on TY_ARRAY element so 1D arrays
* are byte-identical (no 2D consumer pre-#156). */
if (esubu && esubu->kind == TY_ARRAY) {
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
break;
}
/* str/slice element: load the full (ptr, len, cap) header
* into (AX, BX, CX) — both are 24B since #1, so the cap
* word must survive. Kind-gate on type_isstr||type_isslice,
* never size==24: a >16B struct is 24B+ too but takes the
* struct-copy path, not this 3-word header load (#10).
* Gate on esub (= idx_eff'd element, #61), not u->sub —
* for `*[N]str` u->sub is the ARRAY and the gate missed,
* falling to a 1-word load that dropped len/cap. esub ==
* u->sub for every non-ptr-to-array base. Base is BX. */
if (esub && (type_isstr(esub) || type_isslice(esub))) {
cgslicehdr(c, D_BX);
break;
}
/* tagged element: load slot words into (AX=tag,
* DX=val0, CX=val1, R8=val2) — matches the
* tagged-return ABI so let-init / match / call-arg
* paths consume it without spilling. Nullable folded
* element is one word in AX (caller treats it as a
* pointer). */
if (elem_tagged) {
int ssz = (int)esubu->size;
/* #37: >32B box — ADDRESS in AX (the
* cg_tagged_memread convention); the
* 4-reg cursor walk below would
* truncate past payload word 2. */
if (ssz > TUPLE_GPCAP * 8) {
ins2(c, A_MOVQ, areg(D_BX),
areg(D_AX));
break;
}
if (ssz > 24)
ins2(c, A_MOVQ, amem(D_BX, 24),
areg(D_R8));
if (ssz > 16)
ins2(c, A_MOVQ, amem(D_BX, 16),
areg(D_CX));
if (ssz > 8)
ins2(c, A_MOVQ, amem(D_BX, 8),
areg(D_DX));
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
break;
}
/* float element → MOVSS/MOVSD into X0: the consumer's
* ADDSD/MOVSD spill machinery already expects X0, but the
* integer fldloadop below would leave it in AX and the SSE
* side reads stale (#119). Float-ness from esub — the same
* type the esz above reads. Twin of the scalar-float global
* load at cgen.c:2014. */
if (type_isfloat(esub)) {
int op = type_isf32(esub) ? A_MOVSS : A_MOVSD;
ins2(c, op, amem(D_BX, 0), areg(D_X0));
break;
}
int load_op = fldloadop(esub, esz);
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
break;
}
/* Fallback: evaluate base (treat as plain pointer) and
* dereference at base+idx. Pick the load opcode by element
* size — `b.data[i]` on a *u8 must read 1 byte, not 8.
*
* Scale the index in a register before pushing, because
* IMULQ on a memory operand isn't currently encoded by w6a
* (modrm bits use mod=3 register form).
*
* #135: N_DOT base on a `[N]T`-typed field needs the field's
* ADDRESS, not its value. cgexpr on N_DOT would auto-deref and
* load the field's 8-byte value as if it were a pointer — the
* symmetric READ-side of the LHS bug at the cgassign sites.
* cg_dotbase_addr emits the address inline. */
cgexpr(c, n->rhs, locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX));
if (!cg_dotbase_addr(c, n->lhs, D_AX, locals))
cgexpr(c, n->lhs, locals);
ins1(c, A_POPQ, areg(D_BX));
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
/* #156 (PREREQ-1 read-half): array element → AX already holds
* &elem (base+idx*esz); a nested index adds its offset and
* dereferences. See the N_IDENT arm above. */
if (esubu && esubu->kind == TY_ARRAY)
break;
/* str/slice element via fallback base: load the full (ptr, len,
* cap) header into (AX, BX, CX). Kind-gate on type_isstr||
* type_isslice, never size==24 (see Site A). esub, not u->sub
* (#61 — see the ident arm). Base is AX. */
if (esub && (type_isstr(esub) || type_isslice(esub))) {
cgslicehdr(c, D_AX);
break;
}
/* tagged element via fallback base: AX holds the element
* address — copy to BX (the load into AX clobbers it), then
* load slot words. */
if (elem_tagged) {
int ssz = (int)esubu->size;
/* #37: >32B box — AX already holds the element
* address; leave it (cg_tagged_memread). */
if (ssz > TUPLE_GPCAP * 8)
break;
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
if (ssz > 24)
ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8));
if (ssz > 16)
ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX));
if (ssz > 8)
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_DX));
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
break;
}
/* float element via fallback base → X0 (see Site A, #119). The
* base address is in AX; MOVSS/MOVSD reads the element into X0. */
if (type_isfloat(esub)) {
int op = type_isf32(esub) ? A_MOVSS : A_MOVSD;
ins2(c, op, amem(D_AX, 0), areg(D_X0));
break;
}
{
int load_op = fldloadop(esub, esz);
ins2(c, load_op, amem(D_AX, 0), areg(D_AX));
}
break;
}
case N_SLICE: {
/* base[lo:hi] as a slice value. Leaves the triple in
* (AX=base+lo*esz, BX=hi-lo, CX=base_cap-lo) so callers can
* route to a slice slot, return, or arg with the same ABI.
* cap is the storage remaining to the base's end (#20,
* Go/Hare-identical), via cg_base_cap. ptr advances by BYTES
* (lo*esz, #76; ref/hare/rt/ensure.ha:30 membsz-unit); esz
* from the type table, mirroring the N_INDEX idiom. */
Node *base = n->lhs;
Node *lo = n->rhs;
Node *hi = n->cond;
Type *bt = base ? base->type : NULL;
/* Transitive chase (#5 alias arc) — see the N_INDEX twin. */
Type *bu = type_chase_named(bt);
/* esz from the type table for an N_IDENT base (#76) or an
* N_DOT array/slice-field base (#252: a struct-field slice
* `s.obuf[lo:hi]` must scale by the field's element width, not
* stay esz=1 — silently wrong for non-u8 elements). Other
* non-ident bases stay esz=1 (unscaled) -- #76 residual,
* non-ident cluster #74. */
int esz = (base && (base->kind == N_IDENT
|| base->kind == N_DOT || base->kind == N_ARRLIT)
&& bu && bu->sub)
? (int)bu->sub->size : 1;
if (base && base->kind == N_IDENT) {
int boff = localfind(locals, base->str);
int isglobal = (boff == 0) && let_islet(base->str);
if (isglobal && bu && bu->kind == TY_ARRAY) {
ins2(c, A_LEAQ, masym(c, base->str),
areg(D_AX));
} else if (isglobal) {
ins2(c, A_MOVQ, masym(c, base->str),
areg(D_AX));
} else if (bu && bu->kind == TY_ARRAY) {
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
} else {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
}
} else if (base && base->kind == N_ARRLIT && bu
&& bu->kind == TY_ARRAY) {
/* #31: an array LITERAL base — the desugared one-step
* `let xs: []T = [..]` borrow (the ONLY context that
* reaches here; call-arg/return/assign loud-reject at the
* checker, reject_arrlit_borrow, deferred to #33). The
* literal has no storage address — cgexpr would leave
* AX=garbage and the borrow's .ptr would dangle.
* Materialise it into a FRESH per-borrow @slicescr stack
* slot (distinct slot per borrow: a borrow's backing must
* stay live for the slice's lifetime, so it can't share a
* cached SSoT slot the way @aggargscr/@tagscr — drained/
* consumed in place — do; two live borrows would otherwise
* alias one backing). Reuses local_alloc + the shared
* array-init fill; the checker re-stamped base->type to
* [count]T (#25) so the fill stores at the declared
* element width.
*
* Escape (WHY, rob): a `let xs: []T = [..]; return xs;`
* returns a slice pointing at this frame slot, freed on
* return = dangling. This is IDENTICAL to the pre-existing
* named-array borrow (`let a: [N]T = [..]; return a;`) and
* is Hare-consistent: ww has no escape analysis, no GC, no
* heap promotion — borrowing a local past its frame is a
* programmer footgun, not promoted. Don't "fix" this
* expecting heap promotion; ww deliberately doesn't, same
* as Hare. */
int cnt = (int)bu->alen;
int bsz = (bu->sub ? (int)bu->sub->size : 1) * cnt;
if (bsz < 1) bsz = 1;
int scr = local_alloc(c, &locals, "@slicescr", bsz,
cg_frame);
cg_arrlit_fill_bp(c, &locals, bu, base, scr);
ins2(c, A_LEAQ, amem(D_BP, scr), areg(D_AX));
} else if (base) {
/* #252: N_DOT `[N]T`-field base → field ADDRESS via
* cg_dotbase_addr (LEAQ), not the auto-deref VALUE load
* cgexpr would emit. Sibling of the #135 read-side. */
if (!cg_dotbase_addr(c, base, D_AX, locals))
cgexpr(c, base, locals);
}
ins1(c, A_PUSHQ, areg(D_AX));
if (lo) cgexpr(c, lo, locals);
else cgexpr_int(c, 0);
ins1(c, A_PUSHQ, areg(D_AX));
if (hi) {
cgexpr(c, hi, locals);
} else if (bu && bu->kind == TY_ARRAY) {
cgexpr_int(c, (long long)bu->alen);
} else if (base && base->kind == N_IDENT && bu &&
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
int boff = localfind(locals, base->str);
int isglobal = (boff == 0) && let_islet(base->str);
if (isglobal) {
ins2(c, A_LEAQ, masym(c, base->str),
areg(D_CX));
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_AX));
} else {
ins2(c, A_MOVQ, amem(D_BP, boff + 8),
areg(D_AX));
}
} else {
cgexpr_int(c, 0);
}
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
ins1(c, A_POPQ, areg(D_CX));
ins1(c, A_POPQ, areg(D_AX));
/* ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit).
* DX=lo*esz; CX=lo PRESERVED for len + cap (#20). */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_DX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_DX));
ins2(c, A_ADDQ, areg(D_DX), areg(D_AX));
} else {
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
}
ins2(c, A_SUBQ, areg(D_CX), areg(D_BX));
/* cap = base_cap - lo (#20); CX=lo, BX=len here. */
if (cg_base_cap(c, base, bu, locals, D_DX)) {
ins2(c, A_SUBQ, areg(D_CX), areg(D_DX));
ins2(c, A_MOVQ, areg(D_DX), areg(D_CX));
} else {
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
}
break;
}
case N_TUPLE:
/* #241: a literal tuple rvalue `(a, b)` is a value — pack its
* elements into the register cursor (mirror cgreturn's N_TUPLE
* arm) so a let-bind / destructure consumer reads every element,
* not just AX = 0 from the default arm below. */
cg_tuple_lit_to_cursor(c, &locals, n, NULL);
break;
default:
cgexpr_int(c, 0);
break;
}
}
static void
cgstmt(Cg *c, Node *n, Local **locals, int *frame)
{
if (n == NULL) return;
switch (n->kind) {
case N_BLOCK: {
/* Save/restore the locals head across the block (post-#27).
* Inner-scope `let` bindings prepend to *locals via localoff;
* without this restore, the prepended stubs leak into sibling
* and ancestor scopes, and localfind (head-first) returns the
* inner binding's offset for an identifier that semantically
* belongs to the outer scope. The frame is left grown — slot
* lifetimes don't overlap with later siblings observably (the
* popped stubs' offsets are no longer reachable by name), but
* we don't reclaim the frame bytes; that's the conservative
* choice C compilers make for simple lowering.
*
* cgfn iterates fn->body->list directly to bypass this
* save/restore at the function's outermost block — defers
* (and the implicit-return epilogue) need locals intact. */
Local *saved = *locals;
for (Node *s = n->list; s; s = s->next)
cgstmt(c, s, locals, frame);
*locals = saved;
break;
}
case N_EXPRSTMT:
cgexpr(c, n->lhs, *locals);
break;
case N_LET: {
Type *lt = n->type;
/* type_chase_named (#22): a chain `type a = struct{...};
* type b = a;` stacks two TY_NAMED layers. A single peel
* left `lu` pointing at the inner alias (still TY_NAMED),
* collapsed the struct/slice/tagged sizing arms to the 8B
* fallback, and the slot under-allocated the local. */
Type *lu = type_chase_named(lt);
/* #43: every composite kind already has its byte size cached in
* lu->size; route through it instead of re-asserting 16/24 for
* str/slice and re-reading for the others. */
int sz = 8;
if (lu && (lu->kind == TY_ARRAY || lu->kind == TY_SLICE
|| lu->kind == TY_STR || lu->kind == TY_STRUCT
|| lu->kind == TY_TUPLE || lu->kind == TY_TAGGED))
sz = (int)lu->size;
int off = localoff(c, locals, n->str, sz, frame);
int isf = cg_isfloat(lt);
int isf32 = type_isf32(lt);
/* alloc([], n) initialiser for a slice local: allocate
* n*esize bytes, build the {ptr, 0, n} header in the slot.
* Element size comes from the declared slice type.
*
* Task #30 graduated the builtin to `([]T | nomem)`. The let
* declares a bare `[]T`, so the canonical idiom wraps in `!`
* (abort on OOM) or `?` (propagate nomem to the enclosing
* fn's tagged return). Task #45 extends the shortcut to also
* match N_TRYPROP and emit the propret pattern. */
{
Node *call = NULL;
int via_tryunw = 0;
int via_tryprop = 0;
if (n->rhs && n->rhs->kind == N_TRYUNW && n->rhs->lhs
&& n->rhs->lhs->kind == N_CALL) {
call = n->rhs->lhs;
via_tryunw = 1;
} else if (n->rhs && n->rhs->kind == N_TRYPROP
&& n->rhs->lhs
&& n->rhs->lhs->kind == N_CALL) {
call = n->rhs->lhs;
via_tryprop = 1;
}
if (call && lu && lu->kind == TY_SLICE && sz == 24
&& call->lhs && call->lhs->kind == N_IDENT
&& strcmp(call->lhs->str, "alloc") == 0
&& call->list && call->list->kind == N_ARRLIT
&& call->list->list == NULL
&& call->list->next
&& call->list->next->next == NULL) {
Node *count = call->list->next;
int esz = (lu->sub) ? (int)lu->sub->size : 1;
cgexpr(c, count, *locals); /* AX = n */
ins1(c, A_PUSHQ, areg(D_AX)); /* save count */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
ins2(c, A_IMULQ, areg(D_BX), areg(D_AX));
}
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
ins1(c, A_CALL, asym(ffi_resolve("malloc")));
if (via_tryunw) {
char *ok = mklabel(c, "tryunw_ok");
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JNE, abranch(ok));
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
ins0(c, A_SYSCALL);
label(c, ok);
} else if (via_tryprop) {
/* #45: null = nomem; propagate to the
* enclosing fn's tagged return. AX = tag
* of nomem variant in cg_ret_type; epilogue
* RETs to caller. */
char *ok = mklabel(c, "tryprop_ok");
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JNE, abranch(ok));
Type *r = cg_ret_type;
r = type_chase_named(r);
int nidx = cg_tag_for_variant(r, ty_nomem);
if (nidx < 0) nidx = 1;
ins2(c, A_MOVQ, aimm(nidx), areg(D_AX));
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
label(c, ok);
}
ins1(c, A_POPQ, areg(D_BX)); /* count */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16));
break;
}
}
/* str IS []u8: cgexpr produces (AX=ptr, BX=len, CX=cap);
* store all three, same as the slice initialiser below.
* #43 gate via ty_str->size already tracks the 24B bump
* (#1/Phase 3). */
if (n->rhs && type_isstr(lt) && sz == (int)ty_str->size) {
cgexpr(c, n->rhs, *locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
break;
}
/* Tuple initialiser (#105 / #164/#107): every IN-CAP tuple
* receive routes here. Each element rides its SysV class: a
* float its SSE cursor reg (X0,X1 = tuple_sse_seq), an
* integer/ptr word its INTEGER cursor reg (tuple_rseq), a
* slice/str its 3-word {ptr,len,cap} header over consecutive
* INTEGER cursor regs — INDEPENDENT counters, so the RETURN
* leaves floats in X0/X1 and integer words in AX/DX/CX/R8. A
* blanket MOVQ spill would store garbage where a float rode
* and the #103-FACE-Z field read (MOVSD-from-slot) would see
* it. tuple_store routes each element from its real class
* into its positional slot (eoff steps by the element's slot
* size: a slice/str takes its 24B header); the same split
* drives the destructure / reassign sites.
*
* C-t1 (#33 family): keyed on the TYPE's register classify
* (cg_sret_retsize == 0, the shared SSoT), not the sz==16/32
* magic — that key missed sz==24/40/48 in-cap shapes (3-scalar
* tuples dropped words 2+ silently) and pre-C-t0 missed the
* packed sz==8 entirely. Over-cap falls through to the sret
* receive below, exactly as before. */
if (n->rhs && lu && lu->kind == TY_TUPLE
&& cg_sret_retsize(lt) == 0) {
/* #57: a tuple LITERAL rhs carries the DECLARED type
* into the cursor fill — its stamped type is element-
* constructed, so a declared-tagged element's concrete
* rvalue skipped the widen and the fill/receive cursor
* walks skewed (let-twin of the return-position bug;
* probe /tmp/p57/q1_let). Same emission as the cgexpr
* route for every declared-tagged-free literal. */
if (n->rhs->kind == N_TUPLE)
cg_tuple_lit_to_cursor(c, locals, n->rhs, lu);
else
cgexpr(c, n->rhs, *locals);
int gpcur = 0, ssecur = 0, eoff = 0, ef32;
for (Tparam *p = lu->params; p; p = p->next) {
int isflt = fld_isfloat(p->type, &ef32);
tuple_store(c, p->type, gpcur, ssecur,
off + eoff);
if (isflt)
ssecur++;
else
gpcur += tuple_eslot(p->type) / 8;
eoff += tuple_eslot(p->type);
}
break;
}
/* #22a (rule 7, ken R1): an OVER-CAP tuple init whose rhs is
* not a CALL has no store path — only the CALL shape rides
* the generic sret receive below; every other rhs fell past
* ALL the store arms to NOTHING (silent uninitialized-frame
* reads). Pre-existing for (str,str) literals; the #22a
* tagged slot sizes routed tagged shapes into it. cgexpr's
* cursor materialisers carry the loud caps (the literal /
* ident messages wwstage emits for the same shapes — rule
* 10); the fatal after is the net for any rhs kind whose
* cgexpr doesn't loud. Wiring is the #10/#22b sret family. */
if (n->rhs && n->rhs->kind != N_CALL && lu
&& lu->kind == TY_TUPLE && cg_sret_retsize(lt) > 0) {
cgexpr(c, n->rhs, *locals);
fatal("over-cap tuple initialiser from a non-call "
"source unwired (see #10/#22b)");
}
/* #38b residual (rule 7): `let w: T = f()?;` / `f()!` where f
* returns an sret-classified tagged union — the unwrap would
* need a mem-based read of the sret slot. The N_LET arms
* below have no TRYUNW/TRYPROP shape for a >8B lt, so the
* rhs was SILENTLY dropped (no CALL emitted; wwstage's cglet
* default does cgexpr and hits the cgtryunw/cgtryprop gates —
* this keeps acceptance symmetric, rule 10). */
if (n->rhs
&& (n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP)
&& n->rhs->lhs && n->rhs->lhs->kind == N_CALL
&& cg_sret_retsize(n->rhs->lhs->type) > 0)
fatal("#38b: `?`/`!` on an sret-class call result "
"unwired (mem-based unwrap is a #40-family "
"follow-up)");
/* Tagged-union initialiser. Delegates to cg_widen_tagged_store,
* which handles nullable fold, tagged→tagged (with tag remap
* when variant indices differ), struct payload (ident or
* literal — field-by-field at slot+8+field_off), str payload,
* and scalar payload (with zero-pad to the slot size).
*
* #38b: an sret-classified tagged CALL result is in memory,
* not the cursor — an exact-type receive falls through to the
* generic sret receive below (the let's slot IS the dest); a
* widening receive needs mem-to-mem tag-remap (#40, unwired). */
if (n->rhs && lu && lu->kind == TY_TAGGED) {
int rhs_sret_call = n->rhs->kind == N_CALL
&& cg_sret_retsize(n->rhs->type) > 0;
if (!rhs_sret_call) {
cg_widen_tagged_store(c, locals, lu, n->rhs,
D_BP, off, sz);
break;
}
Type *ru = type_chase_named(n->rhs->type);
if (!(ru == lu || type_eq(n->rhs->type, lt)))
fatal("#40: sret-class call result cannot be "
"widened into a tagged slot (mem-to-mem "
"widen unwired)");
}
/* Every slice initialiser routes here — fn-return, slice
* ident, slice param, and sub-slice `buf[lo:hi]`. cgexpr
* leaves (AX=ptr, BX=len, CX=cap); store all three. The
* sub-slice case once had a vestigial inline builder that
* duplicated cgexpr's N_SLICE path and mishandled global
* bases; dropping it aligns cstage onto wwstage's shared
* store path (find-4). Runs after the alloc specialisation
* above so that keeps its direct {ptr,0,n} shape. */
if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24) {
cgexpr(c, n->rhs, *locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
break;
}
/* struct literal initialiser: field-by-field store via the
* shared cg_structlit_fill_bp helper. The literal carries
* op == TK_ELLIPSIS when the source ends in `..., ...` —
* helper zero-fills the slot first so unmentioned fields
* read as 0. Nested struct-typed structlit field values
* recurse into the helper at the correct offset instead of
* landing AX = first-qword via cgexpr (#17 silent zero). */
if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu
&& lu->kind == TY_STRUCT) {
cg_structlit_fill_bp(c, locals, lu, n->rhs, off);
break;
}
/* sret receive (#23 / #10 Fold B): the let's own slot IS the
* caller-prealloc dest; the call writes through hidden RDI
* directly into our slot, no AX/DX/CX shuffle. Set
* cg_sret_dest_off so the nested cgexpr → N_CALL path emits
* `LEAQ off(BP), RDI` before CALL. Keys on cg_sret_retsize
* (the shared sret SSoT), NOT a kind — so an over-cap tuple
* return (Fold A made the callee sret it) materialises its
* WHOLE slot here exactly like a >24B struct, and t.0/t.1
* read by offset afterward. */
if (n->rhs && n->rhs->kind == N_CALL
&& cg_sret_retsize(lt) > 0) {
cg_sret_dest_off = off;
cgexpr(c, n->rhs, *locals);
cg_sret_dest_off = 0;
break;
}
/* Whole-struct receive for sizes <=24B (call-result rhs).
* Counterpart of #4's cgreturn ABI: cgexpr leaves
* AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23], zero-
* padded to 24B by the producer.
*
* ASYMMETRY (do NOT mirror the sender): producer emits three
* uniform MOVQs into a zero-padded 24B scratch slot; the
* receiver must write only `sz` bytes — MOVQ for full 8B
* chunks plus a sized tail (MOVL/MOVW/MOVB) by the *declared*
* struct size. Otherwise a trailing 1..7-byte chunk would
* overrun into the next local slot.
*
* Tail chunks in {3,5,6,7} (would need shift-and-store from
* the register) are unreachable under WW struct alignment
* rules (field aligns force size%align==0); the guard
* excludes them so they fall through to the existing scalar
* path rather than emit a stomping MOVQ tail. Sizes >24B also
* fall through (sret deferred, same constraint as #4). */
/* #171a: float-bearing struct RECEIVE (the return twin of
* #165's param recv). cgexpr leaves each float eightbyte in
* its SSE return reg (X0,X1 = tuple_sse_seq) and each INT
* eightbyte in its INTEGER return reg (AX,DX = tuple_rseq),
* on INDEPENDENT cursors per SysV (ref/qbe/amd64/sysv.c retr)
* — so a float is read from the next XMM regardless of its
* positional eightbyte (struct{f64,i32}: e0←X0, e1←AX). A
* qualifying struct's size is maxalign-rounded to a multiple
* of 8 (an f64 forces align 8), so every eightbyte is a full
* word — the #169 sized tail (MOVL/MOVB) is unreachable here.
* struct_float_class gates to qualifying structs; all-int +
* f32 fall through to the GP recv below (byte-id / #171b). */
if (n->rhs && n->rhs->kind == N_CALL && lu
&& lu->kind == TY_STRUCT) {
int sclass[2], snb;
if ((snb = struct_float_class(lu, sclass)) > 0) {
cgexpr(c, n->rhs, *locals);
int gpcur = 0, ssecur = 0;
for (int e = 0; e < snb; e++) {
if (sclass[e]) {
ins2(c, A_MOVSD,
areg(tuple_sse_seq[ssecur]),
amem(D_BP, off + e * 8));
ssecur++;
} else {
ins2(c, A_MOVQ,
areg(tuple_rseq[gpcur]),
amem(D_BP, off + e * 8));
gpcur++;
}
}
break;
}
}
if (n->rhs && n->rhs->kind == N_CALL && lu
&& (lu->kind == TY_STRUCT || lu->kind == TY_ARRAY)
&& sz <= 24
&& (sz % 8 == 0 || sz % 8 == 1
|| sz % 8 == 2 || sz % 8 == 4)) {
cgexpr(c, n->rhs, *locals);
int regs[3] = { D_AX, D_DX, D_CX };
int full = sz / 8;
int tail = sz % 8;
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(D_BP, off + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW : A_MOVB;
ins2(c, op, areg(regs[full]),
amem(D_BP, off + full * 8));
}
break;
}
/* array literal initialiser: `let xs: [N]T = [a, b, c];`.
* Walk elements in declaration order, store each at off + i*esz
* using the right width for the element type. The trailing
* `...` repeat marker (an N_FIELD with str=="...") fills the
* remaining slots with the last value.
*
* str/slice element (24B = ptr+len+cap, post-#1) needs all
* three words stored: cgexpr leaves it as (AX=ptr, BX=len,
* CX=cap), and a single MOVQ from AX would leave .len/.cap as
* whatever the stack held — silent miscompile (#20/#270 str-
* slice arm). The per-element store branches on TY_STR/TY_SLICE
* before falling through to the scalar MOVB/MOVL/MOVQ path.
* [N]tagged element arrays still land in the multi-word gap
* (is_agg excludes TY_TAGGED) — tracked as task #12. */
if (n->rhs && n->rhs->kind == N_ARRLIT && lu
&& lu->kind == TY_ARRAY) {
cg_arrlit_fill_bp(c, locals, lu, n->rhs, off);
break;
}
/* Struct ident copy: `let p2: T = p1;` where T is a struct
* >8B and rhs is a local ident. Pre-fix the path fell
* through to the `sz == 8` test (false) and emitted
* nothing — the dst slot read whatever the stack held,
* presenting as a silent zero copy on a fresh frame.
* Per-qword MOVQ from src slot to dst slot, with a sized
* tail (MOVL/MOVB) for natural sizes that aren't
* 8-aligned (e.g. `struct { i32, i32, i32 }` is 12B).
* Mirrors the slot-to-slot copy in cg_widen_tagged_store
* for a TY_STRUCT payload (Task #32). */
if (n->rhs && n->rhs->kind == N_IDENT && lu
&& lu->kind == TY_STRUCT && sz > 8) {
Local *src_l = NULL;
for (Local *l = *locals; l; l = l->next)
if (strcmp(l->name, n->rhs->str) == 0) {
src_l = l; break;
}
if (src_l) {
int soff = src_l->off;
int k = 0;
while (k + 8 <= sz) {
ins2(c, A_MOVQ,
amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k));
k += 8;
}
if (k < sz) {
int tail = sz - k;
int lop = (tail == 4) ? A_MOVL :
(tail == 1) ? A_MOVB : A_MOVQ;
ins2(c, lop,
amem(D_BP, soff + k), areg(D_AX));
ins2(c, lop, areg(D_AX),
amem(D_BP, off + k));
}
break;
}
}
/* #265 fold-1/1b (#268): aggregate let-init copy from an
* ADDRESSABLE rhs. The whole family converges on ONE memcpy
* loop fed by a per-rhs source-address setup: `*p` (deref,
* fold-1), an array ident `= s` (struct-ident is the #32 arm
* above), an N_DOT field `= o.i`, an N_INDEX element `= a[i]`
* — T a struct or array >8B. Each shape lands the SOURCE
* ADDRESS in SI; the loop copies sz bytes (lu->size, the #254
* non-slot-padded ABI extent) slot→slot — a MOVQ run plus a
* sized MOVL/MOVW/MOVB tail. Pre-fix every non-deref shape was
* wrong: array-ident/N_DOT truncated to the 8B scalar tail
* below; N_INDEX scalar-loaded the element address as a value
* (segfault). Both stages emit the identical sequence
* (rule-10); the by-value RETURN ABI is fold-2 (#267). The
* source-addr setups reuse closed machinery: LEAQ-slot (ident),
* the deref operand (cgexpr), cg_dotchain_addr (#253, N_DOT),
* the &base[i] spine (#252, N_INDEX). */
if (n->rhs && lu
&& (lu->kind == TY_STRUCT || lu->kind == TY_ARRAY)
&& sz > 8) {
int havesrc = 0;
if (n->rhs->kind == N_UN && n->rhs->op == TK_STAR) {
cgexpr(c, n->rhs->lhs, *locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_SI));
havesrc = 1;
} else if (n->rhs->kind == N_IDENT) {
int soff = localfind(*locals, n->rhs->str);
if (soff != 0) {
ins2(c, A_LEAQ, amem(D_BP, soff),
areg(D_SI));
havesrc = 1;
/* the laid-out-aggregate globals (#129
* A.2/A.3): a let, an array def, or a struct
* def. Struct defs copy here exactly as
* struct-let globals do; omitting def_is-
* structdef truncated the def case alone and
* diverged from wwstage (rule-10). */
} else if (let_islet(n->rhs->str)
|| def_isarraydef(n->rhs->str)
|| def_isstructdef(n->rhs->str)) {
ins2(c, A_LEAQ, masym(c, n->rhs->str),
areg(D_SI));
havesrc = 1;
}
} else if (n->rhs->kind == N_DOT) {
if (cg_dotchain_addr(c, n->rhs, D_SI, *locals))
havesrc = 1;
} else if (n->rhs->kind == N_INDEX) {
Node *base = n->rhs->lhs;
Node *idx = n->rhs->rhs;
Type *bt = base ? base->type : NULL;
Type *bu = type_chase_named(bt);
if (base && base->kind == N_IDENT && bu
&& bu->kind == TY_ARRAY) {
int esz = (bu->sub)
? (int)bu->sub->size : 1;
cgexpr(c, idx, *locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
}
int boff = localfind(*locals,
base->str);
if (boff != 0)
ins2(c, A_LEAQ,
amem(D_BP, boff),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
ins2(c, A_ADDQ, areg(D_BX),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
areg(D_SI));
havesrc = 1;
} else if (base && (base->kind == N_DOT
|| base->kind == N_INDEX)) {
/* #270-3a: the index BASE is an N_DOT
* array-field (`x.arr[i]`) or a nested
* N_INDEX (`a[i][j]`); the N_IDENT-base arm
* above missed both, so the copy fell to the
* 8B truncation below. Compute &base[idx]:
* scaled idx on the stack, then &base via
* cg_dotbase_addr (N_DOT field address) or
* the &abase[bidx] spine (nested N_IDENT-
* array base), then add. */
int esz = (bu && bu->sub)
? (int)bu->sub->size : 1;
cgexpr(c, idx, *locals);
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX));
int baseok = 0;
if (base->kind == N_DOT) {
baseok = cg_dotbase_addr(c, base,
D_AX, *locals);
} else {
Node *ab = base->lhs;
Node *bidx = base->rhs;
Type *abt = ab ? ab->type : NULL;
Type *abu = type_chase_named(abt);
if (ab && ab->kind == N_IDENT
&& abu
&& abu->kind == TY_ARRAY) {
int aesz = (abu->sub)
? (int)abu->sub->size
: 1;
cgexpr(c, bidx, *locals);
if (aesz > 1) {
ins2(c, A_MOVQ,
aimm(aesz),
areg(D_CX));
ins2(c, A_IMULQ,
areg(D_CX),
areg(D_AX));
}
int aoff = localfind(
*locals, ab->str);
if (aoff != 0)
ins2(c, A_LEAQ,
amem(D_BP,
aoff),
areg(D_BX));
else
ins2(c, A_LEAQ,
masym(c,
ab->str),
areg(D_BX));
ins2(c, A_ADDQ,
areg(D_BX),
areg(D_AX));
baseok = 1;
}
}
ins1(c, A_POPQ, areg(D_BX));
if (baseok) {
ins2(c, A_ADDQ, areg(D_BX),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
areg(D_SI));
havesrc = 1;
}
}
}
/* C4 (F5, task #7): the remaining ADDRESSABLE rhs
* shapes — a slice-base element (`= xs[0]`; the arms
* above have TY_ARRAY/N_DOT/N_INDEX bases but no
* TY_SLICE base) and deref-spine leaves
* (`= (*ts)[i].cap`) — resolve through cgplaceaddr
* (the C1 resolver; enumerated arms dispatch first so
* their asm is untouched). Pre-C4 these fell out with
* havesrc=0: cstage emitted NOTHING (slot
* uninitialised), wwstage's scalar default truncated
* to 8B — gate-blind cs≠ww. */
if (!havesrc && cgplaceaddr(c, n->rhs, D_SI, *locals))
havesrc = 1;
if (havesrc) {
int k = 0;
for (; k + 8 <= sz; k += 8) {
ins2(c, A_MOVQ, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k));
}
if (k + 4 <= sz) {
ins2(c, A_MOVL, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BP, off + k));
k += 4;
}
if (k + 2 <= sz) {
ins2(c, A_MOVW, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVW, areg(D_AX),
amem(D_BP, off + k));
k += 2;
}
if (k + 1 <= sz) {
ins2(c, A_MOVB, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BP, off + k));
k += 1;
}
break;
}
/* C4: nothing below this arm can initialise a >8B
* struct/array slot — every fall-through was a silent
* miscompile (rule 7). */
fatal("let: aggregate init from unhandled rhs shape "
"(task #7/rule-7)");
}
if (n->rhs && sz == 8) {
cgexpr(c, n->rhs, *locals);
if (isf) {
int mov = isf32 ? A_MOVSS : A_MOVSD;
ins2(c, mov, areg(D_X0), amem(D_BP, off));
} else {
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
}
} else if (sz == 8) {
ins2(c, A_MOVQ, aimm(0), amem(D_BP, off));
} else if (!n->rhs && sz > 8 && lu && lu->kind != TY_ARRAY) {
/* `let x: T;` with no rhs for a multi-word composite
* (str/slice/tuple/struct/tagged). Zero the slot so
* reads after the bare let see {0...} rather than
* whatever the stack already held. Arrays keep the
* per-index-write contract — leave them uninit. */
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
int zi = 0;
while (zi + 8 <= sz) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + zi));
zi += 8;
}
while (zi + 4 <= sz) {
ins2(c, A_MOVL, areg(D_AX),
amem(D_BP, off + zi));
zi += 4;
}
while (zi < sz) {
ins2(c, A_MOVB, areg(D_AX),
amem(D_BP, off + zi));
zi += 1;
}
}
/* arrays left uninitialised — caller writes via index */
break;
}
case N_RETURN:
/* run all defers in reverse before the actual return */
for (int di = ndefers - 1; di >= 0; di--)
cgexpr(c, defers[di], *locals);
/* If the function returns a tagged union and the value is
* one of the variant types, wrap into (tag, value). If rhs
* already produces a tagged union (e.g. forwarding another
* fallible call), pass it through unchanged.
*
* Tagged-return ABI: AX=tag, DX=value0[, CX=value1]. CX is
* only meaningful when the union has a >8B variant (e.g.
* str, where ptr→DX and len→CX).
*
* Bare `return;` from a tagged-union-returning function: this
* is producing the void variant. Emit its tag; the payload is
* undefined (void has size 0). */
if (n->lhs == NULL && cg_ret_type) {
Type *rt = type_chase_named(cg_ret_type);
if (rt && rt->kind == TY_TAGGED) {
/* #38b: an sret-classified tagged return (slot
* > the AX/DX/CX/R8 cursor) writes the void-
* variant tag through *(@sretarg) and returns
* the dest pointer — the cursor can't carry the
* slot and the caller reads memory. */
if (cg_sret_retsize(rt) > 0) {
int tag = cg_tag_for_variant(rt, ty_void);
if (tag < 0) tag = 0;
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_BX));
ins2(c, A_MOVQ, aimm(tag),
amem(D_BX, 0));
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
if (rt->nullable) {
/* bare `return;` is the void/null
* variant: emit AX = 0. */
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
} else {
int tag = cg_tag_for_variant(rt, ty_void);
if (tag < 0) tag = 0;
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
}
if (n->lhs && cg_ret_type) {
/* #89: the single peel left a 2-level alias return
* type NAMED (rt — the whole tagged block skipped, no
* tag synthesis) and an alias struct source NAMED (vu
* — isstruct missed, the scalar arm shuffled word0
* into DX and ZEROED the rest of the payload). Both
* silent; ww routes via rhsstructpayload→
* structlookupchain since B2-c2 and is the runtime-
* correct reference. */
Type *rt = type_chase_named(cg_ret_type);
if (rt && rt->kind == TY_TAGGED) {
Type *vt = n->lhs->type;
Type *vu = type_chase_named(vt);
int istagged = vu && vu->kind == TY_TAGGED;
/* #263: passthrough forwards the source's AX/DX/CX
* unchanged — correct ONLY when the source already
* materialised the FULL tagged slot into registers:
* N_CALL / N_INDEX / N_DOT (the #261-broadened set).
* A tagged LOCAL ident leaves only word0 (the tag)
* in AX (cgexpr of an ident loads a single word), so
* DX (the payload) is garbage and the passthrough
* drops it. Route a tagged-ident return through the
* scratch-widen path below instead. Mirrors wwstage's
* forwardtagged kind filter, which already excludes
* N_IDENT (selfhost cgenstmt). */
int srcreg = n->lhs->kind == N_CALL ||
n->lhs->kind == N_INDEX ||
n->lhs->kind == N_DOT;
int passthrough = istagged && srcreg && (vu == rt ||
type_eq(vt, cg_ret_type));
int isstruct = vu && vu->kind == TY_STRUCT;
/* #242: a tuple variant must be PACKED into the union
* payload (tag + per-element words), not shuffled like a
* bare scalar — route it through the scratch-slot widen
* path (cg_widen_tagged_store TY_TUPLE arm). The scalar
* arm below zeroed the whole value (never packed the
* operands). */
int istuple = vu && vu->kind == TY_TUPLE;
/* #38b: sret-classified tagged return (slot >
* the AX/DX/CX/R8 cursor). Three shapes:
* - exact-type N_CALL forward: inner sret's
* straight into outer's caller dest (#9
* shape, cg_sret_forward).
* - widening from an sret-class tagged source
* ((A|B)→(A|B|C) mem-to-mem tag-remap):
* unwired, loud-stop — #40.
* - everything else: cg_widen_tagged_store
* through *(@sretarg) (the widener already
* speaks non-BP bases, the #34 precedent),
* then return the dest pointer. */
if (cg_sret_retsize(rt) > 0) {
int sz = (int)rt->size;
if (passthrough
&& n->lhs->kind == N_CALL) {
cg_sret_forward = 1;
cgexpr(c, n->lhs, *locals);
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_AX));
} else if (istagged
&& n->lhs->kind != N_IDENT
&& (int)vu->size > TUPLE_GPCAP * 8
&& !cg_tagged_memread(n->lhs)) {
/* #37 wired the N_INDEX/N_DOT
* mem-read into the widener;
* the remaining >32B kinds
* stay loud. */
fatal("#40: widening tagged "
"return-forward of a >32B "
"source needs mem-to-mem "
"tag-remap (unwired)");
} else {
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_BX));
cg_widen_tagged_store(c, locals,
rt, n->lhs, D_BX, 0, sz);
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_AX));
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
if (rt->nullable) {
cgexpr(c, n->lhs, *locals);
} else if (passthrough) {
/* same tagged type: forward AX/DX/CX. */
cgexpr(c, n->lhs, *locals);
} else if (!istagged && !isstruct && !istuple) {
/* str / slice / scalar variant: synthesise
* the tag in AX and shuffle the value into
* DX[/CX[/R8]]. Direct register path keeps
* the asm short — no scratch slot.
* Tagged-return ABI: AX=tag, DX=word0,
* CX=word1, R8=word2. Slice payload uses
* all four; str uses three; scalar uses
* two. Unused ABI words must still be
* zeroed because the receiver
* (cg_widen_tagged_store call-source arm)
* writes AX/DX/CX/R8 unconditionally sized
* by the dst slot; stale CX/R8 from the
* caller (e.g. a slice-stride IMULQ) would
* land in slot+16 / slot+24. (Task #18.) */
int tag = cg_tag_for_variant(rt, vt);
int rsz = (int)rt->size;
cgexpr(c, n->lhs, *locals);
if (type_isslice(vt)) {
/* cgexpr leaves (AX=ptr, BX=len,
* CX=cap). Move into the return
* shuffle: DX=ptr, CX=len, R8=cap. */
ins2(c, A_MOVQ, areg(D_CX),
areg(D_R8));
ins2(c, A_MOVQ, areg(D_BX),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
} else if (type_isstr(vt)) {
/* str IS []u8: cgexpr leaves
* (AX=ptr, BX=len, CX=cap). Same
* shuffle as the slice arm above —
* DX=ptr, CX=len, R8=cap
* (#1/Phase 3). */
ins2(c, A_MOVQ, areg(D_CX),
areg(D_R8));
ins2(c, A_MOVQ, areg(D_BX),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
} else if (type_isfloat(vt)) {
/* #157: float variant. cgexpr left
* the value in X0, not AX; there is
* no MOVQ-xmm->gp encoding, so bridge
* X0->DX through a stack slot (same
* SUBQ/MOVSD/ADDQ idiom as the arg-
* push at cgen.c:5367). Zero the slot
* first so the f32 case (MOVSS writes
* only the low 4 bytes) leaves a
* deterministic high-4 — cs==ww byte-
* id, matching f64's MOVSD which fills
* all 8. The AX-independent spill also
* removes the stale-AX cs!=ww on
* multi-variant returns. */
int isf32 = type_isf32(vt);
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, A_MOVQ, aimm(0),
amem(D_SP, 0));
ins2(c, isf32 ? A_MOVSS : A_MOVSD,
areg(D_X0), amem(D_SP, 0));
ins2(c, A_MOVQ, amem(D_SP, 0),
areg(D_DX));
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
if (rsz > 16)
ins2(c, A_MOVQ, aimm(0),
areg(D_CX));
if (rsz > 24)
ins2(c, A_MOVQ, aimm(0),
areg(D_R8));
} else {
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
/* scalar fills DX only. Zero
* CX / R8 if dst slot covers
* slot+16 / slot+24. */
if (rsz > 16)
ins2(c, A_MOVQ, aimm(0),
areg(D_CX));
if (rsz > 24)
ins2(c, A_MOVQ, aimm(0),
areg(D_R8));
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
areg(D_AX));
} else {
/* Struct variant or tagged-subset:
* materialise the widened value in a
* scratch slot, then load AX/DX/CX/R8
* from the slot. Struct literal: field
* stores; struct ident: word copy;
* tagged subset: copy + tag remap.
* 4th word in R8 covers slice payload
* variants (slot >= 32B).
*
* Single-slot @retscr (#14): returns are
* terminal, so all retscr uses in this fn
* share one slot. Pre-fix per-site fresh
* allocation over-grew the frame by sz
* bytes per extra return. */
int sz = (int)rt->size;
int scr;
if (cg_retscr != 0) {
scr = cg_retscr;
} else {
/* Fixed "@retscr" SSoT name —
* mirrors wwstage's localadd
* @-prefix dedup. Pre-fix
* mklabel(c, "retscr") consumed
* one labelseq counter slot per
* function with a tagged return,
* pushing every subsequent ct/ce/
* else/end label 1 ahead of
* wwstage. Site 1 sentinel
* masked by latent struct-widen
* offset divergence (#20/#21);
* fix is preventive symmetry per
* rule 10. */
scr = local_alloc(c, locals,
"@retscr", sz, cg_frame);
cg_retscr = scr;
}
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + k));
cg_widen_tagged_store(c, locals, rt,
n->lhs, D_BP, scr, sz);
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
areg(D_AX));
if (sz > 8)
ins2(c, A_MOVQ,
amem(D_BP, scr + 8),
areg(D_DX));
if (sz > 16)
ins2(c, A_MOVQ,
amem(D_BP, scr + 16),
areg(D_CX));
if (sz > 24)
ins2(c, A_MOVQ,
amem(D_BP, scr + 24),
areg(D_R8));
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
}
/* sret return (#23): plain TY_STRUCT >24B. Callee writes
* the value through `*(@sretarg)` (the caller-prealloc
* dest passed in RDI at entry; saved to @sretarg in the
* prologue), then loads @sretarg into RAX and rets — the
* SysV sret discipline of "return the pointer". No
* AX/DX/CX shuffle, no scratch slot beyond @sretarg. */
if (n->lhs && cg_ret_type && cg_sret_arg_off != 0) {
/* type_chase_named (#22). A single peel left `rt` still
* TY_NAMED when the declared return type is `type b
* = a;` where a is itself a NAMED alias of a struct,
* so the TY_STRUCT gate below missed and the sret
* return arm fell through to the scalar-AX default —
* corrupting the caller's receive slot even though
* the prologue wired @sretarg. */
Type *rt = type_chase_named(cg_ret_type);
/* sret return-forwarding (task #9 follow-up to #23,
* generalised for #10 Fold B): `return f();` where outer
* + inner both return the same sret shape (>24B struct OR
* over-cap tuple — gate keys cg_sret_retsize, not a kind).
* Outer's @sretarg already
* holds its caller's prealloc dest; pass it to inner
* in RDI (set by cgcall via cg_sret_forward), inner
* writes directly there, inner's RAX (dest pointer)
* is already outer's return value. The trailing
* MOVQ @sretarg(BP), AX is redundant after inner's
* RET but kept for byte-id symmetry with the
* N_IDENT / N_STRUCTLIT arms below. */
if (cg_sret_retsize(rt) > 0
&& n->lhs->kind == N_CALL) {
cg_sret_forward = 1;
cgexpr(c, n->lhs, *locals);
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
if (rt && (rt->kind == TY_STRUCT || rt->kind == TY_ARRAY)
&& (int)rt->size > 24
&& (n->lhs->kind == N_IDENT
|| n->lhs->kind == N_STRUCTLIT
|| n->lhs->kind == N_ARRLIT
|| n->lhs->kind == N_DOT
|| n->lhs->kind == N_INDEX
|| (n->lhs->kind == N_UN
&& n->lhs->op == TK_STAR))) {
/* Natural size = max(foff + fsz) over declared
* fields; mirrors selfhost cgenutil.ww
* structnaturalsize / sretretsize. Pre-fix this
* used the slot-padded rt->size, so a trailing
* narrow field (e.g. bool@32 in a 33B struct
* padded to 40B) widened to an 8B MOVQ at the
* loop tail — diverged from wwstage's MOVB
* tail. Task #33, Class A. An array (#267) has no
* fields; its natural size IS rt->size. */
int sz = 0;
if (rt->kind == TY_ARRAY) {
sz = (int)rt->size;
} else for (Tfield *fl = rt->fields; fl; fl = fl->next) {
int end = (int)fl->offset + (int)(fl->type
? fl->type->size : 8);
if (end > sz) sz = end;
}
if (n->lhs->kind == N_STRUCTLIT) {
/* Delegate to the shared *-relative
* fill helper. Same store sequence the
* ≤24B path emits, but the base reg is
* reloaded from @sretarg(BP) before each
* field store. Mirrors DST_PTR_LOCAL
* usage at N_ASSIGN N_DOT via_ptr. */
cg_structlit_fill(c, locals, rt,
n->lhs, DST_PTR_LOCAL,
cg_sret_arg_off, NULL, 0);
} else if (n->lhs->kind == N_IDENT) {
/* N_IDENT: word-copy from rhs slot to
* *(@sretarg). Whole 8B words via MOVQ;
* trailing partial words via MOVL/MOVB
* so the read stays inside the source
* slot's declared size. */
int rhsoff = localfind(*locals,
n->lhs->str);
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_BX));
int k = 0;
while (k + 8 <= sz) {
ins2(c, A_MOVQ,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, k));
k += 8;
}
while (k + 4 <= sz) {
ins2(c, A_MOVL,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BX, k));
k += 4;
}
while (k < sz) {
ins2(c, A_MOVB,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BX, k));
k += 1;
}
} else if (n->lhs->kind == N_ARRLIT) {
/* #272/#276: a >24B array-literal return has
* no consumer and the ptr-relative element fill
* is untested. Loud-stop (rule 7) rather than
* fall to the scalar default. ≤24B is wired. */
fatal("#272/#276: >24B array-literal return "
"unsupported (rule 7, no consumer)");
} else {
/* #272: N_DOT / N_INDEX / deref — land the
* source ADDRESS in SI FIRST (aggarg_srcaddr
* clobbers BX on its N_INDEX spine), THEN
* reload the dest ptr from @sretarg into BX
* and memcpy sz bytes — same #265/#268 copy
* shape as the ≤24B arm. Loud-stop any source
* the helper can't address. */
if (!aggarg_srcaddr(c, n->lhs, D_SI, *locals))
fatal("#272: aggregate return from "
"unsupported source kind %d",
n->lhs->kind);
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_BX));
int k = 0;
while (k + 8 <= sz) {
ins2(c, A_MOVQ, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BX, k));
k += 8;
}
while (k + 4 <= sz) {
ins2(c, A_MOVL, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BX, k));
k += 4;
}
while (k < sz) {
ins2(c, A_MOVB, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BX, k));
k += 1;
}
}
/* sret return: RAX = dest pointer. */
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off), areg(D_AX));
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
}
/* Whole-struct return for sizes ≤24B. ABI: AX=bytes[0..7],
* DX=bytes[8..15], CX=bytes[16..23]. Sizes >24B route
* through the sret arm above. Materialise rhs into a
* zero-padded 24B scratch slot, then emit AX/DX/CX loads
* unconditionally so the instruction shape is constant
* regardless of declared struct size. The receive side
* masks via the dst slot's declared size. Two rhs shapes
* are wired: N_IDENT (word-copy from rhs local slot) and
* N_STRUCTLIT (field-by-field store at scratch+foff). Call-
* result chain return is deferred to #5's receive side. */
if (n->lhs && cg_ret_type) {
/* type_chase_named (#22); see the >24B sret arm above
* for the same rationale. The ≤24B register-return
* ABI uses the same TY_STRUCT gate. */
Type *rt = type_chase_named(cg_ret_type);
/* #272: aggregate-return source-shape closure. Beyond the
* #267 N_IDENT/N_STRUCTLIT pair, every OTHER addressable
* aggregate rvalue (`return [..]` N_ARRLIT, `return o.f`
* N_DOT, `return a[i]` N_INDEX, `return *p` deref) fell to
* the scalar-AX default below = silent truncation. Funnel
* them through the SAME @retscr materialise the arg side
* closed in #271 (aggarg_srcaddr). N_CALL still passes
* through the tail (the callee already left AX/DX/CX). */
if (rt && (rt->kind == TY_STRUCT || rt->kind == TY_ARRAY)
&& rt->size <= 24
&& (n->lhs->kind == N_IDENT
|| n->lhs->kind == N_STRUCTLIT
|| n->lhs->kind == N_ARRLIT
|| n->lhs->kind == N_DOT
|| n->lhs->kind == N_INDEX
|| (n->lhs->kind == N_UN
&& n->lhs->op == TK_STAR))) {
int sz = (int)rt->size;
/* Single-slot @retscr (#14): see tagged arm
* above for rationale. Fixed "@retscr" name
* avoids bumping labelseq; mirrors wwstage's
* localadd @-prefix dedup. */
int scr;
if (cg_retscr != 0) {
scr = cg_retscr;
} else {
scr = local_alloc(c, locals, "@retscr",
24, cg_frame);
cg_retscr = scr;
}
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + 0));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + 8));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + 16));
if (n->lhs->kind == N_STRUCTLIT) {
/* Delegate to the shared BP-relative
* fill helper. Same store sequence the
* inline pre-#17 walk emitted, plus
* nested struct-typed structlit values
* recurse instead of dropping the
* trailing bytes. */
cg_structlit_fill_bp(c, locals, rt,
n->lhs, scr);
} else if (n->lhs->kind == N_ARRLIT) {
/* #272: materialise the array literal into
* @retscr per element, mirroring the
* let-init N_ARRLIT scalar/float fill
* (cgen.c N_LET). Non-scalar elements
* (struct/array/str/slice) loud-stop: no
* return-by-value consumer exists (rule 7),
* and the let-init path already covers them
* for the addressable forms. */
Type *esub = rt->sub;
int esz = esub ? (int)esub->size : 1;
Type *esubu = type_chase_named(esub);
if ((esubu && (esubu->kind == TY_STRUCT
|| esubu->kind == TY_ARRAY
|| esubu->kind == TY_TUPLE))
|| type_isstr(esub)
|| type_isslice(esub))
fatal("#272: array-literal return "
"with non-scalar element "
"unsupported (rule 7, no "
"consumer)");
int isfl = type_isfloat(esub);
int fmov = type_isf32(esub) ? A_MOVSS
: A_MOVSD;
int op = A_MOVQ;
if (esz == 1) op = A_MOVB;
else if (esz == 2) op = A_MOVW;
else if (esz == 4) op = A_MOVL;
int idx = 0;
Node *last = NULL;
int repeat = 0;
for (Node *e = n->lhs->list; e;
e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
cgexpr(c, e, *locals);
if (isfl)
ins2(c, fmov, areg(D_X0),
amem(D_BP,
scr + idx * esz));
else
ins2(c, op, areg(D_AX),
amem(D_BP,
scr + idx * esz));
last = e;
idx++;
}
if (repeat && last)
while (idx < (int)rt->alen) {
if (isfl)
ins2(c, fmov,
areg(D_X0),
amem(D_BP,
scr + idx * esz));
else
ins2(c, op,
areg(D_AX),
amem(D_BP,
scr + idx * esz));
idx++;
}
} else if (n->lhs->kind == N_IDENT) {
/* N_IDENT: word-copy rhs slot into
* scratch. Whole 8B words via MOVQ;
* trailing partial word via MOVL/MOVB
* so we read no further than the
* source slot's declared size. */
int rhsoff = localfind(*locals,
n->lhs->str);
int k = 0;
while (k + 8 <= sz) {
ins2(c, A_MOVQ,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + k));
k += 8;
}
while (k + 4 <= sz) {
ins2(c, A_MOVL,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BP, scr + k));
k += 4;
}
while (k < sz) {
ins2(c, A_MOVB,
amem(D_BP, rhsoff + k),
areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BP, scr + k));
k += 1;
}
} else {
/* #272: N_DOT / N_INDEX / deref — land the
* source ADDRESS in SI via the #271 arg-side
* helper, then memcpy sz bytes into @retscr
* (the #265/#268 let-init copy shape). Loud-
* stop any source the helper can't address
* (rule 7); the gate above already excludes
* N_CALL (tail passthrough). */
if (!aggarg_srcaddr(c, n->lhs, D_SI,
*locals))
fatal("#272: aggregate return "
"from unsupported source "
"kind %d", n->lhs->kind);
int k = 0;
while (k + 8 <= sz) {
ins2(c, A_MOVQ, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, scr + k));
k += 8;
}
if (k + 4 <= sz) {
ins2(c, A_MOVL, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BP, scr + k));
k += 4;
}
if (k + 2 <= sz) {
ins2(c, A_MOVW, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVW, areg(D_AX),
amem(D_BP, scr + k));
k += 2;
}
if (k + 1 <= sz) {
ins2(c, A_MOVB, amem(D_SI, k),
areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BP, scr + k));
k += 1;
}
}
/* #171a: float-bearing struct RETURN (the return
* twin of #165's param recv). A qualifying struct's
* float eightbytes ride the SSE return row (X0,X1 =
* tuple_sse_seq), its INT eightbytes the INTEGER
* return row (AX,DX = tuple_rseq), on INDEPENDENT
* cursors per SysV (ref/qbe/amd64/sysv.c retr) — so a
* float lands in the next XMM regardless of its
* positional eightbyte (struct{f64,i32}: e0→X0, e1→AX,
* NOT DX). The scr slot is zero-padded to 24B, so a
* full MOVQ on a trailing INT eightbyte reads no
* garbage — the #169 sized tail is a RECV-only concern.
* struct_float_class gates to qualifying structs (>=1
* f64, every eightbyte lone-f64 or pure-INT); all-int +
* f32 keep the AX/DX/CX transport (byte-id / #171b). */
int sclass[2], snb;
if ((snb = struct_float_class(rt, sclass)) > 0) {
int gpcur = 0, ssecur = 0;
for (int e = 0; e < snb; e++) {
if (sclass[e]) {
ins2(c, A_MOVSD,
amem(D_BP, scr + e * 8),
areg(tuple_sse_seq[ssecur]));
ssecur++;
} else {
ins2(c, A_MOVQ,
amem(D_BP, scr + e * 8),
areg(tuple_rseq[gpcur]));
gpcur++;
}
}
} else {
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, scr + 8),
areg(D_DX));
ins2(c, A_MOVQ, amem(D_BP, scr + 16),
areg(D_CX));
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
}
/* #272 close-by-construction: the addressable aggregate-return
* sources (IDENT/STRUCTLIT/ARRLIT/DOT/INDEX/deref) all route
* through the @retscr / *(@sretarg) arms above and break; an
* aggregate N_CALL passes through the cgexpr tail (the callee
* already left AX/DX/CX). Any OTHER aggregate rvalue reaching
* here would truncate to AX silently — loud-stop (rule 7) so a
* future unhandled shape is caught, not miscompiled. */
if (n->lhs && cg_ret_type) {
Type *rtc = type_chase_named(cg_ret_type);
if (rtc && (rtc->kind == TY_STRUCT || rtc->kind == TY_ARRAY)
&& n->lhs->kind != N_CALL)
fatal("#272: aggregate return reaches scalar default "
"(source kind %d) — unclosed shape",
n->lhs->kind);
}
if (n->lhs && node_isstr(n->lhs)) {
/* str IS []u8: AX=ptr, BX=len, CX=cap from cgexpr —
* no AX:DX shuffle, same as a slice (#1/Phase 3). */
cgexpr(c, n->lhs, *locals);
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
if (n->lhs && n->lhs->kind == N_TUPLE) {
/* #83 / #164 (#107): positional register-return over a SysV
* dual class cursor. Each element rides its SysV class
* (harec create_unpack_bindings, ref/harec/src/check.c:1354-
* 1416): a float takes one SSE eightbyte (X0,X1 = tuple_sse_
* seq), everything else INTEGER eightbytes over tuple_rseq —
* a slice/str its 3-word {ptr,len,cap} header (ref/hare/rt/
* ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a scalar 1 word
* in AX. Integer words spill L→R to the stack and pop into the
* INTEGER cursor in reverse so positional slot i lands in
* tuple_rseq[i] (byte-id with #83 when no float is present).
* Each float must spill X0 to @tupfscr as we walk, because a
* later element's cgexpr clobbers X0; after the integer pops
* the saved floats reload into X0/X1 by SSE index — INDEPENDENT
* of the integer cursor (ref/qbe/amd64/sysv.c retr L95-108).
* Both rows are loud-stopped at their cap (rule-7, never a
* silent collide): INTEGER 4, SSE 2. The SAME class split
* drives the receive sites. */
int ssecap = TUPLE_SSECAP;
int gptotal = 0, ssecount = 0, f32;
/* #57: count + push key on the DECLARED return-type
* element (cg_ret_type tuple params) — the literal's
* stamped type is element-constructed, so a declared-
* TAGGED element's concrete rvalue counted 1 word and
* skipped the widen while the caller's receive walks
* the declared eslot (2 words sent for a 3-word shape;
* ken /tmp/ken57 p8/p9). Same pp walk the over-cap arm
* already does (#240/#22b). */
Type *rttc = cg_ret_type
? type_chase_named(cg_ret_type) : NULL;
Tparam *rp0 = (rttc && rttc->kind == TY_TUPLE)
? rttc->params : NULL;
Tparam *rp = rp0;
for (Node *e = n->lhs->list; e; e = e->next) {
Type *rdu = rp
? type_chase_named(rp->type) : NULL;
int rdtag = rdu && rdu->kind == TY_TAGGED;
if (!rdtag && fld_isfloat(e->type, &f32))
ssecount++;
else
gptotal += tuple_lit_gpwords(e,
rp ? rp->type : NULL);
if (rp) rp = rp->next;
}
/* #22b: classify and emit MUST agree (the #10 SSoT
* note at TUPLE_GPCAP). The over-cap DECISION rides
* cg_sret_retsize on the DECLARED return type — the
* same predicate the prologue (@sretarg) and the
* caller key on. The expr-shape count above only
* pairs the in-cap push/pop: a declared-tagged
* element whose expr is the unwidened payload counts
* 1 word here vs 2+ declared eightbytes, so the emit
* took the register path against an sret-classified
* caller — silent garbage, both stages, gate-blind
* (probe /tmp/i22b/p2). */
int overcap = cg_ret_type != NULL
? cg_sret_retsize(cg_ret_type) > 0
: (gptotal > TUPLE_GPCAP || ssecount > ssecap);
if (overcap) {
/* #10 Fold A: over-cap tuple returns via sret. The
* prologue wired @sretarg (cg_sret_retsize agrees on
* the caps — the shared SSoT), holding the caller-
* prealloc dest. Store each element through
* *(@sretarg) at its packed layout offset (running
* sum of element sizes — the t.0/t.1 positional
* layout, N_DOT TY_TUPLE arm), each at its natural
* width so a narrow tail doesn't over-MOVQ (#169);
* the dest base is reloaded into DX each step since a
* wide element's cgexpr clobbers AX/BX/CX. Then reuse
* the struct-sret epilogue. The CALL/receive side
* stays loud-stopped (#10 Fold B). */
/* #240: foff advances by the DECLARED return-type
* element size (cg_ret_type tuple params), NOT the
* literal expression's type. A bare int literal
* element is stamped TY_UNTYPED_INT (size 0), so
* `e->type->size` collapsed foff to 0 for a leading
* scalar — the next element then clobbered it at
* offset 0 and every trailing element packed 8 bytes
* low, diverging from the t.N reader (f->offset) and
* from wwstage (cgenstmt.ww walks c.fnret.list). */
Type *rtt = type_chase_named(cg_ret_type);
Tparam *pp = (rtt && rtt->kind == TY_TUPLE)
? rtt->params : NULL;
int foff = 0;
for (Node *e = n->lhs->list; e; e = e->next) {
Type *du = pp
? type_chase_named(pp->type) : NULL;
if (du && du->kind == TY_TAGGED) {
/* #22b (task #28): MEMORY-class
* tagged element — the whole box
* copies through the sret pointer
* mem-to-mem from the element's
* local slot. cgexpr can't source
* it: the tagged ident load is
* word0-only (every tagged
* consumer reads memory) and the
* AX/DX/CX/R8 box cursor would
* collide with the DX dest-base
* reload. Ident-only, mirroring
* tuple_lit_push_elem; widening /
* non-ident sources stay loud
* (#23/#40 follow-ups). */
int eslot = tuple_eslot(pp->type);
Type *eu = type_chase_named(e->type);
int eoff = 0;
if (e->kind == N_IDENT && e->str
&& eu && eu->kind == TY_TAGGED
&& tuple_eslot(e->type) == eslot)
eoff = localfind(*locals,
e->str);
if (eoff == 0)
fatal("#22b: tagged element "
"in an over-cap (sret) "
"tuple return from a "
"non-ident or widening "
"source unwired (ident "
"locals only; rule 7; "
"call-source is task "
"#41, widening #23/#40)");
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_DX));
for (int k = 0; k < eslot; k += 8) {
ins2(c, A_MOVQ,
amem(D_BP, eoff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, foff + k));
}
foff += eslot;
pp = pp->next;
continue;
}
int isflt = fld_isfloat(e->type, &f32);
int wide = node_isstr(e) || node_isslice(e);
int esz = 8;
if (pp && pp->type)
esz = (int)pp->type->size;
else if (e->type)
esz = (int)e->type->size;
cgexpr(c, e, *locals);
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off), areg(D_DX));
if (isflt)
ins2(c, f32 ? A_MOVSS : A_MOVSD,
areg(D_X0), amem(D_DX, foff));
else if (wide) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_DX, foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, foff + 16));
} else
ins2(c, fldstoreop(e->type, esz),
areg(D_AX), amem(D_DX, foff));
/* C-t0/#22: the sret buffer is slot-laid
* like every tuple home (checker size,
* t.N reader, mlet receive agree) — the
* stride is THE accessor's (a declared
* void element's 0-slot included; the
* old wide?esz:8 advanced 8 where every
* receive walks 0). esz keeps the store
* WIDTH natural. */
foff += tuple_eslot(pp ? pp->type
: e->type);
if (pp) pp = pp->next;
}
ins2(c, A_MOVQ, amem(D_BP, cg_sret_arg_off),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
/* rule-7 net: register-classified by the declared type
* but the expr-shape count overflows the cursor — the
* pops below would index past tuple_rseq. Unreachable
* while expr counts never exceed declared counts;
* loud, not OOB, if a future shape breaks that. */
if (gptotal > TUPLE_GPCAP || ssecount > ssecap)
fatal("register-classified tuple return "
"exceeds the cursor (classify/emit skew; "
"rule 7, #22b)");
int fscr = 0;
if (ssecount > 0) {
if (cg_tupfscr != 0)
fscr = cg_tupfscr;
else {
fscr = local_alloc(c, locals, "@tupfscr",
ssecap * 8, cg_frame);
cg_tupfscr = fscr;
}
}
int sseidx = 0;
rp = rp0;
for (Node *e = n->lhs->list; e; e = e->next) {
Type *rdu = rp
? type_chase_named(rp->type) : NULL;
int rdtag = rdu && rdu->kind == TY_TAGGED;
int isflt = !rdtag
&& fld_isfloat(e->type, &f32);
if (isflt) {
cgexpr(c, e, *locals); /* float=X0 */
ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0),
amem(D_BP, fscr + sseidx * 8));
sseidx++;
} else {
/* scalar=AX; slice/str=AX,BX,CX; tagged
* box from its slot or widened scratch
* (tuple_lit_push_elem) */
tuple_lit_push_elem(c, locals, e,
rp ? rp->type : NULL);
}
if (rp) rp = rp->next;
}
for (int i = gptotal - 1; i >= 0; i--)
ins1(c, A_POPQ, areg(tuple_rseq[i]));
int j = 0;
rp = rp0;
for (Node *e = n->lhs->list; e; e = e->next) {
Type *rdu = rp
? type_chase_named(rp->type) : NULL;
int rdtag = rdu && rdu->kind == TY_TAGGED;
if (!rdtag && fld_isfloat(e->type, &f32)) {
ins2(c, f32 ? A_MOVSS : A_MOVSD,
amem(D_BP, fscr + j * 8),
areg(tuple_sse_seq[j]));
j++;
}
if (rp) rp = rp->next;
}
} else if (n->lhs) {
cgexpr(c, n->lhs, *locals);
} else {
cgexpr_int(c, 0);
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
case N_IF: {
char *els = mklabel(c, "else");
char *end = mklabel(c, "end");
cgexpr(c, n->cond, *locals);
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JE, abranch(n->els ? els : end));
cgstmt(c, n->body, locals, frame);
if (n->els) {
ins1(c, A_JMP, abranch(end));
label(c, els);
cgstmt(c, n->els, locals, frame);
}
label(c, end);
break;
}
case N_FORRANGE: {
/* Lower `for (let x .. s) body` (and its tuple-destructure
* cousin `for (let (a, b) .. s)`). The body is wrapped in a
* counted loop driven by stack-spilled `_i`/`_len`. Each
* iteration computes the element address `s.ptr + i*esz`
* and either loads the whole element into the named local
* or pulls each tuple field into its respective local. */
Node *slc = n->lhs;
Type *st = slc ? slc->type : NULL;
/* Transitive chase (#5 alias arc): the checker admits range
* bases through 2-level alias chains (F0 8b); the single peel
* left u TY_NAMED → esz=1 + the non-array base arm (MOVQ of
* array words as a pointer — SEGV). */
Type *u = type_chase_named(st);
int esz = (u && u->sub) ? (int)u->sub->size : 1;
Type *etu = type_chase_named(u ? u->sub : NULL);
int destruct = (n->list != NULL);
/* allocate temp slots: _i (8B), _len (8B). #70: a NON-IDENT
* slice/str base (field chain, indexed element, call) also
* needs a _base spill — pre-#70 the init stored cgexpr's AX
* (the DATA POINTER — a slice-valued cgexpr leaves AX=ptr,
* BX=len, CX=cap) into _len, and the per-iteration code had
* no non-ident base arm at all, so the bound-reload BX
* doubled as the base: i was compared against the POINTER
* and walked off the end (regex.finish, SEGV on the first
* non-empty charsets; empty slices coincidentally exited on
* ptr==0 — latent since fold 1, byte-id both stages). */
char *iname = aprintf(c->a, ".rgi_%d", c->labelseq++);
char *lname = aprintf(c->a, ".rgl_%d", c->labelseq++);
int ioff = localoff(c, locals, iname, 8, frame);
int loff = localoff(c, locals, lname, 8, frame);
/* #11: cgexpr on a slice DEREF (*p) does not deliver the
* AX/BX/CX header convention the spill below assumes (the
* deref-spine load family) — pre-#70 this shape crashed or
* mis-summed; keep it LOUD until #11 wires the deref load. */
if (slc && slc->kind == N_UN && slc->op == TK_STAR
&& u && (u->kind == TY_SLICE || u->kind == TY_STR))
fatal("for-range over a deref base unwired (#11)");
int baseoff = 0;
if (slc && slc->kind != N_IDENT
&& !(u && u->kind == TY_ARRAY)) {
char *bname = aprintf(c->a, ".rgb_%d", c->labelseq++);
baseoff = localoff(c, locals, bname, 8, frame);
}
/* allocate per-name slots */
struct { int off, sz, foff; Type *ftype; } binds[8] = {0};
int nbinds = 0;
if (destruct) {
Tparam *tp = (etu && etu->kind == TY_TUPLE) ?
etu->params : NULL;
int field_off = 0;
for (Node *nm = n->list; nm && nbinds < 8; nm = nm->next) {
int fsz = tp && tp->type ? (int)tp->type->size : 8;
int slot_sz = (fsz < 8) ? 8 : fsz;
binds[nbinds].sz = fsz;
binds[nbinds].foff = field_off;
binds[nbinds].ftype = tp ? tp->type : NULL;
binds[nbinds].off = localoff(c, locals,
nm->str, slot_sz, frame);
field_off += fsz;
nbinds++;
if (tp) tp = tp->next;
}
} else {
int slot_sz = (esz < 8) ? 8 : esz;
binds[0].off = localoff(c, locals, n->str, slot_sz, frame);
binds[0].sz = esz;
binds[0].foff = 0;
binds[0].ftype = u ? u->sub : NULL;
nbinds = 1;
}
ins2(c, A_MOVQ, aimm(0), amem(D_BP, ioff));
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
&& slc->kind == N_IDENT) {
int boff = localfind(*locals, slc->str);
ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff));
} else if (u && u->kind == TY_ARRAY) {
/* #70: a non-ident ARRAY base has no base spill and
* its cgexpr register shape is not the slice header —
* the per-iteration base would be garbage. Loud (rule
* 7) until a consumer wires it. */
if (slc->kind != N_IDENT)
fatal("for-range over a non-ident array base "
"unwired (#70)");
ins2(c, A_MOVQ, aimm((long long)u->alen),
amem(D_BP, loff));
} else {
cgexpr(c, slc, *locals);
if (baseoff != 0) {
/* #70: slice/str header from cgexpr is AX=ptr,
* BX=len, CX=cap — bound is LEN; spill the base
* ptr for the per-iteration element address. */
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, loff));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, baseoff));
} else {
/* ident with unresolved type — legacy path,
* unchanged (per-iteration base loads the
* ident's own slot). */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff));
}
}
char *loop = mklabel(c, "rloop");
char *end = mklabel(c, "rend");
char *natural_exit = end;
if (n->els) natural_exit = mklabel(c, "relseloop");
/* #138 (range form): `continue` must run the implicit `i+=1`
* post-step before re-testing the loop bound. Pre-fix the
* cont-target was `loop` (top), skipping the ADDQ $1, ioff
* below the body — infinite loop on the value that triggered
* continue. Dedicated `rpost` label; bootstrap-NEUTRAL (no
* range-form continue callers in lib/ or selfhost/). */
char *rpost = mklabel(c, "rpost");
if (nloops < LOOP_MAX) {
loop_cont[nloops] = rpost;
loop_brk[nloops] = end;
nloops++;
}
label(c, loop);
ins2(c, A_MOVQ, amem(D_BP, ioff), areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, loff), areg(D_BX));
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
ins1(c, A_JGE, abranch(natural_exit));
/* compute element base: s.ptr + i*esz → BX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
if (slc->kind == N_IDENT && u && u->kind == TY_ARRAY) {
int boff = localfind(*locals, slc->str);
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX));
} else if (slc->kind == N_IDENT) {
int boff = localfind(*locals, slc->str);
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
} else {
/* #70: non-ident slice/str base — reload the spilled
* data pointer (pre-#70 BX held the bound reload). */
ins2(c, A_MOVQ, amem(D_BP, baseoff), areg(D_BX));
}
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
/* load each binding from BX + foff into its slot. C4 (F5/FC0,
* task #7): a by-value AGGREGATE element (struct / tuple /
* str/slice header, esz > 8) copies its FULL extent — the
* single fldloadop word truncated it to 8B, so every field
* past word 0 (str/slice .len/.cap included) read stale slot
* bytes (regex.finish's 24B charset binding, gate-blind
* cs≠ww). Same word-run + sized-tail idiom as the N_LET
* aggregate copy. */
if (!destruct && esz > 8) {
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, binds[0].off + k));
}
if (k + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX),
amem(D_BP, binds[0].off + k));
k += 4;
}
if (k + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX),
amem(D_BP, binds[0].off + k));
k += 2;
}
if (k + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX),
amem(D_BP, binds[0].off + k));
k += 1;
}
} else {
for (int b = 0; b < nbinds; b++) {
int op = fldloadop(binds[b].ftype, binds[b].sz);
ins2(c, op, amem(D_BX, binds[b].foff),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, binds[b].off));
}
}
cgstmt(c, n->body, locals, frame);
label(c, rpost);
ins2(c, A_ADDQ, aimm(1), amem(D_BP, ioff));
ins1(c, A_JMP, abranch(loop));
if (n->els) {
label(c, natural_exit);
cgstmt(c, n->els, locals, frame);
}
label(c, end);
if (nloops > 0) nloops--;
break;
}
case N_FOR: {
char *loop = mklabel(c, "loop");
char *end = mklabel(c, "endloop");
/* `else` runs at normal cond-false exit; break skips it.
* Separate the natural exit label from the break target so
* the else block sits between them. */
char *natural_exit = end;
if (n->els) natural_exit = mklabel(c, "elseloop");
/* #138: `continue` in a 3-clause `for (init; cond; post)` must
* run the post-step before re-testing cond. Pre-fix the
* continue-target was `loop` (top), which SKIPPED post → state
* never advanced → infinite loop. Allocate a dedicated `post`
* label only when there IS a post-step (`n->rhs`); else keep
* continue → loop-top, byte-id with 1-clause for. */
char *cont_target = loop;
if (n->rhs) cont_target = mklabel(c, "post");
if (n->lhs) cgstmt(c, n->lhs, locals, frame);
label(c, loop);
if (n->cond) {
cgexpr(c, n->cond, *locals);
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
ins1(c, A_JE, abranch(natural_exit));
}
if (nloops < LOOP_MAX) {
loop_cont[nloops] = cont_target;
loop_brk[nloops] = end;
nloops++;
}
cgstmt(c, n->body, locals, frame);
if (nloops > 0) nloops--;
if (n->rhs) {
label(c, cont_target);
cgexpr(c, n->rhs, *locals);
}
ins1(c, A_JMP, abranch(loop));
if (n->els) {
label(c, natural_exit);
cgstmt(c, n->els, locals, frame);
}
label(c, end);
break;
}
case N_MLET: {
/* #83 / #164 (#107): positional per-element destructure store.
* The rhs left each tuple element in its SysV-class register
* (see N_RETURN / harec create_unpack_bindings, ref/harec/src/
* check.c:1354-1416); walk the bindings over the SAME dual
* cursor and store each at its own width — a slice/str's 3-word
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8) into a
* header-sized slot (sized from u->size so #1 propagates), a
* float from X0/X1 (SSE cursor), a scalar's 1 word from the
* INTEGER cursor into an 8B slot. Both rows loud-stop at their
* cap. */
/* #10 Fold B: over-cap tuple destructure RECEIVE. The callee
* sret'd the whole tuple into the @sretscr discard slot (cgcall
* sees cg_sret_retsize > 0, no lvalue dest wired). Copy each
* element out to its binding slot at the SAME packed offset the
* SEND wrote (foff += element size — the t.0/t.1 layout), each
* at its NATURAL width (#169). The receive has no single lvalue
* dest, so it reuses the same per-fn @sretscr slot a discarded
* sret call would; the in-reg path below is unchanged. */
/* #242: rhs is a tuple already materialised in a local slot (a
* match-bound union payload, `let (a,b)=t`), NOT a register-
* returning call. cgexpr(tuple ident) loads only word0->AX, so
* the register-cursor path below reads DX/CX stale. Copy each
* element from the ident's slot at the register-ABI 8B stride
* (24B for a slice/str header) — the SAME layout the tagged
* construct + match payload-bind write. */
if (n->rhs && n->rhs->kind == N_IDENT) {
Type *rty = type_chase_named(n->rhs->type);
if (rty && rty->kind == TY_TUPLE) {
int srcoff = localfind(*locals, n->rhs->str);
int lf32b;
int foff = 0;
for (Node *l = n->list; l; l = l->next) {
Type *t = l->type;
int eslot = tuple_eslot(t);
int isflt = fld_isfloat(t, &lf32b);
int esz = t ? (int)t->size : 8;
int off = localoff(c, locals, l->str,
eslot > 8 ? eslot : 8, frame);
if (isflt) {
ins2(c, lf32b ? A_MOVSS : A_MOVSD,
amem(D_BP, srcoff + foff),
areg(D_X0));
ins2(c, lf32b ? A_MOVSS : A_MOVSD,
areg(D_X0), amem(D_BP, off));
} else if (eslot > 8) {
for (int k = 0; k < eslot; k += 8) {
ins2(c, A_MOVQ,
amem(D_BP, srcoff + foff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k));
}
} else {
ins2(c, fldloadop(t, esz),
amem(D_BP, srcoff + foff),
areg(D_AX));
ins2(c, fldstoreop(t, esz),
areg(D_AX), amem(D_BP, off));
}
foff += eslot;
}
break;
}
}
int sret_recv = (n->rhs && n->rhs->kind == N_CALL)
? cg_sret_retsize(n->rhs->type) : 0;
cgexpr(c, n->rhs, *locals);
int lf32;
if (sret_recv > 0) {
int scr = cg_sretscr_off;
int foff = 0;
for (Node *l = n->list; l; l = l->next) {
Type *t = l->type;
int eslot = tuple_eslot(t);
int isflt = fld_isfloat(t, &lf32);
int esz = t ? (int)t->size : 8;
int off = localoff(c, locals, l->str,
eslot > 8 ? eslot : 8, frame);
if (isflt) {
ins2(c, lf32 ? A_MOVSS : A_MOVSD,
amem(D_BP, scr + foff), areg(D_X0));
ins2(c, lf32 ? A_MOVSS : A_MOVSD,
areg(D_X0), amem(D_BP, off));
} else if (eslot > 8) {
for (int k = 0; k < eslot; k += 8) {
ins2(c, A_MOVQ,
amem(D_BP, scr + foff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k));
}
} else {
ins2(c, fldloadop(t, esz),
amem(D_BP, scr + foff), areg(D_AX));
ins2(c, fldstoreop(t, esz),
areg(D_AX), amem(D_BP, off));
}
/* C-t0: slot stride — must mirror the
* N_RETURN over-cap SEND's buffer layout. */
foff += eslot;
}
break;
}
int gpcap = TUPLE_GPCAP;
int ssecap = TUPLE_SSECAP;
int gptotal = 0, ssetotal = 0;
for (Node *l = n->list; l; l = l->next) {
if (fld_isfloat(l->type, &lf32))
ssetotal++;
else
gptotal += tuple_eslot(l->type) / 8;
}
if (gptotal > gpcap)
fatal("tuple destructure exceeds integer register-return "
"ABI capacity (%d eightbytes: AX,DX,CX,R8); "
"see return-ABI #10", gpcap);
if (ssetotal > ssecap)
fatal("tuple destructure exceeds SSE register-return ABI "
"capacity (%d eightbytes: X0,X1); see return-ABI #10",
ssecap);
int gpcur = 0, ssecur = 0;
for (Node *l = n->list; l; l = l->next) {
Type *t = l->type;
int eslot = tuple_eslot(t);
int isflt = fld_isfloat(t, &lf32);
int off = localoff(c, locals, l->str,
eslot > 8 ? eslot : 8, frame);
tuple_store(c, t, gpcur, ssecur, off);
if (isflt)
ssecur++;
else
gpcur += eslot / 8;
}
break;
}
case N_MASSIGN: {
/* #83: positional per-element destructure REASSIGN. Same cursor
* as N_MLET (and N_RETURN; harec create_unpack_bindings,
* ref/harec/src/check.c:1354-1416), but the slots already exist
* (reassignment) so localfind them. Element WIDTH comes from the
* rhs tuple's element types (n->rhs->type->params) — the SAME
* producer source the SEND site walks and wwstage reads via the
* callee return type — NOT the binding type: a `_` lvalue is an
* N_IDENT with empty str the checker never type-stamps (it skips
* cexpr on `_`, cmd/wcc/check.c N_MASSIGN), so a binding-typed
* width would mis-size a wide `_` and DESYNC the cursor for the
* next element. harec `_` skips the store but CONSUMES its tuple
* offset; the cursor advance below honours that. A wide element's
* 3-word {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8) is stored
* at its slot. This bare-comma `a, s = f()` multi-assign is a
* retained ww-EXTENSION beyond Hare (Hare tuple-unpack is binding-
* only); ww keeps the Go/rob-pike multi-assign idiom — rule-9
* carve-out. Over-capacity is a loud stop, not a silent drop. */
/* #10 Fold B: over-cap tuple destructure REASSIGN. Same sret
* copy-out as N_MLET but the slots already exist (localfind);
* a `_` / missing binding (off == 0) SKIPS its store yet still
* ADVANCES foff so the next element stays aligned (harec `_`).
* Element widths come from the rhs tuple's element types — the
* SAME producer source the SEND walks. */
int sret_recv = (n->rhs && n->rhs->kind == N_CALL)
? cg_sret_retsize(n->rhs->type) : 0;
/* #64 (filed, rule 7): an N_TUPLE literal rhs rides this
* decl-less cgexpr route, so a declared-TAGGED element's
* concrete rvalue still fills the cursor stamped-keyed
* (silent skew) — the #57 decl wire stops at return/let. */
cgexpr(c, n->rhs, *locals);
Type *rt = n->rhs ? n->rhs->type : NULL;
Type *ru = type_chase_named(rt);
Tparam *tp0 = (ru && ru->kind == TY_TUPLE) ? ru->params : NULL;
int mf32;
if (sret_recv > 0) {
int scr = cg_sretscr_off;
int foff = 0;
Tparam *tp = tp0;
for (Node *l = n->list; l; l = l->next) {
Type *et = tp ? tp->type : NULL;
int eslot = tuple_eslot(et);
int isflt = fld_isfloat(et, &mf32);
int esz = et ? (int)et->size : 8;
int off = (l->kind == N_IDENT)
? localfind(*locals, l->str) : 0;
if (off != 0) {
if (isflt) {
ins2(c, mf32 ? A_MOVSS : A_MOVSD,
amem(D_BP, scr + foff),
areg(D_X0));
ins2(c, mf32 ? A_MOVSS : A_MOVSD,
areg(D_X0), amem(D_BP, off));
} else if (eslot > 8) {
for (int k = 0; k < eslot; k += 8) {
ins2(c, A_MOVQ,
amem(D_BP, scr + foff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k));
}
} else {
ins2(c, fldloadop(et, esz),
amem(D_BP, scr + foff),
areg(D_AX));
ins2(c, fldstoreop(et, esz),
areg(D_AX), amem(D_BP, off));
}
}
/* C-t0: slot stride — must mirror the
* N_RETURN over-cap SEND's buffer layout. */
foff += eslot;
if (tp) tp = tp->next;
}
break;
}
int gpcap = TUPLE_GPCAP;
int ssecap = TUPLE_SSECAP;
int gptotal = 0, ssetotal = 0;
for (Tparam *tp = tp0; tp; tp = tp->next) {
if (fld_isfloat(tp->type, &mf32))
ssetotal++;
else
gptotal += tuple_eslot(tp->type) / 8;
}
if (gptotal > gpcap)
fatal("tuple destructure exceeds integer register-return "
"ABI capacity (%d eightbytes: AX,DX,CX,R8); "
"see return-ABI #10", gpcap);
if (ssetotal > ssecap)
fatal("tuple destructure exceeds SSE register-return ABI "
"capacity (%d eightbytes: X0,X1); see return-ABI #10",
ssecap);
int gpcur = 0, ssecur = 0;
Tparam *tp = tp0;
for (Node *l = n->list; l; l = l->next) {
Type *et = tp ? tp->type : NULL;
int isflt = fld_isfloat(et, &mf32);
int off = (l->kind == N_IDENT)
? localfind(*locals, l->str) : 0;
/* harec `_` (off==0): skip the store but CONSUME the
* cursor slot so the next element stays aligned. */
if (off != 0)
tuple_store(c, et, gpcur, ssecur, off);
if (isflt)
ssecur++;
else
gpcur += tuple_eslot(et) / 8;
if (tp) tp = tp->next;
}
break;
}
case N_DEFER:
if (ndefers < DEFER_MAX) {
defers[ndefers++] = n->lhs;
}
break;
case N_YIELD:
/* Evaluate the value into AX, then jump to the enclosing
* match's end label. str-typed yields land in (AX, BX);
* the consumer's let-init or call-arg site reads both. */
if (n->lhs) cgexpr(c, n->lhs, *locals);
if (nyields > 0)
ins1(c, A_JMP, abranch(yield_target[nyields - 1]));
break;
case N_BREAK:
if (nloops > 0)
ins1(c, A_JMP, abranch(loop_brk[nloops - 1]));
break;
case N_CONTINUE:
if (nloops > 0)
ins1(c, A_JMP, abranch(loop_cont[nloops - 1]));
break;
case N_SWITCH: {
/* Lower to a chain of compares. Scrutinee lands in a fresh
* local slot so case bodies can spill through SP without
* losing it. Cases are tried top-to-bottom; a `case:` arm
* with no exprs is the default and runs after all named
* arms fail. */
char *swname = aprintf(c->a, ".sw_%d", c->labelseq++);
int sloff = localoff(c, locals, swname, 8, frame);
cgexpr(c, n->lhs, *locals); /* AX = scrutinee */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sloff));
char *end = mklabel(c, "swend");
Node *defcase = NULL;
for (Node *cs = n->list; cs; cs = cs->next) {
if (cs->list == NULL) {
defcase = cs; /* save for last */
continue;
}
char *body = mklabel(c, "swcase");
char *next = mklabel(c, "swnext");
for (Node *e = cs->list; e; e = e->next) {
cgexpr(c, e, *locals); /* AX = case-expr */
ins2(c, A_MOVQ, amem(D_BP, sloff), areg(D_BX));
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
ins1(c, A_JE, abranch(body));
}
ins1(c, A_JMP, abranch(next));
label(c, body);
cgstmt(c, cs->body, locals, frame);
ins1(c, A_JMP, abranch(end));
label(c, next);
}
if (defcase)
cgstmt(c, defcase->body, locals, frame);
label(c, end);
break;
}
default:
break;
}
}
static void
cgfn(Cg *c, FILE *out, Node *fn)
{
if (fn->body == NULL) return; /* extern decl, no body */
/* fresh per-fn state */
c->head = c->tail = NULL;
c->fnname = fn->str;
c->cur_mod = (fn->module && fn->module[0]) ? fn->module : NULL;
c->labelseq = 0;
cg_stack_arg_cursor = 0;
ndefers = 0;
nloops = 0;
cg_ret_type = fn->type ? fn->type->ret : NULL;
cg_retscr = 0;
cg_tupfscr = 0;
cg_tupargscr = 0;
cg_tupargscr_sz = 0;
cg_aggargscr = 0;
cg_aggargscr_sz = 0;
cg_tagbase = 0;
cg_tagbase_sz = 0;
cg_ntagscr = 0;
cg_appendscr = 0;
cg_appendsroot = 0;
cg_appendsoff = 0;
cg_sret_arg_off = 0;
cg_sret_dest_off = 0;
cg_sret_dest_sym = NULL;
cg_sretscr_off = 0;
cg_sretscr_sz = 0;
cg_sret_forward = 0;
int frame = 0;
Local *locals = NULL;
cg_frame = &frame;
/* TEXT directive comes first; framesize is filled at the end. */
Prog *text = newprog(c, A_TEXT);
/* Mangle the label using the fn's own module as the hint — picks
* the right entry when multiple modules export the same leaf. */
text->to = mafn(c, fn->str, c->cur_mod);
text->from.offset = 0; /* framesize patched below */
emit(c, text);
/* prologue */
ins1(c, A_PUSHQ, areg(D_BP));
ins2(c, A_MOVQ, areg(D_SP), areg(D_BP));
Prog *subsp = newprog(c, A_SUBQ);
subsp->from = aimm(0);
subsp->to = areg(D_SP);
emit(c, subsp);
/* sret discipline (#23): plain TY_STRUCT return > 24B consumes
* RDI as a hidden first-arg dest pointer. Spill it to @sretarg
* before the user-param loop so cgreturn can write through it,
* and start the user-arg register counter at 1 to shift every
* declared arg right by one (SI/DX/CX/R8/R9/+stack). */
if (cg_sret_retsize(cg_ret_type) > 0) {
cg_sret_arg_off = local_alloc(c, &locals, "@sretarg",
8, &frame);
ins2(c, A_MOVQ, areg(D_DI),
amem(D_BP, cg_sret_arg_off));
}
/* spill incoming arg registers to local slots. Slice params
* occupy 24 bytes; float params land in XMM0..7 (counted
* separately from integer DI/SI/DX/CX/R8/R9). */
int argi = (cg_sret_arg_off != 0) ? 1 : 0;
int fargi = 0;
int memparam_words = 0;
Tparam *tp = fn->type ? fn->type->params : NULL;
for (Node *p = fn->list; p; p = p->next) {
if (p->str == NULL || strcmp(p->str, "...") == 0) {
if (tp) tp = tp->next;
continue;
}
Type *pt = tp ? tp->type : NULL;
/* Transitive chase (#61): the callee-side classify twin of
* struct_arg_size/aggarg_size — a 2-level alias param fell to
* the scalar arm and spilled ONLY DI. */
Type *pu = type_chase_named(pt);
int slice = (pu && pu->kind == TY_SLICE);
int is_str = type_isstr(pt);
int is_struct = pu && pu->kind == TY_STRUCT && pu->size <= 16;
/* #271: a by-value array param, or a struct param > 16B —
* received as ceil(sz/8) GP eightbytes, the callee twin of the
* generalised aggregate-arg push. The ≤16B struct keeps its own
* (possibly SSE-classified) path above. */
int is_bigagg = pu && ((pu->kind == TY_ARRAY)
|| (pu->kind == TY_STRUCT && pu->size > 16));
int agg_eb = is_bigagg ? (int)((pu->size + 7) / 8) : 0;
int tagged_sz = tagged_arg_size(pt);
int is_tagged = tagged_sz > 0;
int isf = cg_isfloat(pt);
/* #38b: MEMORY-class (>48B tagged) param — the caller staged
* the whole slot below the return address; read it in place
* at positive BP offsets. No spill, no frame growth, zero
* prologue bytes. ref/qbe/amd64/sysv.c:80-85 / :411-426. */
int mem_sz = tagged_memarg_size(pt);
if (mem_sz > 0) {
Local *l = amalloc(c->a, sizeof *l);
l->name = p->str;
l->off = 16 + cg_stack_arg_cursor * 8;
cg_stack_arg_cursor += mem_sz / 8;
memparam_words += mem_sz / 8;
l->next = locals;
locals = l;
if (tp) tp = tp->next;
continue;
}
/* #163: tuple PARAM receive (param twin of #164's return).
* Walk the tuple's elements over the SysV arg cursor — a float
* reads its XMM (X0..X7), everything else an INTEGER arg reg
* (DI/SI/..); a slice/str its 3-word {ptr,len,cap} header — and
* store each into the param's frame slot positionally (eoff
* steps by the element's slot width: a slice/str 24B, else 8B,
* matching the tuple-field-access offset walk + the SEND). Reg
* overflow loud-stops (rule 7), the partial-spill stitch out of
* scope (twin of #164's cap). Placed before the single-class
* eightbytes logic below, which can't model a mixed GP/SSE
* aggregate. */
if (pu && pu->kind == TY_TUPLE) {
int sz = (int)pu->size;
int off = localoff(c, &locals, p->str, sz, &frame);
int eoff = 0, ef32;
for (Tparam *te = pu->params; te; te = te->next) {
if (fld_isfloat(te->type, &ef32)) {
if (fargi >= 8)
fatal("tuple param float element "
"overflows SSE arg regs "
"(X0..X7); stitch out of "
"scope, see #163");
ins2(c, ef32 ? A_MOVSS : A_MOVSD,
areg(sysv_fargregs[fargi]),
amem(D_BP, off + eoff));
fargi++;
eoff += 8;
continue;
}
int eb = tuple_eslot(te->type) / 8;
if (argi + eb > 6)
fatal("tuple param element overflows "
"integer arg regs (DI/SI/DX/CX/R8/"
"R9); stitch out of scope, see #163");
for (int k = 0; k < eb; k++, argi++)
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
amem(D_BP, off + eoff + k * 8));
eoff += tuple_eslot(te->type);
}
if (tp) tp = tp->next;
continue;
}
/* #165: float-bearing struct PARAM receive (param twin of
* #163's tuple). Classify each SysV eightbyte; a lone-f64
* eightbyte reads its XMM (X0..X7), a pure-INT eightbyte its
* INTEGER arg reg (DI/SI/..), stored into the param's frame
* slot at the 8-byte eightbyte stride. Gated to qualifying
* structs by struct_float_class — all-int + f32-packed keep
* the GP transport below (byte-id / #165b). Placed before the
* single-class eightbyte logic, which can't model a mixed
* GP/SSE aggregate. Reg overflow loud-stops (rule 7). */
if (is_struct) {
int sclass[2], snb;
if ((snb = struct_float_class(pt, sclass)) > 0) {
int sz = (int)pu->size;
int off = localoff(c, &locals, p->str, sz, &frame);
for (int e = 0; e < snb; e++) {
if (sclass[e]) {
if (fargi >= 8)
fatal("float struct param "
"eightbyte overflows SSE "
"arg regs (X0..X7); stitch "
"out of scope, see #165");
ins2(c, A_MOVSD,
areg(sysv_fargregs[fargi]),
amem(D_BP, off + e * 8));
fargi++;
} else {
if (argi >= 6)
fatal("float struct param "
"eightbyte overflows "
"integer arg regs (DI/SI/"
"DX/CX/R8/R9); stitch out "
"of scope, see #165");
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
amem(D_BP, off + e * 8));
argi++;
}
}
if (tp) tp = tp->next;
continue;
}
}
/* Args overflowing register classes live at positive offsets
* from BP (16 + i*8). We register them as Locals at those
* offsets, no spill needed. */
int struct_eb = is_struct ? ((pu->size > 8) ? 2 : 1) : 0;
int tagged_eb = is_tagged ? (tagged_sz / 8) : 0;
/* str IS []u8: 3 eightbytes (ptr,len,cap), same as a slice
* — the caller pushes the triple (#1/Phase 3). */
int eightbytes = (slice || is_str) ? 3 :
(is_struct ? struct_eb :
(is_bigagg ? agg_eb :
(is_tagged ? tagged_eb : 1)));
int regs_left = isf ? (8 - fargi) : (6 - argi);
if (regs_left >= eightbytes) {
/* #60: route slice/str slot widths through Type.size SSoT
* so #1's ty_str.size bump propagates without retouching
* this site (or its stack-stitch mirror below). */
int sz = (slice || is_str) ? (int)pu->size :
(is_struct ? (int)pu->size :
(is_bigagg ? (int)pu->size :
(is_tagged ? tagged_sz : 8)));
int off = localoff(c, &locals, p->str, sz, &frame);
if (slice || is_str || is_struct || is_bigagg || is_tagged) {
for (int k = 0; k < eightbytes; k++, argi++)
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
amem(D_BP, off + k * 8));
} else if (isf) {
int mov = type_isf32(pt) ? A_MOVSS : A_MOVSD;
ins2(c, mov,
areg(sysv_fargregs[fargi]),
amem(D_BP, off));
fargi++;
} else {
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
amem(D_BP, off));
argi++;
}
} else if (eightbytes > 1 && regs_left > 0 &&
(slice || is_str || is_struct || is_bigagg || is_tagged)) {
/* Multi-word arg that partially fits in regs: caller
* filled (regs_left) registers greedily, the rest spilled
* to stack at positive BP offsets. Stitch a single local
* slot from both sources so the body sees a contiguous
* value. Mirrors the SysV greedy reg fill the caller
* does. */
/* #60: same SSoT routing as the regs-fit arm above. */
int sz = (slice || is_str) ? (int)pu->size :
(is_struct ? (int)pu->size :
(is_bigagg ? (int)pu->size :
(is_tagged ? tagged_sz : 8)));
int off = localoff(c, &locals, p->str, sz, &frame);
extern int cg_stack_arg_cursor;
int k = 0;
for (; k < regs_left; k++, argi++)
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
amem(D_BP, off + k * 8));
for (; k < eightbytes; k++) {
int stack_off = 16 +
cg_stack_arg_cursor * 8;
cg_stack_arg_cursor++;
ins2(c, A_MOVQ, amem(D_BP, stack_off),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k * 8));
}
} else {
/* stack-spilled. Access in place via positive BP offset. */
static int stack_arg_off;
(void)stack_arg_off;
Local *l = amalloc(c->a, sizeof *l);
l->name = p->str;
/* spilled args layout: each takes 8B (ptr/len/etc); we
* only support the simple case of plain int/float here. */
extern int cg_stack_arg_cursor;
l->off = 16 + cg_stack_arg_cursor * 8;
cg_stack_arg_cursor += eightbytes;
l->next = locals;
locals = l;
}
if (tp) tp = tp->next;
}
/* #38b: a MEMORY-class tagged param cannot coexist with stack-
* spilled register-class params — both walk the same positive-BP
* cursor in declaration order while the caller's residual region
* puts spill words below every mem copy. Any non-mem cursor use
* leaves the cursor past the mem words. Mirror of the cgcall
* caller-side check. */
if (memparam_words > 0 && cg_stack_arg_cursor != memparam_words)
fatal("#38b: >48B tagged param mixed with stack-spilled "
"params unwired");
/* Iterate the fn body's statements directly rather than dispatching
* the outermost N_BLOCK through cgstmt — N_BLOCK now save/restores
* the locals head to scope inner shadows, but the function body is
* not "an inner block": defers (queued during the body) and the
* implicit-return epilogue both call cgexpr after this loop and
* resolve identifiers via localfind, so the body's locals must
* still be in *locals when we get there. */
if (fn->body && fn->body->kind == N_BLOCK) {
for (Node *s = fn->body->list; s; s = s->next)
cgstmt(c, s, &locals, &frame);
} else {
cgstmt(c, fn->body, &locals, &frame);
}
/* implicit return for void functions */
if (c->tail->as != A_RET) {
for (int di = ndefers - 1; di >= 0; di--)
cgexpr(c, defers[di], locals);
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
}
/* round frame to 16; patch SUBQ */
if (frame & 15) frame = (frame + 15) & ~15;
subsp->from.offset = frame;
text->from.offset = frame;
txt_emit(out, c->head);
}
/* Escape one byte for an asm string literal — the same rules
* emit_data and emit_defs already use. */
static void
emit_data_byte(FILE *out, u8 b)
{
if (b == '"' || b == '\\')
fprintf(out, "\\%c", b);
else if (b < 0x20 || b >= 0x7f)
fprintf(out, "\\x%02x", b);
else
fputc(b, out);
}
/* Emit `DIR NAME(SB),"<8 LE bytes of v>"`. Used for scalar `def`
* constants (DATA) and scalar `let` globals (DATAW). */
static void
emit_data_row(FILE *out, const char *dir, const char *name, u64 v)
{
fprintf(out, "%s %s(SB),\"", dir, name);
for (int i = 0; i < 8; i++)
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
fputs("\"\n", out);
}
/* Emit `DIR NAME(SB),"<sz zero bytes>"`. Used for top-level str/
* slice/struct lets without a baked-in initialiser — the slot is
* pre-zeroed and the program writes the real value at runtime. */
static void
emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz)
{
fprintf(out, "%s %s(SB),\"", dir, name);
for (int i = 0; i < sz; i++)
emit_data_byte(out, 0);
fputs("\"\n", out);
}
/* Emit DATAW directives for top-level mutable `let` decls.
*
* Scalar lets (8B): emit the literal value, or 0 if no init.
* Non-literal init: skip — undefined symbol surfaces at link time.
*
* str lets (16B): three init shapes are wired:
* - no rhs / `nil` / `""` → 16 zero bytes
* - `"literal"` (non-empty) → 8 zero placeholder + 8 LE len,
* plus DATAR patching the ptr
* half with the interned strlit's
* runtime VA at link time.
*
* Slice lets (24B): no-init only — the slot is zero. There's no
* literal slice syntax to honour, so this is the natural shape.
*
* Struct lets (size from Type.size): no-init only. */
/* Emit a (DATA|DATAW) row for a float-typed top-level let/def with a
* FLOATLIT RHS (optionally wrapped in N_CAST or N_UN(±, ...)). Shared
* SSoT for emit_lets's float arm and emit_defs's float arm (#129
* Phase A.1, rule-12). The N_UN peel mirrors fold_int_literal's
* MINUS/TILDE/PLUS peel (#24) — the float arm had never been given
* the same treatment, so `let g: f64 = -1.5;` silently fell through
* to no-emit + undef-ref at link. Returns 1 on emit, 0 if the rhs
* shape doesn't reduce to a foldable float literal. */
static int
emit_floatlit_data(FILE *out, Cg *c, const char *directive,
const char *name, const char *module, Type *t, Node *rhs)
{
int isf32 = type_isf32(t);
int sz = isf32 ? 4 : 8;
u64 v = 0;
int neg = 0;
if (rhs != NULL) {
Node *r = rhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
if (r != NULL && r->kind == N_UN
&& (r->op == TK_MINUS || r->op == TK_PLUS)) {
if (r->op == TK_MINUS) neg = 1;
r = r->lhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
}
if (r == NULL || r->kind != N_FLOATLIT) return 0;
if (isf32) {
union { float f; u32 u; } x;
x.f = (float)r->fval;
v = (u64)x.u;
} else {
union { double d; u64 u; } x;
x.d = r->fval;
v = x.u;
}
}
fprintf(out, "%s %s(SB),\"", directive,
mod_mangle_value(c, name, module));
/* IEEE-754 sign-bit XOR for negation happens INSIDE the emit
* loop on the top byte only — semantically identical to a whole-
* u64 XOR with 2^63 (or 2^31 for f32) but never materialises
* that constant. Mirrors the wwstage helper's shape so the
* cgen.ww self-rebuild stays cs==ww byte-identical. */
for (int i = 0; i < sz; i++) {
u8 b = (u8)((v >> (i * 8)) & 0xff);
if (neg && i == sz - 1)
b = (u8)(b ^ 0x80);
emit_data_byte(out, b);
}
fputs("\"\n", out);
return 1;
}
/* Forward declaration: emit_struct_lit_bytes recurses into
* emit_array_lit_bytes for nested array fields (#129 A.3 closes the
* A.2 shape-15 park). Defined further down. */
static int emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs,
int emit_phase);
/* emit_struct_lit_bytes — emit the byte sequence for a struct-typed
* top-level let/def whose rhs is an N_STRUCTLIT (or NULL for bare
* no-rhs). Walks Tfield list in declaration order, zero-fills padding
* gaps via the offset table (rule 13), and dispatches per field type:
* integer/bool/nil via fold_int_literal, float via emit_floatlit_data's
* peel+bitcast core inlined, nested struct via recursion (the per-field
* inner literal lookup; nested-struct field-name-leak is a separate
* #145 bug filed against the parser/checker — the recursion is
* unblocked because emit-time field resolution goes through the type
* table, not the parser's symbol table). Array / str / slice / ptr-
* with-address fields are out of #129 A.2 scope — fatals loudly per
* rule-7 so a future consumer gets a precise stop rather than a
* silent zero-emit.
*
* Shared by emit_struct_data (#129 Phase A.2) below; broken out so the
* recursive call can recurse on the inner field bytes without re-
* opening the "DIR name(SB),\"" prefix. */
static int
emit_struct_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, u64 base)
{
/* Transitive entry chase (#77/#78 g-fold, condition-3 member): the
* single peel return-0'd on a 2-level-alias struct and the caller's
* skip-path emitted NO DATA for a registered global — undefined
* reference where pre-G1 it was a silent frame-local read. */
Type *u = type_chase_named(t);
if (u == NULL || u->kind != TY_STRUCT) return 0;
u64 pos = base;
for (Tfield *f = u->fields; f != NULL; f = f->next) {
u64 fstart = base + f->offset;
while (pos < fstart) {
emit_data_byte(out, 0);
pos++;
}
Node *v = NULL;
if (rhs != NULL) {
for (Node *fn = rhs->list; fn != NULL; fn = fn->next) {
if (fn->str && f->name
&& strcmp(fn->str, f->name) == 0) {
v = fn->lhs;
break;
}
}
}
int fsz = (int)f->type->size;
if (v == NULL) {
for (int i = 0; i < fsz; i++) emit_data_byte(out, 0);
pos += (u64)fsz;
continue;
}
Node *vr = v;
while (vr != NULL && vr->kind == N_CAST) vr = vr->lhs;
Type *fu = type_chase_named(f->type);
if (fu && fu->kind == TY_STRUCT) {
/* Recurse into nested struct lit. Pre-#145 the parser/
* checker has its own gap on inner-N_STRUCTLIT field
* name resolution; this emit recursion goes through
* the type table so it's correct in isolation. */
if (vr == NULL || vr->kind != N_STRUCTLIT)
fatal("emit_struct_lit_bytes: nested struct "
"field '%s' rhs is not N_STRUCTLIT "
"(#129 A.2)", f->name ? f->name : "?");
(void)emit_struct_lit_bytes(out, c, f->type, vr, fstart);
pos = fstart + (u64)fsz;
continue;
}
/* #129 A.3: array-typed field with N_ARRLIT rhs (the shape
* parked in A.2). Calls emit_array_lit_bytes which dispatches
* by element kind (int/float/struct). Returns 0 if the rhs
* shape can't reduce — fatal here per rule-7 since the field
* is declared array-typed and a non-reducible inner rhs is
* a real bug surface, not a fall-through. */
if (fu && fu->kind == TY_ARRAY) {
if (vr == NULL || vr->kind != N_ARRLIT)
fatal("emit_struct_lit_bytes: array field "
"'%s' rhs is not N_ARRLIT (#129 A.3)",
f->name ? f->name : "?");
if (!emit_array_lit_bytes(out, c, f->type, vr, 1))
fatal("emit_struct_lit_bytes: array field "
"'%s' rhs has non-reducible elements "
"(#129 A.3)", f->name ? f->name : "?");
pos = fstart + (u64)fsz;
continue;
}
if (type_isfloat(f->type)) {
int isf32 = type_isf32(f->type);
u64 fv = 0;
int neg = 0;
Node *fr = vr;
if (fr != NULL && fr->kind == N_UN
&& (fr->op == TK_MINUS || fr->op == TK_PLUS)) {
if (fr->op == TK_MINUS) neg = 1;
fr = fr->lhs;
while (fr != NULL && fr->kind == N_CAST)
fr = fr->lhs;
}
if (fr == NULL || fr->kind != N_FLOATLIT)
fatal("emit_struct_lit_bytes: float field "
"'%s' rhs not foldable FLOATLIT (#129 A.2)",
f->name ? f->name : "?");
if (isf32) {
union { float f; u32 u; } x;
x.f = (float)fr->fval;
fv = (u64)x.u;
} else {
union { double d; u64 u; } x;
x.d = fr->fval;
fv = x.u;
}
for (int i = 0; i < fsz; i++) {
u8 b = (u8)((fv >> (i * 8)) & 0xff);
if (neg && i == fsz - 1) b = (u8)(b ^ 0x80);
emit_data_byte(out, b);
}
pos = fstart + (u64)fsz;
continue;
}
u64 iv = 0;
if (!fold_int_literal(vr, &iv))
fatal("emit_struct_lit_bytes: field '%s' rhs not a "
"foldable literal (str/slice/ptr/array fields "
"are out of #129 A.2 scope)",
f->name ? f->name : "?");
for (int i = 0; i < fsz; i++)
emit_data_byte(out, (u8)((iv >> (i * 8)) & 0xff));
pos = fstart + (u64)fsz;
}
/* Tail padding to t->size. */
u64 end = base + t->size;
while (pos < end) {
emit_data_byte(out, 0);
pos++;
}
return 1;
}
/* emit_struct_data — top-level wrapper that opens the DATA/DATAW
* directive and delegates the byte payload to emit_struct_lit_bytes.
* Shared SSoT between emit_lets's struct arm and emit_defs's struct
* arm (#129 Phase A.2, rule-12 sea-of-stars). Returns 1 on emit, 0 if
* the type isn't a struct. */
static int
emit_struct_data(FILE *out, Cg *c, const char *directive,
const char *name, const char *module, Type *t, Node *rhs)
{
Type *u = type_chase_named(t);
if (u == NULL || u->kind != TY_STRUCT) return 0;
fprintf(out, "%s %s(SB),\"", directive,
mod_mangle_value(c, name, module));
emit_struct_lit_bytes(out, c, t, rhs, 0);
fputs("\"\n", out);
return 1;
}
/* emit_array_lit_bytes — emit alen * esz bytes for an [N]T top-level
* let/def with N_ARRLIT rhs. Per-element dispatch:
* - int element (covers bool/rune/typed-int/N_UN-int): fold_int_literal
* per element, emit LE bytes. Existing pre-#129-A.3 emit_lets array
* arm logic preserved byte-for-byte so the bootstrap consumers in
* lib/os, lib/bufio, lib/strings, lib/encoding/utf8, lib/strconv/
* stof_data don't shift.
* - float element (f32/f64): peel N_CAST/N_UN(±), bitcast magnitude
* via union (mirrors emit_floatlit_data), sign-XOR top byte of each
* element inline. NO 2^63 immediate.
* - struct element: per element call emit_struct_lit_bytes (#129 A.2
* helper).
* - other element kinds (str/slice/ptr-with-address/nested-array):
* return 0 — caller falls through to zero-init (str/slice accepts
* no-rhs already).
*
* Trailing `...` repeat marker fills remaining slots with the last
* value (mirrors the scalar repeat path). Returns 1 on emit, 0 if the
* rhs shape can't reduce to a foldable literal — caller MUST then
* fall back to zero-init / skip path; the caller opens the DATA/DATAW
* directive AFTER a successful validate-only call. Two-call pattern
* keeps emit-on-failure from emitting partial bytes.
*
* `emit_phase = 0` runs validate-only (returns 1 if ok); `emit_phase
* = 1` actually emits. */
static int
emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase)
{
Type *u = type_chase_named(t);
if (u == NULL || u->kind != TY_ARRAY) return 0;
Type *etype = u->sub;
Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype;
int esz = etype ? (int)etype->size : 1;
int alen = (int)u->alen;
if (eu && eu->kind == TY_STRUCT) {
/* Validate: every element must be N_STRUCTLIT (after N_CAST
* peel). */
int idx = 0;
Node *last_ev = NULL;
int repeat = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev == NULL || ev->kind != N_STRUCTLIT) return 0;
last_ev = ev;
idx++;
}
if (!emit_phase) return 1;
idx = 0;
repeat = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
emit_struct_lit_bytes(out, c, etype, ev, 0);
idx++;
}
while (idx < alen) {
if (repeat && last_ev != NULL)
emit_struct_lit_bytes(out, c, etype, last_ev, 0);
else
for (int b = 0; b < esz; b++)
emit_data_byte(out, 0);
idx++;
}
return 1;
}
/* #129 A.3 capstone (PREREQ-1, #156): nested-array element [M]T
* inside [N][M]T. Mirror of the TY_STRUCT-element arm above and of
* the TY_ARRAY-field-in-struct arm in emit_struct_lit_bytes — recurse
* into emit_array_lit_bytes per element; recursion bottoms out at
* scalar (int/float) elements. esz = etype->size gives the per-
* element stride (rule 13, no manual stride math). The `...` repeat
* marker with nested-array elements is rejected loud (rule 7): no
* consumer needs it (powers_of_ten is fully enumerated) and the
* scalar-repeat byte-fill cannot reduce a nested N_ARRLIT. */
if (eu && eu->kind == TY_ARRAY) {
int idx = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0)
fatal("emit_array_lit_bytes: '...' repeat with "
"nested-array elements unsupported "
"(#129 A.3, rule 7)");
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev == NULL || ev->kind != N_ARRLIT) return 0;
if (!emit_array_lit_bytes(out, c, etype, ev, 0))
return 0;
idx++;
}
if (!emit_phase) return 1;
idx = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
emit_array_lit_bytes(out, c, etype, ev, 1);
idx++;
}
while (idx < alen) {
for (int b = 0; b < esz; b++)
emit_data_byte(out, 0);
idx++;
}
return 1;
}
if (type_isfloat(etype)) {
int isf32 = type_isf32(etype);
/* Validate: every element must be N_FLOATLIT (after N_CAST
* + optional N_UN(±) peel). */
int idx = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) break;
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev != NULL && ev->kind == N_UN
&& (ev->op == TK_MINUS || ev->op == TK_PLUS)) {
ev = ev->lhs;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
}
if (ev == NULL || ev->kind != N_FLOATLIT) return 0;
idx++;
}
if (!emit_phase) return 1;
idx = 0;
u64 last_bits = 0;
int last_neg = 0;
int repeat = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
int neg = 0;
if (ev != NULL && ev->kind == N_UN
&& (ev->op == TK_MINUS || ev->op == TK_PLUS)) {
if (ev->op == TK_MINUS) neg = 1;
ev = ev->lhs;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
}
u64 bits = 0;
if (isf32) {
union { float f; u32 u; } x;
x.f = (float)ev->fval;
bits = (u64)x.u;
} else {
union { double d; u64 u; } x;
x.d = ev->fval;
bits = x.u;
}
for (int b = 0; b < esz; b++) {
u8 byt = (u8)((bits >> (b * 8)) & 0xff);
if (neg && b == esz - 1) byt = (u8)(byt ^ 0x80);
emit_data_byte(out, byt);
}
last_bits = bits;
last_neg = neg;
idx++;
}
while (idx < alen) {
if (repeat) {
for (int b = 0; b < esz; b++) {
u8 byt = (u8)((last_bits >> (b * 8)) & 0xff);
if (last_neg && b == esz - 1)
byt = (u8)(byt ^ 0x80);
emit_data_byte(out, byt);
}
} else {
for (int b = 0; b < esz; b++)
emit_data_byte(out, 0);
}
idx++;
}
return 1;
}
/* Int-element path — preserved BYTE-FOR-BYTE from the pre-A.3
* emit_lets in-place array arm so the bootstrap consumers (u8 /
* i8 / u16 arrays in lib/os, lib/bufio, lib/strings, lib/
* encoding/utf8, lib/strconv/stof_data) don't shift. */
u64 *vals = amalloc(c->a, sizeof(u64) * (size_t)alen);
int idx = 0;
int ok = 1;
u64 last = 0;
int repeat = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev == NULL) { ok = 0; break; }
if (!fold_int_literal(ev, &last)) { ok = 0; break; }
vals[idx++] = last;
}
if (!ok) return 0;
if (!emit_phase) return 1;
if (repeat) {
while (idx < alen) vals[idx++] = last;
} else {
while (idx < alen) vals[idx++] = 0;
}
for (int i = 0; i < alen; i++) {
u64 v = vals[i];
for (int b = 0; b < esz; b++)
emit_data_byte(out, (u8)((v >> (b * 8)) & 0xff));
}
return 1;
}
/* emit_strarray_data — module-level `let xs: [N]str = ["a","b",…];`
* static init (#18). The str-element case can't ride emit_array_lit_bytes:
* a str element carries a ptr→rodata relocation, not just bytes. So the
* scalar-str-global pattern (emit_lets str arm: DATAW header with a zero
* ptr placeholder + inline LE len, then a DATAR patching the ptr half)
* is applied per element at offset idx*esz. Each strlit was pre-interned
* by let_pre_intern so its rodata _S_ row exists before this row's DATAR
* references it.
*
* Scoped to DATAW (writable `let`): A_DATAR requires its holder be a
* DATAW slot (w6a asm.c:362), so a read-only `def [N]str` can't carry
* the relocs — that generalisation is a #18 follow-up. Returns 0 if the
* element type isn't str, leaving the generic array path / zero-init to
* the caller. */
static int
emit_strarray_data(FILE *out, Cg *c, const char *directive,
const char *name, const char *module, Type *t, Node *rhs)
{
Type *u = type_chase_named(t);
if (u == NULL || u->kind != TY_ARRAY) return 0;
Type *etype = u->sub;
Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype;
if (eu == NULL || eu->kind != TY_STR) return 0;
if (strcmp(directive, "DATAW") != 0) return 0;
int esz = (int)etype->size;
int alen = (int)u->alen;
/* Validate: each cast-peeled element is an N_STRLIT, up to an
* optional trailing `...` repeat marker. Bail (return 0) on any
* non-strlit so a non-reducible rhs still falls through to the
* generic path rather than emitting a partial row. */
Node *last_ev = NULL;
int repeat = 0;
int cnt = 0;
for (Node *e = rhs->list; e && cnt < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) { repeat = 1; break; }
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev == NULL || ev->kind != N_STRLIT) return 0;
last_ev = ev;
cnt++;
}
const char *sym = mod_mangle_value(c, name, module);
fprintf(out, "DATAW %s(SB),\"", sym);
int idx = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) break;
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
for (int i = 0; i < 8; i++) emit_data_byte(out, 0);
u64 v = ev->strlen;
for (int i = 0; i < 8; i++)
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
for (int i = 16; i < esz; i++) emit_data_byte(out, 0);
idx++;
}
while (idx < alen) {
u64 v = (repeat && last_ev != NULL) ? last_ev->strlen : 0;
for (int i = 0; i < 8; i++) emit_data_byte(out, 0);
for (int i = 0; i < 8; i++)
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
for (int i = 16; i < esz; i++) emit_data_byte(out, 0);
idx++;
}
fputs("\"\n", out);
idx = 0;
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) break;
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev->strlen > 0) {
const char *lab = intern_strlit(c, ev->str, ev->strlen);
fprintf(out, "DATAR %s+%d(SB),%s(SB)\n",
sym, idx * esz, lab);
}
idx++;
}
while (idx < alen) {
if (repeat && last_ev != NULL && last_ev->strlen > 0) {
const char *lab = intern_strlit(c, last_ev->str,
last_ev->strlen);
fprintf(out, "DATAR %s+%d(SB),%s(SB)\n",
sym, idx * esz, lab);
}
idx++;
}
return 1;
}
/* emit_tuple_data — module-level `let g: (T0, T1, ...) = (v0, v1, ...);`
* static init (C-t3, #48). Slot layout (C-t0): a scalar element is one
* 8B LE word, a str element its 24B header slot (8 zero ptr placeholder
* + LE len + 8 zero cap) with a DATAR patching the ptr word at the
* element's slot offset — the per-element twin of the scalar-str-global
* arm, offset like emit_strarray_data's rows. Elements must reduce to
* int (fold_int_literal) or str literals; anything else returns 0 and
* the caller loud-stops (rule 7 — pre-C-t3 the whole definition was
* SILENTLY skipped and reads saw BP-frame garbage). rhs == NULL
* zero-inits the slot. */
static int
emit_tuple_data(FILE *out, Cg *c, const char *name, const char *module,
Type *t, Node *rhs)
{
Type *u = type_unwrap(t);
if (u == NULL || u->kind != TY_TUPLE) return 0;
const char *sym = mod_mangle_value(c, name, module);
if (rhs == NULL) {
fprintf(out, "DATAW %s(SB),\"", sym);
for (int i = 0; i < (int)u->size; i++)
emit_data_byte(out, 0);
fputs("\"\n", out);
return 1;
}
if (rhs->kind != N_TUPLE) return 0;
/* Validate: every cast-peeled element folds (int) or is a strlit
* in a str slot. Two-pass so a partial row never reaches the
* output (emit_array_data precedent). */
Tparam *tp = u->params;
for (Node *e = rhs->list; e; e = e->next, tp = tp ? tp->next : NULL) {
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
if (ev == NULL) return 0;
/* #22a (rule 7): a tagged element slot has no static-init
* shape (tag word + payload widening) — reject so the
* caller loud-stops; pre-guard an int init would have
* emitted one 8B word into the 16B+ box (silent layout
* skew). */
{
Type *eu = type_chase_named(tp ? tp->type : NULL);
if (eu && eu->kind == TY_TAGGED) return 0;
}
int wide = tp && (type_isstr(tp->type)
|| type_isslice(tp->type));
if (wide) {
if (ev->kind != N_STRLIT) return 0;
continue;
}
u64 v;
if (!fold_int_literal(ev, &v)) return 0;
}
fprintf(out, "DATAW %s(SB),\"", sym);
tp = u->params;
for (Node *e = rhs->list; e; e = e->next, tp = tp ? tp->next : NULL) {
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
int wide = tp && (type_isstr(tp->type)
|| type_isslice(tp->type));
if (wide) {
u64 v = ev->strlen;
for (int i = 0; i < 8; i++) emit_data_byte(out, 0);
for (int i = 0; i < 8; i++)
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
for (int i = 16; i < (int)ty_str->size; i++)
emit_data_byte(out, 0);
continue;
}
u64 v = 0;
(void)fold_int_literal(ev, &v);
for (int i = 0; i < 8; i++)
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
}
fputs("\"\n", out);
int foff = 0;
tp = u->params;
for (Node *e = rhs->list; e; e = e->next, tp = tp ? tp->next : NULL) {
Node *ev = e;
while (ev && ev->kind == N_CAST) ev = ev->lhs;
int wide = tp && (type_isstr(tp->type)
|| type_isslice(tp->type));
if (wide && ev->strlen > 0) {
const char *lab = intern_strlit(c, ev->str,
ev->strlen);
fprintf(out, "DATAR %s+%d(SB),%s(SB)\n",
sym, foff, lab);
}
foff += tuple_eslot(tp ? tp->type : NULL);
}
return 1;
}
/* emit_array_data — opens DATA/DATAW prefix on validate success, then
* emits payload. Two-pass keeps emit-on-failure from emitting partial
* bytes (would corrupt the asm if rhs reduces partway through). */
static int
emit_array_data(FILE *out, Cg *c, const char *directive,
const char *name, const char *module, Type *t, Node *rhs)
{
Type *u = type_chase_named(t);
if (u == NULL || u->kind != TY_ARRAY) return 0;
/* str-element arrays carry per-element ptr relocations — handled
* by the dedicated DATAW+DATAR helper (#18). */
if (emit_strarray_data(out, c, directive, name, module, t, rhs))
return 1;
if (!emit_array_lit_bytes(out, c, t, rhs, 0)) return 0;
fprintf(out, "%s %s(SB),\"", directive,
mod_mangle_value(c, name, module));
emit_array_lit_bytes(out, c, t, rhs, 1);
fputs("\"\n", out);
return 1;
}
/* emit_slice_data — module-level `let g: []T = [v0, v1, …];` static
* init (#10 part a). A slice literal needs three things: a writable
* backing holding the k elements, a 24B header { ptr, len, cap }, and a
* DATAR patching the ptr word with the backing's VA. The backing rides
* the emit_array_lit_bytes choke-point via a synthesized [k]T so int /
* float / struct / nested-array elements reduce exactly as a [N]T
* global's do. The backing symbol is "<mangled g>.d": a second '.' can
* never collide with a user global, since source identifiers carry no
* '.' (one is inserted only by the module mangle).
*
* Scoped to a writable `let` — A_DATAR's holder must be a DATAW slot
* (w6a asm.c:362), so a read-only `def []T = [...]` can't carry the ptr
* reloc. That, a `...` repeat (a slice literal has no target length),
* and slice-of-{str,slice,tagged} elements (per-element relocs / #17)
* all loud-stop (rule 7) — #10 follow-ups, never silent fall-through.
* Returns 0 only on the early shape guards (not a slice / rhs not
* N_ARRLIT) so the caller's gate stays the sole entry contract. */
static int
emit_slice_data(FILE *out, Cg *c, const char *directive, const char *name,
const char *module, Type *t, Node *rhs)
{
Type *u = type_chase_named(t);
if (u == NULL || u->kind != TY_SLICE) return 0;
if (rhs == NULL || rhs->kind != N_ARRLIT) return 0;
if (strcmp(directive, "DATAW") != 0)
fatal("emit_slice_data: slice-literal static-init needs a "
"writable `let` (DATAR holder must be DATAW, w6a "
"asm.c:362); read-only `def` unsupported (#10, rule 7)");
Type *etype = u->sub;
Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype;
if (eu && (eu->kind == TY_STR || eu->kind == TY_SLICE
|| eu->kind == TY_TAGGED))
fatal("emit_slice_data: slice-of-{str,slice,tagged} literal "
"static-init unsupported (#10 follow-up, rule 7)");
int k = 0;
for (Node *e = rhs->list; e; e = e->next) {
if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0)
fatal("emit_slice_data: '...' repeat has no target "
"length in a slice literal (#10, rule 7)");
k++;
}
int esz = etype ? (int)etype->size : 1;
/* Synthesize [k]T to ride the emit_array_lit_bytes choke-point. */
Type arr;
memset(&arr, 0, sizeof arr);
arr.kind = TY_ARRAY;
arr.sub = etype;
arr.alen = (u64)k;
arr.size = (u64)k * (u64)esz;
if (!emit_array_lit_bytes(out, c, &arr, rhs, 0))
fatal("emit_slice_data: slice-literal element not a foldable "
"constant (#10, rule 7)");
const char *sym = mod_mangle_value(c, name, module);
const char *bk = aprintf(c->a, "%s.d", sym);
/* Writable backing data. */
fprintf(out, "DATAW %s(SB),\"", bk);
emit_array_lit_bytes(out, c, &arr, rhs, 1);
fputs("\"\n", out);
/* 24B header: ptr placeholder + LE len + LE cap (both = k). Word
* sizes from the type table (rule 13). */
fprintf(out, "DATAW %s(SB),\"", sym);
for (int i = 0; i < (int)ty_uintptr->size; i++) emit_data_byte(out, 0);
u64 kv = (u64)k;
for (int i = 0; i < (int)ty_size->size; i++)
emit_data_byte(out, (u8)((kv >> (i * 8)) & 0xff));
for (int i = 0; i < (int)ty_size->size; i++)
emit_data_byte(out, (u8)((kv >> (i * 8)) & 0xff));
fputs("\"\n", out);
/* Patch the ptr word with the backing VA. */
fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, bk);
return 1;
}
static void
emit_lets(Cg *c, FILE *out, Node *file)
{
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_LET) continue;
if (d->str == NULL || d->str[0] == '\0') continue;
int sz = let_emit_size(d->type);
if (sz == 0) continue;
if (let_isfloat(d->type)) {
(void)emit_floatlit_data(out, c, "DATAW",
d->str, d->module, d->type, d->rhs);
continue;
}
/* C-t3 (#48): tuple global — slot-laid DATAW row (+ DATAR
* ptr patches for str elements). Unsupported element inits
* die LOUD; pre-C-t3 the whole definition was silently
* skipped (no DATA, no diagnostic) and reads saw BP-frame
* garbage. */
{
Type *tu = type_unwrap(d->type);
if (tu != NULL && tu->kind == TY_TUPLE) {
Node *tr = d->rhs;
while (tr != NULL && tr->kind == N_CAST)
tr = tr->lhs;
if (!emit_tuple_data(out, c, d->str,
d->module, d->type, tr))
fatal("global tuple let `%s`: "
"unsupported element init "
"(int/str literals only; rule 7)",
d->str);
continue;
}
}
/* #129 A.2: gate `!let_isstruct` so an 8B struct lit
* (`struct { i32, i32 }`, `struct { f32, f32 }`, …) does
* NOT short-circuit through the scalar 8B `fold_int_literal`
* arm — fold-fail-`continue` would otherwise drop the let
* entirely, emitting no DATA and diverging from wwstage's
* emitletdataw (which gates its 8B scalar with `!issg`).
* Symmetric ordering with the wwstage struct arm. */
if (sz == 8 && !let_isarray(d->type) && !let_isstruct(d->type)) {
u64 v = 0;
if (d->rhs != NULL) {
Node *r = d->rhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
if (r == NULL) continue;
/* Same helper as emit_defs (#24): widens
* the gate to cover N_UN(TK_MINUS/TILDE/PLUS,
* leaf) so `let x: i8 = -1i8;` and friends
* encode as sign-extended two's-complement
* bytes. emit_data_row writes 8 LE bytes
* so narrow signed types just naturally
* round-trip via the sign-extended u64. */
if (!fold_int_literal(r, &v)) continue;
}
emit_data_row(out, "DATAW",
mod_mangle_value(c, d->str, d->module), v);
continue;
}
/* Strip leading casts on the rhs so a `nil: str` etc.
* reads the same as a bare nil. */
Node *r = NULL;
if (d->rhs != NULL) {
r = d->rhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
if (r == NULL) continue;
}
/* str literal init: bake the interned label's address
* into the ptr half via a DATAR reloc, set the len half
* inline. */
/* #43: gate via ty_str->size so #1 propagates. */
if (sz == (int)ty_str->size && r != NULL && r->kind == N_STRLIT
&& r->strlen > 0) {
const char *lab = intern_strlit(c, r->str, r->strlen);
const char *sym = mod_mangle_value(c, d->str, d->module);
u64 v = r->strlen;
/* 16-byte payload: 8 zero placeholder + LE len. */
fprintf(out, "DATAW %s(SB),\"", sym);
for (int i = 0; i < 8; i++) emit_data_byte(out, 0);
for (int i = 0; i < 8; i++)
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
fputs("\"\n", out);
fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, lab);
continue;
}
/* Array literal init: `let xs: [N]T = [v0, v1, ...];`. The
* helper dispatches per element kind (int/float/struct).
* Int-element path preserved BYTE-FOR-BYTE from pre-A.3 so
* bootstrap consumers (lib/os, lib/bufio, lib/strings, lib/
* encoding/utf8, lib/strconv/stof_data) don't shift. Float
* + struct elements gain emit; ptr / nested-array fall
* through to zero-init (existing path below). */
if (r != NULL && r->kind == N_ARRLIT && let_isarray(d->type)) {
if (emit_array_data(out, c, "DATAW", d->str,
d->module, d->type, r))
continue;
/* fall through to zero-init */
}
/* #10: slice-literal static init `let g: []T = [v0, …];`.
* Header { ptr, len, cap } + a writable backing + a DATAR
* patching ptr → backing. emit_slice_data loud-stops on the
* deferred element kinds and on the read-only / `...` shapes
* (rule 7); when the gate matches it always emits or fatals,
* never silently falls through. */
if (r != NULL && r->kind == N_ARRLIT && let_isslice(d->type)) {
if (emit_slice_data(out, c, "DATAW", d->str,
d->module, d->type, r))
continue;
}
/* Otherwise: zero-init. str accepts nil / ""; struct
* accepts no rhs at all; slice accepts nil; array with no
* literal init (or a non-constant one) zero-fills. */
if (r != NULL) {
int is_struct = let_isstruct(d->type);
int is_array = let_isarray(d->type);
int empty_str = (r->kind == N_STRLIT && r->strlen == 0);
/* #129 A.2: struct-typed let with N_STRUCTLIT rhs
* routes through the emit_struct_data SSoT. Pre-#129
* this fell through to `continue` and emit-NOTHING,
* so the link surfaced an undefined ref. */
if (is_struct && r->kind == N_STRUCTLIT) {
if (emit_struct_data(out, c, "DATAW", d->str,
d->module, d->type, r))
continue;
}
if (is_struct) continue;
if (is_array) continue;
if (r->kind != N_NIL && !empty_str) continue;
}
emit_data_row_zero(out, "DATAW",
mod_mangle_value(c, d->str, d->module), sz);
}
}
/* Emit DATA directives for top-level `def` constants whose value
* folds to an integer literal. The w6a side stores the bytes inside
* .text and accesses are RIP-relative.
*
* fold_int_literal (cmd/wcc/check.c) gates: int/rune literal,
* true/false/nil, and a unary +/-/~ over the same. `def NEG: i32 =
* -100;` arrives as N_UN(TK_MINUS, N_INTLIT) — the unary peel is
* exactly what the gate is for. Anything richer (sibling refs,
* arithmetic) falls through; emit_defs has no scope to resolve
* names. */
static void
emit_defs(Cg *c, FILE *out, Node *file)
{
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_DEF || d->rhs == NULL) continue;
u64 v;
if (fold_int_literal(d->rhs, &v)) {
fprintf(out, "DATA %s(SB),\"",
mod_mangle_value(c, d->str, d->module));
for (int i = 0; i < 8; i++) {
unsigned b = (unsigned)((v >> (i * 8)) & 0xff);
if (b == '"' || b == '\\')
fprintf(out, "\\%c", b);
else if (b < 0x20 || b >= 0x7f)
fprintf(out, "\\x%02x", b);
else
fputc(b, out);
}
fputs("\"\n", out);
continue;
}
/* Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) rhs.
* Routes through the same SSoT helper as emit_lets's float
* arm — pre-#129 this fell through to no-emit + undef-ref
* at link. */
if (let_isfloat(d->type)) {
(void)emit_floatlit_data(out, c, "DATA",
d->str, d->module, d->type, d->rhs);
continue;
}
/* #129 A.2: struct-typed def with N_STRUCTLIT rhs. Parallel
* to emit_lets's struct arm; uses DATA (read-only) directive.
* Without the LOAD-side widening below the def's address
* still wouldn't be reachable, but storage is the precondition
* for the LOAD path to find something. */
if (let_isstruct(d->type) && d->rhs->kind == N_STRUCTLIT) {
(void)emit_struct_data(out, c, "DATA",
d->str, d->module, d->type, d->rhs);
continue;
}
/* #129 A.3: array-typed def with N_ARRLIT rhs. Parallel to
* emit_lets's array arm; uses DATA (read-only). LOAD-side
* widening at cgindex/cgdot resolves the def's address via
* LEAQ name(SB). */
if (let_isarray(d->type) && d->rhs->kind == N_ARRLIT) {
(void)emit_array_data(out, c, "DATA",
d->str, d->module, d->type, d->rhs);
continue;
}
/* #10: a read-only `def g: []T = [...]` slice literal can't
* carry the ptr reloc emit_slice_data needs (DATAR holder must
* be DATAW, w6a asm.c:362). Loud-stop rather than silently
* emit nothing and surface an undefined-ref at link. */
if (let_isslice(d->type) && d->rhs->kind == N_ARRLIT)
fatal("emit_defs: module-level slice-literal init needs "
"a writable `let` (DATAR holder must be DATAW, w6a "
"asm.c:362); read-only `def` unsupported (#10, "
"rule 7)");
}
(void)c;
}
/* Collect str-typed `def`s so cgexpr N_IDENT can splice them inline.
* Walks past any leading cast on the rhs (e.g. `def x: error = "x": error;`
* shows up as N_CAST wrapping an N_STRLIT). */
static void
sdef_collect(Cg *c, Node *file)
{
(void)file;
sdefs = NULL;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_DEF || d->rhs == NULL) continue;
Node *r = d->rhs;
while (r && r->kind == N_CAST) r = r->lhs;
if (r == NULL || r->kind != N_STRLIT) continue;
Sdef *s = amalloc(c->a, sizeof *s);
s->name = d->str;
s->mod = (d->module && d->module[0]) ? d->module : NULL;
s->bytes = r->str;
s->len = r->strlen;
s->next = sdefs;
sdefs = s;
}
}
/* Pre-intern strlits referenced from top-level `let` initialisers
* (e.g. `let g: str = "hello";`). Interning has to happen before
* emit_data walks the strlit list, but we don't want to reorder
* emit_data after emit_lets (the (DATA strlits, DATAW lets) section
* order is part of the byte-identity contract with the selfhost
* cgen). So this pass populates the strlit table; emit_lets later
* just looks up the label. */
static void
let_pre_intern(Cg *c, Node *file)
{
if (file == NULL) return;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_LET) continue;
Node *r = d->rhs;
while (r != NULL && r->kind == N_CAST) r = r->lhs;
/* #18: `let xs: [N]str = […];` — pre-intern each element's
* strlit in element order (then repeat-fill) so emit_strarray_
* data's DATAR rows find an _S_ rodata row. Must match that
* helper's interning order exactly to keep labels stable.
* Chase transitively (#77/#78 g-fold): emit_strarray_data now
* reaches 2-level-alias [N]str globals; a single peel here
* would intern their labels in emit order, not decl order. */
Type *u = type_chase_named(d->type);
if (u != NULL && u->kind == TY_ARRAY
&& r != NULL && r->kind == N_ARRLIT) {
Type *eu = (u->sub && u->sub->kind == TY_NAMED)
? u->sub->under : u->sub;
if (eu != NULL && eu->kind == TY_STR) {
int alen = (int)u->alen;
int cnt = 0;
Node *last_ev = NULL;
int repeat = 0;
for (Node *e = r->list; e && cnt < alen;
e = e->next) {
if (e->kind == N_FIELD && e->str
&& strcmp(e->str, "...") == 0) {
repeat = 1;
break;
}
Node *ev = e;
while (ev && ev->kind == N_CAST)
ev = ev->lhs;
if (ev == NULL || ev->kind != N_STRLIT)
break;
if (ev->strlen > 0)
(void)intern_strlit(c, ev->str,
ev->strlen);
last_ev = ev;
cnt++;
}
if (repeat && last_ev != NULL
&& last_ev->strlen > 0) {
while (cnt < alen) {
(void)intern_strlit(c,
last_ev->str,
last_ev->strlen);
cnt++;
}
}
continue;
}
}
/* C-t3 (#48): tuple global — pre-intern str-element literals
* in element order so emit_tuple_data's DATAR rows find
* their _S_ rodata rows (the #18 array-arm pattern). */
if (u != NULL && u->kind == TY_TUPLE
&& r != NULL && r->kind == N_TUPLE) {
Tparam *tp = u->params;
for (Node *e = r->list; e;
e = e->next, tp = tp ? tp->next : NULL) {
Node *ev = e;
while (ev && ev->kind == N_CAST)
ev = ev->lhs;
if (ev == NULL || ev->kind != N_STRLIT)
continue;
if (!(tp && (type_isstr(tp->type)
|| type_isslice(tp->type))))
continue;
if (ev->strlen > 0)
(void)intern_strlit(c, ev->str,
ev->strlen);
}
continue;
}
if (let_emit_size(d->type) != (int)ty_str->size) continue;
if (r == NULL || r->kind != N_STRLIT) continue;
if (r->strlen == 0) continue;
(void)intern_strlit(c, r->str, r->strlen);
}
}
void
cg_file(Cg *c, FILE *out, Node *file)
{
if (file == NULL || file->kind != N_FILE) return;
ffi_collect(c, file);
mod_collect(c, file);
sdef_collect(c, file);
let_collect(c, file);
strlits = NULL;
strlit_seq = 0;
for (Node *d = file->list; d; d = d->next) {
if (d->kind != N_FNDECL) continue;
cgfn(c, out, d);
}
let_pre_intern(c, file);
emit_data(c, out);
emit_defs(c, out, file);
emit_lets(c, out, file);
}
void peephole(Cg *c) { (void)c; }
void regalloc_init(Cg *c) { (void)c; }