/* * 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 (SB), let w6a/w6l resolve. * Calling C externs: same — extern symbols are just unresolved CALLs. */ #include "gc.h" #include #include 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 — 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 — 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; } /* node_tuplearg_decl — #68: like node_tuplearg, but an N_TUPLE LITERAL arg * with a tuple PARAM type reports the DECLARED param tuple (element widths * keyed on the param, so a declared-tagged element's box words drive the * send/drain). The literal's own type is element-constructed (a concrete * rvalue under a tagged slot counts ONE word, not the box) — see the cgcall * paramtup send. Every other source falls back to node_tuplearg. */ static Type * node_tuplearg_decl(Node *n, Type *param) { if (n && n->kind == N_TUPLE && param) { Type *pu = type_chase_named(param); if (pu && pu->kind == TY_TUPLE) return pu; } return node_tuplearg(n); } /* #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; } /* agg_has_float — does an aggregate Type carry ANY float leaf * (recursively through struct fields / array element / tuple * positionals)? The #12 producer (N_TRYPROP/N_TRYUNW success shuffle) * loud-stops a float-bearing aggregate success variant: the union * return places a float eightbyte in the SSE class (X0/X1) which the GP * AX/DX/CX payload shuffle cannot reach (mirror #11/#165). Conservative * — ANY float, not a per-eightbyte SSE classify like struct_float_class * — a loud-stop only needs to refuse, not transport. Mirrors wwstage * tinfo_agg_float (cgenutil.ww). */ static int agg_has_float(Type *t) { t = type_chase_named(t); if (t == NULL) return 0; if (cg_isfloat(t)) return 1; if (t->kind == TY_STRUCT) { for (Tfield *f = t->fields; f; f = f->next) if (agg_has_float(f->type)) return 1; return 0; } if (t->kind == TY_ARRAY) return agg_has_float(t->sub); if (t->kind == TY_TUPLE) { for (Tparam *p = t->params; p; p = p->next) if (agg_has_float(p->type)) return 1; return 0; } return 0; } /* 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++) { /* A single-level inspection is sufficient here (batch-2 c3-B2, * 018ef66): constructible variant params never carry nested * NAMED layers at this scan; the ww twin uses the same invariant. */ 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. * These single-level inspections 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) { /* 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 1b (#95): NAMED source, no exact variant — chain membership. * An alias IS-A every type on its NAMED chain (ali2 is-a ali is-a * base), so declaring the variant as `ali` admits any source whose * chain shares a node with ali's. Two linear NAMED chains intersect * iff they share their chased bottom node (.ai/ken-95-oracle.md §1), * so membership reduces to pointer identity of the chased ends — * type_chase_named is the blessed chase, no raw hops. Variants are * counted UNGATED (bare prims are type-table singletons, so a bare * variant node can BE the source's bottom): that keeps the guard ≡ * harec's nassign>=2 → NULL (ref/harec/src/types.c:734-738; the * P1-exact short-circuit is pass 1 above). >=2 chain hits cannot be * disambiguated once the nominal-lossy model collapses the chain — * hard-error (drew's ambiguity proviso extended to the chained * set). */ if (vt->kind == TY_NAMED) { Type *sb = type_chase_named(vt); if (sb == NULL) return -1; int found = -1, n = 0; idx = 0; for (Tparam *p = t->params; p; p = p->next, idx++) { if (p->type && type_chase_named(p->type) == sb) { if (found < 0) found = idx; n++; } } if (n >= 2) fatal("cg_tag_for_variant: source alias chain " "reaches >=2 variants — ambiguous without " "nominal layout (#95)"); if (found >= 0) return found; /* c2 (#95): no chain hit (found/n are still -1/0 here) — * structural fallback on the chased ends. harec interns * bare composites structurally (type_hash, ref/harec/src/ * types.c:72-81), so a nominally-unrelated structurally- * equal decl DEALIASES TO THE SAME NODE there and the * assignability arm accepts via `to == from` * (types.c:1000-1002) — acceptance is definitional, not an * arm we could misread. Our store does not intern, so the * pointer compare of pass 1b misses it; chased type_eq is * the non-interned rendering of the same rule. EQUALITY * only — no type_is_assignable scalar import. The >=2 * hard-error is the nominal-lossy-model rendering of a case * harec cannot represent (two structurally-identical * variants intern to ONE type — a union cannot contain it * twice), not a harec deviation. */ idx = 0; for (Tparam *p = t->params; p; p = p->next, idx++) { if (p->type && type_eq(type_chase_named(p->type), sb)) { if (found < 0) found = idx; n++; } } if (n >= 2) fatal("cg_tag_for_variant: source structurally " "matches >=2 variants — ambiguous without " "nominal layout (#95)"); return found; } /* 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); /* #49: per-unit prefix on the strlit label. Under separate * compilation two str-bearing packages both emit `_S_0`.. from this * global counter → w6l link collision. Prefix with the owning * package PATH (c->cur_mod — set per-fn by cgfn, per-decl by * let_pre_intern for static-data strings) so the label is unique per * compilation unit by construction. Pure function of the module path * (NOT a build-nonce) so the self-host fixed-point holds across * ww2/ww3/ww4. Same `.` spelling as mklabel (cgen.c:1853). */ s->label = aprintf(c->a, "%s%s_S_%d", c->cur_mod ? c->cur_mod : "", c->cur_mod ? "." : "", 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 . 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; /* M1 #22: alias→import-path map built from the N_USE nodes, so a * qualified-ref codegen hint (`utf8.decoderune`) keys mod_map on the * dotted path the symbols are registered under, not the bare alias. * For single-level packages alias == path, so this is a no-op there. */ typedef struct Use Use; struct Use { const char *alias; const char *path; const char *module; /* owning module of the `use` decl (#40) */ Use *next; }; static Use *use_map; /* * use_hint — map a `use` alias to its dotted import path for the * qualified-ref mangle hint. NOT file-global: two modules in one unit * may bind the same leaf alias to different paths (#40 — module one's * `import a.math` and module two's `import b.math` both alias `math`). * The import declared in the SAME module as the reference (curmod) is * authoritative; preferring it routes each `math.pick()` to its own * package. Falls back to any matching alias when curmod has no own * import (single-occurrence case). Mirrors the checker's use_path * curmod-preference (check.c, M1 55f54fb). Returns the alias unchanged * when no `use` matches. */ static const char * use_hint(const char *curmod, const char *alias) { const char *any = NULL; if (alias == NULL) return alias; for (Use *u = use_map; u; u = u->next) { if (strcmp(u->alias, alias) != 0) continue; int same = (u->module == NULL) ? (curmod == NULL) : (curmod != NULL && strcmp(u->module, curmod) == 0); if (same) return u->path; if (any == NULL) any = u->path; } return any ? any : alias; } /* 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 `&`) 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. */ case TY_TAGGED: /* #87: non-nullable tagged-union global — box size (tag word * + payload, mirror of the runtime local box). int/str-literal * variant init via emit_tagged_data; match-scrutinee reads * resolve the box at name(SB). Pre-#87 the missing arm sized 0 * → no DATA, no letvar registration, and match read saved BP * as the tag (SEGV). * #45 (silent→loud bridge, task #15): a nullable `(*T | void)` * GLOBAL has no storage path. Returning 0 here made let_collect * + emit_lets silently skip the decl (no DATA, no letvar reg), * so a later match read 0(BP) and is/as emitted MOVQ name(SB) * for an undefined symbol — a silent miscompile in the CSP * handle-singleton substrate. Die loud at the size/storage layer * so all three read paths hit one diagnostic; the full storage + * read-class arc is task #15 (CSP-prereq). */ if (u->nullable) fatal("nullable-global storage unimplemented (task #15)"); return (int)u->size; default: return 0; } } /* 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; } /* 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; } /* 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; } /* 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; } /* 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, root-main, empty-module}. Do NOT skip on `export`: * every exported decl (fn AND data) path-qualifies, so cross-package exports * can't collide under separate compilation (§7-A / #53). The only bare * symbols are @symbol FFI overrides and the ROOT unit's `main`. * Both stages must match exactly — ww2/ww3/ww4 byte-identity depends on it. */ static void mod_collect(Cg *c, Node *file) { mod_map = NULL; use_map = NULL; if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { /* M1 #22: record alias→path for the qualified-ref hint. */ if (d->kind == N_USE && d->str && d->usepath) { Use *u = amalloc(c->a, sizeof *u); u->alias = d->str; u->path = d->usepath; u->module = d->module; u->next = use_map; use_map = u; continue; } int isfn = (d->kind == N_FNDECL); int track = isfn || (d->kind == N_TYPEDECL) || (d->kind == N_DEF) || (d->kind == N_LET); if (!track) continue; /* §7-A / #53: exported non-fn decls (let/def/type) path-qualify * like fns — `export def MAX` becomes `.MAX`, not a bare * `MAX` that two packages could clash on under sep-compile. No * export skip: every decl with a module mangles identically. */ if (decl_has_ffisym(d)) continue; int bare = (d->module == NULL || d->module[0] == '\0'); /* #84: register bare-module (package-less `//ww:module-reset`) * FNS too — previously ALL bare decls were skipped here, so a * bare fn never entered mod_map and a bare-ident ref to it * first-matched an imported module's same-leaf fn (mod_lookup_ * for_fn) → a #40 residual / #263-class silent dead-dup. With * the bare entry present, mod_lookup_for_fn prefers it on a NULL * hint. Non-fn bare decls keep the legacy skip (their collision * class is outside #84's fn scope; pre-bind proven). */ /* #85 (deferred): the bare-NON-fn sibling (a package-less let/def/ * type leaf colliding with an imported same-kind export) needs a * different fix — mod_lookup (non-fn) is hint-LESS — and has no * corpus repro; retained skip until then (rule-11). */ if (bare && !isfn) continue; /* `main` is the linker entry-point convention. A `package main` * primary's decls are MODULED "main", so main is NOT bare here; * skip it (cgfn force-emits the bare `main` label). M1 #32: only * the ROOT main (imported==0) stays bare; an IMPORTED `fn main` * mangles on its path (closes #31's dup-main by construction). * #99: under sep a dep unit's main is imported==0 too (its body * is composed with a path-carrying `//ww:module-reset`, #57), so * imported==0 no longer means "root unit" per-unit. Gate on * !sep_isdep (wwiout==NULL <=> root/link-entry unit, #69) so only * the root's main stays bare; a dep's main mangles on its path. */ if (d->str && strcmp(d->str, "main") == 0 && !d->imported && !c->sep_isdep) continue; Mod *m = amalloc(c->a, sizeof *m); m->name = d->str; m->module = bare ? NULL : 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 decl. By-name first-match — used at non-fn ref sites with an * unambiguous leaf. Hint-disambiguated refs (and value globals where two * modules could share a leaf) go through mod_lookup_for_fn. */ 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 (m->module == NULL) { /* #84: a bare-module entry (now registered). A bare-ident * ref (hint==NULL: the caller is itself in the bare/root * module) resolves to it — bare wins over an imported * same-leaf fn, by construction, order-independent. With a * non-NULL hint the caller named a module, so a bare decl * is irrelevant: skip it, leaving the legacy first-imported * -match untouched (byte-id-neutral for moduled callers). */ if (hint == NULL) return NULL; continue; } if (hint != NULL && strcmp(m->module, hint) == 0) return m->module; if (first == NULL) first = m->module; } return first; } /* 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 `&`. */ 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; } 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 → .. * - 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); } /* 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)); } 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)); } 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. */ /* localslot — reserve a fresh stack slot (bump *frame, build the Local) * but DON'T link it into the lookup chain. #152: the N_LET case links the * binding only AFTER its initializer emits, so a self-shadowing init * (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in * the outer scope: harec check.c clet runs cexpr before scope_define). */ static Local * localslot(Cg *c, const char *name, int size, int *frame) { int al = 8; *frame = (*frame + size + al - 1) & ~(al - 1); Local *l = amalloc(c->a, sizeof *l); l->name = name; l->off = -*frame; l->next = NULL; return l; } static int localoff(Cg *c, Local **head, const char *name, int size, int *frame) { Local *l = localslot(c, name, size, frame); l->next = *head; *head = l; return l->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" * 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. */ static void cgexpr(Cg*, Node*, Local*); static void cgstmt(Cg*, Node*, Local**, int*); static int cgplaceaddr(Cg*, Node*, int, Local*); 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) { if (cg_dotchain_addr(c, src, dst, locals)) return 1; /* task #6: an N_INDEX link inside the dot chain * (handles[k].result) — the chain walker has no index hop * (AX-clean spill contract for cg_dotbase_addr); the place * resolver is the C4/#40 fallback, and the failed walk is * emission-free so the fallback starts clean. */ return cgplaceaddr(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; } /* task #6 sibling: slice-element / non-ident-array base * (use(sl[k])) — the same C4/#40 place-resolver fallback * as the N_DOT arm; the missed walk is emission-free. */ return cgplaceaddr(c, src, dst, locals); } 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; } } /* cg_agg_reg_store — the ONE register-cursor aggregate materialise: an * in-cap (<=24B, GP-class) struct/array/tuple already held in the * {AX,DX,CX} return cursor is stored into the place base_reg+disp. * full = sz/8 exact-8B MOVQ words land straight in (safe into any dest); * the sz%8 tail is one sized MOVB/MOVW/MOVL for {1,2,4} (BOTH branches). * Extracted from site E (the #10 template) so every narrow-tail * materialise funnels through one place — close-by-construction (#14). * * dest_padded forks ONLY the {3,5,6,7} tail (no GP sub-register exists * for those widths and w6a has no shift): * dest_padded == "the dest is a ceil-8/round8 slot (a scratch, or a * #75 let/local aggregate slot) so an 8B tail over-store stays * in-bounds." Post-#9: packed struct fields + array elements => 0; * let/local/scratch slots => 1. It CANNOT be derived — paddedness is * routing knowledge the caller owns. * 1 -> a single full MOVQ of the cursor tail eightbyte (= E's #10 * template; the over-store lands in the slot's pad). * 0 -> MOVQ the cursor tail eightbyte into the helper's own ceil-8 * @tagscr pad, then a sized aggcopy of the tail bytes into the * dest so a packed field / array element is never overrun. * PRECONDITION (caller-owned, stays per-site): the value is already in * AX/DX/CX, the dest base is resolved into base_reg+disp, and the * float-class (#165) / over-cap-sret (#234) loud-stops have already * fired. Clobbers SI/BX/AX only on the dest_padded==0 detour (the value * words are in memory by then). */ static void cg_agg_reg_store(Cg *c, Local **locals, int base_reg, int disp, int sz, int dest_padded) { 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(base_reg, disp + i * 8)); if (tail == 0) return; if (tail == 1 || tail == 2 || tail == 4) { int op = (tail == 1) ? A_MOVB : (tail == 2) ? A_MOVW : A_MOVL; ins2(c, op, areg(regs[full]), amem(base_reg, disp + full * 8)); return; } if (dest_padded) { ins2(c, A_MOVQ, areg(regs[full]), amem(base_reg, disp + full * 8)); return; } int pad = cg_tagscr_slot(c, locals, 8); ins2(c, A_MOVQ, areg(regs[full]), amem(D_BP, pad)); ins2(c, A_LEAQ, amem(D_BP, pad), areg(D_SI)); ins2(c, A_LEAQ, amem(base_reg, disp + full * 8), areg(D_BX)); cg_aggcopy(c, tail); } /* 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; /* Auto-deref: a *[N]T indexed base takes one pointer hop — * the (*hs)[k] form made implicit (checker already stamps * the element type). Load shape is the slice .ptr word's. */ int viaptr = 0; if (bu->kind == TY_PTR) { Type *p = type_chase_named(bu->sub); if (p && p->kind == TY_ARRAY) viaptr = 1; } if (!viaptr && 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; a *[N]T place holds the array address. */ if (bu->kind == TY_SLICE || viaptr) 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; } /* #6 — a module-GLOBAL `*struct` pointer base (`gp.f = v`, `gp.f += v`) * carries via_ptr=1 but has NO local slot: localfind returns 0, so the * enumerated via_ptr field-store arms emit `MOVQ (BP),BX` (deref the * saved BP = garbage → SEGV). wwstage routes every such case EXCEPT * plain-scalar-`=` through its F6 cgplaceaddr resolver (the scalar-only * dedicated arm, selfhost cgenexpr.ww; compound + non-scalar = the * deferred #60/#61). cstage's own F6 assign-resolver (cgen.c N_ASSIGN, * via cgplaceaddr) emits byte-identically. So decline those cases at the * enumerated arm's dispatch — they fall to the resolver, aligning cstage * UP to wwstage and restoring byte-id (handle where the resolver handles; * loud-stop, same message, where it loud-stops on tagged/float). The * plain-scalar-`=` struct-field case STAYS in the enumerated arm — its * #47 fix already matches wwstage's dedicated arm. Discriminant is * localfind-ABSENCE + let_islet (storage class), NEVER a boff==0 * sentinel: a param at offset 0 has localfind != 0 and stays local. */ static int global_ptr_field_decline(Node *lhs, int op, Local *locals) { if (lhs == NULL || lhs->kind != N_DOT || lhs->lhs == NULL) return 0; Node *base = lhs->lhs; if (base->kind == N_UN && base->op == TK_STAR) base = base->lhs; if (base == NULL || base->kind != N_IDENT) return 0; if (localfind(locals, base->str) != 0 || !let_islet(base->str)) return 0; Type *bu = type_chase_named(base->type); if (bu == NULL || bu->kind != TY_PTR) return 0; Type *su = type_chase_named(bu->sub); if (su == NULL) return 0; /* `(*gp).ptr/.len/.cap` pseudo-field: no struct field, always * resolver (wwstage loud-stops "unsupported assign target shape"). */ if (su->kind == TY_SLICE || su->kind == TY_STR) return 1; if (su->kind != TY_STRUCT) return 0; if (op != TK_ASSIGN) return 1; for (Tfield *fl = su->fields; fl; fl = fl->next) if (strcmp(fl->name, lhs->str) == 0) { Type *fu = type_chase_named(fl->type); int isf32 = 0; if (fld_isfloat(fl->type, &isf32)) return 1; if (fu && (fu->kind == TY_STRUCT || fu->kind == TY_ARRAY || fu->kind == TY_TUPLE || fu->kind == TY_STR || fu->kind == TY_SLICE || fu->kind == TY_TAGGED)) return 1; return 0; } return 0; } /* #6 (chained twin) — a multi-dot LHS (`gp.x.y = v`) rooted at a module- * GLOBAL pointer the walk dereferences: the chained value-struct walker * (cgen.c N_ASSIGN) conflates is_global with ptr_root and emits the same * `MOVQ (BP),CX` SEGV for plain `=`. Decline at entry → the F6 resolver * (byte-id with wwstage). A global VALUE-struct root (`gv.x.y`, root type * TY_STRUCT) and a LOCAL pointer root (localfind != 0) both stay in the * walker — they are already correct. */ static int global_ptr_chain_root_decline(Node *lhs, Local *locals) { Node *cur = lhs; while (cur && cur->kind == N_DOT) cur = cur->lhs; if (cur == NULL || cur->kind != N_IDENT) return 0; if (localfind(locals, cur->str) != 0 || !let_islet(cur->str)) return 0; Type *u = type_chase_named(cur->type); return (u != NULL && u->kind == TY_PTR); } /* 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, DST_PTR_SP = 3, }; 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 && localfind(*locals_p, src->str) != 0) { 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 if (src->kind == N_IDENT) { /* #44 (#263): a module-global tagged ident * source has no BP slot — land gi(SB) in SI * (aggarg_srcaddr) and copy the inner box into * slot+8. Pre-fix the BP copy read frame garbage * off 0(BP). ww half landed F8-c8. */ if (aggarg_srcaddr(c, src, D_SI, *locals_p)) { for (int k = 0; k < ssz; k += 8) { ins2(c, A_MOVQ, amem(D_SI, 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; /* #116: a NON-LITERAL tuple-typed source (ident, index, * deref) is no longer loud here — it routes to the * addressable block-copy arm just below the literal arm. */ } 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) { /* #47 gap-A: a tagged element rides its OWN box * (tag + payload, roundup8) per tuple slot — NOT one * 8B word. Mirror the checker's N_TTUPLE accumulation * (check.c:715-720): aggregate/tagged elem += its * box size rounded to 8; the store loop below boxes it * recursively (two-level widen). */ Type *eu = type_chase_named(e->type); if (eu && eu->kind == TY_TAGGED) total += ((int)eu->size + 7) & ~7; else 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) { /* #47 gap-A: a tagged element is its own tag+payload * box. Recurse so the inner box (tag@slot+0, * payload@slot+8) is built at the element's tuple- * payload offset, exactly as a top-level tagged-store * does — two-level widen (inner boxes here, outer tuple * tag stamped below). slot stride = roundup8(box). */ Type *eu = type_chase_named(e->type); if (eu && eu->kind == TY_TAGGED) { int ebox = ((int)eu->size + 7) & ~7; cg_widen_tagged_store(c, locals_p, e->type, e, D_BP, write_off + 8 + foff, ebox); foff += ebox; continue; } 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; } /* #116: a NON-LITERAL but ADDRESSABLE tuple source — a tuple IDENT * var, a slice/array INDEX (tbl[i]), or a DEREF (*p). The tuple in * memory uses the SAME tuple_eslot strides as the box payload the * literal loop above fills (a scalar 8B, a slice/str its 24B header, * a tagged element its boxed tag+payload), so the source-in-memory * layout already equals the box payload layout — the fill is a flat * block-copy of sum(tuple_eslot) bytes from the source address into * write_off+8, no re-slotting (a tagged element rides over as its * already-built box, so no recursion is needed). Kept loud (out of * scope): a CALL/sret result (the tuple sits at the sret address, * #40-kin) and a struct-field / array-literal-element source (loud * EARLIER at construction, #49 / #270-1c). A cast wrapping a * concrete-variant tuple (`(tbl[i]: ci)`) survived the widen-cast * peel above; its operand is the addressable expr. */ if (su && su->kind == TY_TUPLE) { Node *addrsrc = src; if (addrsrc->kind == N_CAST && addrsrc->lhs) addrsrc = addrsrc->lhs; if (addrsrc->kind != N_IDENT && addrsrc->kind != N_INDEX && !(addrsrc->kind == N_UN && addrsrc->op == TK_STAR)) fatal("cg_widen_tagged_store: tuple-typed source shape " "unwired (only the bare/cast tuple literal and the " "addressable ident/index/deref trio carry a full " "payload; see #116)"); int tag = cg_tag_for_variant(du, st); if (tag < 0) fatal("cg_widen_tagged_store: tuple-in-union variant " "tag unresolved (untyped/literal tuple element; " "see #242 / #241)"); /* The tuple's type-table size IS sum(tuple_eslot) under the * 8B-slot tuple layout (every walk takes its stride from * tuple_eslot; the type's size is their sum), so the payload * byte-count routes through the type table (rule 13) without * re-walking the elements — and matches the wwstage twin, whose * tuple tinfo carries no per-element params list. */ int total = (int)su->size; 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)"); if (!cgplaceaddr(c, addrsrc, D_SI, *locals_p)) fatal("cg_widen_tagged_store: addressable tuple source " "address unresolved (see #116)"); 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)); for (int k = 0; k < total; k += 8) { ins2(c, A_MOVQ, amem(D_SI, k), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8 + k)); } ins2(c, A_MOVQ, aimm(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 srcreg = D_BP, srcbase = soff; if (soff == 0) { /* #43 (#263): a module-global struct ident has * no BP slot — land g(SB) in SI (aggarg_srcaddr) * and copy the struct words into slot+8. Pre-fix * the BP copy read frame garbage off 0(BP). ww * half landed F8-c7. */ if (aggarg_srcaddr(c, src, D_SI, *locals_p)) { srcreg = D_SI; srcbase = 0; } } int k = 0; while (k + 8 <= ssz) { ins2(c, A_MOVQ, amem(srcreg, srcbase + 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(srcreg, srcbase + 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)); } /* #49: an f64/f32 payload sits in X0 (cgexpr left it there), * not AX — spill it through the stack so the callee reads the * real bits. A plain PUSHQ AX pushed whatever AX last held * (stale for a runtime float producer; only a const folder * leaves the bits in AX, which is why #48's no-payload-read arm * passed but #49's `d == 2.5` read did not). Both stages * (#263); wwstage cgenutil.ww scalar-variant twin. */ if (node_isfloat(src)) { int fmov = node_isf32(src) ? A_MOVSS : A_MOVSD; ins2(c, A_SUBQ, aimm(8), areg(D_SP)); ins2(c, fmov, areg(D_X0), amem(D_SP, 0)); } else { 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. * DST_PTR_SP — base = BX, reloaded via `MOVQ (SP), BX` with the * same reload cadence as DST_PTR_LOCAL. The dst is the * alloc-heap base saved by the alloc(value) arm's * `PUSHQ AX` (top-of-stack); cgexpr is stack-balanced * so (SP) keeps pointing at it across the field walk. * srcoff/name unused. C7c: recurse into a nested * struct/array/tuple field VALUE so alloc(Outer{x = * Inner{..}}) writes the inner leaves instead of * storing AX=0 over the whole inner slot. * * 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 routes through fldstoreop {1->MOVB, 2->MOVW, * 4->MOVL, else MOVQ}: the missing MOVW for fsz==2 over-stored a * 2-byte tail field past its slot into saved BP on a union-return * success variant (#15). Natural field offsets aren't 8-aligned, so * MOVQ-rely-on-ceil-8-pad (sound for #10's 8-aligned scratch) is * wrong here; the store must be width-sized. */ 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)); else if (mode == DST_PTR_SP) ins2(c, A_MOVQ, amem(D_SP, 0), 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)); else if (mode == DST_PTR_SP) ins2(c, A_MOVQ, amem(D_SP, 0), 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). * * The choke-point stores MOVQ for full 8B chunks plus a * sized tail (MOVL/MOVW/MOVB) by `tail = fsz%8`. * * Guard `fsz <= 24` (#14 widened from {0,1,2,4}): the * choke-point handles every in-cap tail incl. 3/5/6/7 (its * non-padded scratch detour, dest_padded=0 — a struct-lit * field is packed); >24B still falls through (sret deferred). * * 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) { 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)); else if (mode == DST_PTR_SP) ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX)); cg_agg_reg_store(c, locals_p, base_reg, disp + (int)foff, fsz, 0); 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 if (mode == DST_PTR_SP) ins2(c, A_MOVQ, amem(D_SP, 0), 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)); else if (mode == DST_PTR_SP) ins2(c, A_MOVQ, amem(D_SP, 0), 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)); else if (mode == DST_PTR_SP) ins2(c, A_MOVQ, amem(D_SP, 0), 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 if (mode == DST_PTR_SP) ins2(c, A_MOVQ, amem(D_SP, 0), 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)); else if (mode == DST_PTR_SP) ins2(c, A_MOVQ, amem(D_SP, 0), 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 = fldstoreop(ft, fsz); 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++; } } } /* cg_dotfield_combine — single-dot field compound combine. The old * field value is in BX, the rhs in AX; the result is left in AX. * PLUSEQ/MINUSEQ preserve the pre-#34 emission (byte-id); the other 8 * ops were silently DROPPED (the arm fell through to a plain store of * the rhs -> `s.f = rhs`, #34/#263). SLASHEQ/PERCENTEQ/LSHIFTEQ/RSHIFTEQ * need the lhs in AX and the divisor/count in CX, so swap (rhs AX->CX, * old BX->AX) first. Signed RSHIFTEQ uses SARQ, unsigned SHRQ (#136). * Mirrors wwstage cgdotfieldcombine — both stages emit identical asm. */ static void cg_dotfield_combine(Cg *c, int op, int unsignd) { switch (op) { case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); break; case TK_STAREQ: ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); break; case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); break; case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break; case TK_CARETEQ: ins2(c, A_XORQ, areg(D_BX), areg(D_AX)); break; case TK_SLASHEQ: 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)); ins1(c, A_DIVQ, areg(D_CX)); } else { ins0(c, A_CQO); ins1(c, A_IDIVQ, areg(D_CX)); } break; case TK_PERCENTEQ: 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)); ins1(c, A_DIVQ, areg(D_CX)); } else { ins0(c, A_CQO); ins1(c, A_IDIVQ, areg(D_CX)); } ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); break; case TK_LSHIFTEQ: ins2(c, A_MOVQ, areg(D_AX), areg(D_CX)); ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); ins2(c, A_SHLQ, areg(D_CX), areg(D_AX)); break; case TK_RSHIFTEQ: ins2(c, A_MOVQ, areg(D_AX), areg(D_CX)); ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); ins2(c, unsignd ? A_SHRQ : A_SARQ, areg(D_CX), areg(D_AX)); break; default: fatal("single-dot field compound: unknown op tk=%d " "(#34/rule-7)", op); } } /* cg_dotfield_hardstop — loud-stop a single-dot field compound on a * non-integer field (float/str/slice/tagged): the combine arm only * speaks integer ABI; pre-#34 these silently became `s.f = rhs`. * Mirrors the wwstage cgdotfieldhardstop gate. (#34/rule-7) */ static void cg_dotfield_hardstop(Type *ft) { int isf32 = 0; Type *u = type_chase_named(ft); if (u && u->kind == TY_TAGGED) fatal("single-dot field compound on tagged field not wired " "(#34/rule-7)"); if (u && u->kind == TY_STR) fatal("single-dot field compound on str field not wired " "(#34/rule-7)"); if (u && u->kind == TY_SLICE) fatal("single-dot field compound on slice field not wired " "(#34/rule-7)"); if (fld_isfloat(ft, &isf32)) fatal("single-dot field compound on float field not wired " "(#34/rule-7)"); } 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 = type_chase_named(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, mafn(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, mafn(c, n->str, c->cur_mod), areg(D_CX)); ins2(c, op, amem(D_CX, 0), areg(D_X0)); goto ident_done; } /* #140: a `!void` error-singleton spelled as a value * (`return too_long`, `let e = too_long`, * `e = too_long`, `f(too_long)`) is a tag-only variant: * the void payload has size 0 and no storage, so it is * neither a let nor a def — it reaches this generic * global-value fallthrough as a bare type-name. Emit * NOTHING; the enclosing widen arm (N_RETURN scalar arm * / cg_widen_tagged_store) stamps the variant tag. * Without this guard the load below emitted * `MOVQ main.(SB),AX` for a payload symbol that is * never defined → w6l undefined-reference. wwstage emits * tag-only already (the runtime-correct reference; align * cs UP). ken-139 oracle. */ { Type *vu = type_chase_named(n->type); if (vu && vu->kind == TY_VOID && !let_islet(n->str) && !def_isanydef(n->str)) 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, mafn(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, mafn(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 = type_chase_named(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 = type_chase_named(lt); if (lu && lu->kind == TY_FN) ins2(c, A_LEAQ, mafn(c, opnd->str, use_hint(c->cur_mod, 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, mafn(c, opnd->str, use_hint(c->cur_mod, 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 = 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) { 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; } 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) || def_isarraydef(base->str)) { /* #94: a def-array base has DATA at * masym(name) (emit_array_data, same as a * let) — its address is LEAQ name(SB), the * mirror of the let leg. Without this it * fell to the XORQ zero-base below = wild * pointer (&D[i] SEGV). */ 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; /* C1b: a whole str/slice loaded BY VALUE * through *str / *[]T — AX (the operand value) IS * the 24B {ptr,len,cap} header address. The scalar * load below pulled ONLY word0 (.ptr); .len/.cap * were then stored from stale BX/CX, so len(*p) read * garbage — byte-id-blind on both stages. Reuse the * same 3-word header load as the slice-FIELD / * N_INDEX str-element arms (cgslicehdr). */ if (ru && (ru->kind == TY_STR || ru->kind == TY_SLICE)) { cgslicehdr(c, D_AX); 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); /* #154: localfind→0 for a module global, but the * str header lives at name(SB), not BP+0. Mirror * #148's slice global branch (cgen.c:9082): LEAQ * the symbol into a base reg, push len then ptr. */ if (off == 0 && let_islet(n->rhs->str)) { ins2(c, A_LEAQ, masym(c, n->rhs->str), areg(D_BX)); ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); } else if (off == 0 && def_isanydef(n->rhs->str)) { /* A str `def` has no name(SB) header: cgexpr * materialises its literal as AX=ptr, BX=len. */ cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, areg(D_BX)); ins1(c, A_PUSHQ, areg(D_AX)); } else { 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); /* #154: see the rhs branch above — a module-global * str ident lives at name(SB), not BP+0. */ if (off == 0 && let_islet(n->lhs->str)) { ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_BX)); ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); } else if (off == 0 && def_isanydef(n->lhs->str)) { cgexpr(c, n->lhs, locals); ins1(c, A_PUSHQ, areg(D_BX)); ins1(c, A_PUSHQ, areg(D_AX)); } else { 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: { /* #145 (c1.5a): bulk slice-copy-assign into a range place * `s.arr[lo:hi] = bs` (LHS is N_SLICE). No legacy N_ASSIGN arm * catches N_SLICE (they gate INDEX/STAR/DOT/ident), so the * statement fell through to the scalar tail and emitted NOTHING * — a silent no-op, both stages, byte-id-green (#263-class). * Reuse the N_SLICE READ lowering: cgexpr(lhs) leaves AX = dst * ptr (base+lo*esz), BX = hi-lo (element count), CX = cap. The * element width is the SAME read-path esz (rule-13: chased base * element tinfo, [N]u8 -> 1); multiply BX by it for the byte * count, then a runtime-counted byte-granular copy from bs.ptr. * Byte loop because the length is RUNTIME — the #265/#268 memcpy * emitters are compile-time-sz unrolled and w6a has no REP/MOVSB. * Only plain `=`; a compound op on a range place is meaningless. * * Hare asserts len(bs)==hi-lo (ref/hare/path/stack.ha:72, * appendlit). We copy exactly hi-lo elems and do NOT runtime- * check len(bs). The length-equality assert is task #149 * (rule-7: documented, not a c2 blocker — appendlit's lengths * are equal by construction). */ if (n->op == TK_ASSIGN && n->lhs && n->lhs->kind == N_SLICE) { Node *sl = n->lhs; Node *sbase = sl->lhs; Type *sbu = type_chase_named(sbase ? sbase->type : NULL); int esz = (sbase && (sbase->kind == N_IDENT || sbase->kind == N_DOT || sbase->kind == N_ARRLIT) && sbu && sbu->sub) ? (int)sbu->sub->size : 1; cgexpr(c, sl, locals); /* AX=dst ptr, BX=hi-lo */ if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_DX)); ins2(c, A_IMULQ, areg(D_DX), areg(D_BX)); } ins1(c, A_PUSHQ, areg(D_AX)); /* dst ptr */ ins1(c, A_PUSHQ, areg(D_BX)); /* byte count */ cgexpr(c, n->rhs, locals); /* AX=src ptr */ ins2(c, A_MOVQ, areg(D_AX), areg(D_SI)); ins1(c, A_POPQ, areg(D_CX)); /* byte count */ ins1(c, A_POPQ, areg(D_DI)); /* dst ptr */ char *top = mklabel(c, "scpy"); char *end = mklabel(c, "scpe"); label(c, top); ins2(c, A_CMPQ, aimm(0), areg(D_CX)); ins1(c, A_JLE, abranch(end)); ins2(c, A_MOVB, amem(D_SI, 0), areg(D_AX)); ins2(c, A_MOVB, areg(D_AX), amem(D_DI, 0)); ins2(c, A_ADDQ, aimm(1), areg(D_SI)); ins2(c, A_ADDQ, aimm(1), areg(D_DI)); ins2(c, A_SUBQ, aimm(1), areg(D_CX)); ins1(c, A_JMP, abranch(top)); label(c, end); break; } /* 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)"); } /* A plain aggregate field-to-field assignment is a memory copy, * not a scalar expression/store. The legacy direct-field arms * below enumerate CALL, STRUCTLIT, and local IDENT producers; an * addressable N_DOT/N_INDEX/deref rhs fell through, so a 16-byte * time.instant copied only its first word. Resolve both places * through the existing generic address funnels and use the single * tail-aware aggregate copier. Calls/literals/unwraps stay on their * specialized ABI paths, and tagged/str/slice fields are excluded by * the destination type gate. */ if (n->op == TK_ASSIGN && n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && (n->lhs->lhs->kind == N_IDENT || n->lhs->lhs->kind == N_DOT || (n->lhs->lhs->kind == N_UN && n->lhs->lhs->op == TK_STAR)) && n->rhs) { Type *au = type_chase_named(n->lhs->type); int memrhs = n->rhs->kind == N_IDENT || n->rhs->kind == N_DOT || n->rhs->kind == N_INDEX || (n->rhs->kind == N_UN && n->rhs->op == TK_STAR); if (au && (au->kind == TY_STRUCT || au->kind == TY_ARRAY || au->kind == TY_TUPLE) && memrhs) { if (!cgplaceaddr(c, n->lhs, D_BX, locals)) fatal("aggregate field destination unresolved"); ins1(c, A_PUSHQ, areg(D_BX)); if (!aggarg_srcaddr(c, n->rhs, D_SI, locals)) fatal("aggregate field source unresolved"); ins1(c, A_POPQ, areg(D_BX)); cg_aggcopy(c, (int)au->size); break; } } /* #16: a single-dot aggregate-field unwrap store whose base * is a module-GLOBAL value-struct (`g.f = mk()!`) or a * CHAINED struct field (`o.m.f = mk()!`). The #12 single-dot * arm covered only a LOCAL / via-ptr base: it gated !is_global * and its enclosing N_DOT arm requires an N_IDENT local base, * so the global case fell to the generic single-word store * (DROPPED w1/w2) and the chained case never reached any field * arm (SILENT both-stage, byte-id blind). Route the destination * ADDRESS through cgplaceaddr (which resolves a global LEAQ root * and a chained deref+offset spine uniformly) and feed the SAME * {AX,DX,CX} producer-shift materialise. cgplaceaddr clobbers * AX/CX, so it runs BEFORE cgexpr(rhs) and the address is saved * across the call. In-cap struct field only (size<=24, #12 * scope, #14 widened from {0,1,2,4} — the choke-point handles * every tail incl. 3/5/6/7 via its non-padded scratch detour, * dest_padded=0 for a packed/chained field); a float-bearing / * over-cap success variant loud-stops at the producer. * Local/via-ptr single-dot stays on #12. */ if (n->op == TK_ASSIGN && n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && n->rhs && (n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP)) { Node *db = n->lhs->lhs; int chained = (db->kind == N_DOT); int globalbase = 0; if (db->kind == N_IDENT && localfind(locals, db->str) == 0 && let_islet(db->str)) { Type *dbu = type_chase_named(db->type); if (dbu && dbu->kind == TY_STRUCT) globalbase = 1; } Type *fu = type_chase_named(n->lhs->type); if ((chained || globalbase) && fu && fu->kind == TY_STRUCT && (int)fu->size <= 24) { int ssz = (int)fu->size; if (!cgplaceaddr(c, n->lhs, D_BX, locals)) fatal("#16: global/chained aggregate " "unwrap field dest unresolved " "(cgplaceaddr)"); ins1(c, A_PUSHQ, areg(D_BX)); cgexpr(c, n->rhs, locals); ins1(c, A_POPQ, areg(D_BX)); cg_agg_reg_store(c, &locals, D_BX, 0, ssz, 0); break; } } /* 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 (!global_ptr_field_decline(n->lhs, n->op, locals) && 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); /* A global has no frame slot; treating its lookup miss as * offset zero writes .len at the caller return address. */ int is_global = boff == 0 && !via_ptr && let_islet(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 if (is_global) { ins2(c, A_LEAQ, masym(c, base->str), 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)); cg_dotfield_combine(c, n->op, 0); } 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 if (is_global) { ins2(c, A_LEAQ, masym(c, base->str), 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) { 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. */ /* #12: an unwrap `b.f = mk()!` (direct) / `p.f = * mk()!` (via-ptr) rides the same {AX,DX,CX} shape * (producer shift) — admit it alongside N_CALL. Gated * !is_global: the wwstage single-dot arms cover a direct * / via-ptr base only; a global `g.f` unwrap stays on * its pre-existing path in BOTH stages (out of scope, * not regressed). Float/over-cap loud-stop at the * producer. #14 widened size<=24 from {0,1,2,4}: the * choke-point handles every tail incl. 3/5/6/7 via its * non-padded scratch detour (dest_padded=0 — a packed * single-dot field). */ if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STRUCT && (int)str_fu->size <= 24 && n->rhs && (n->rhs->kind == N_CALL || ((n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP) && !is_global))) { int ssz = (int)str_fu->size; cgexpr(c, n->rhs, locals); 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; } cg_agg_reg_store(c, &locals, base_reg, base_disp, ssz, 0); 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 + 4 <= ssz) { ins2(c, A_MOVL, amem(D_BP, soff + k), areg(D_AX)); if (via_ptr || is_global) ins2(c, A_MOVL, areg(D_AX), amem(D_BX, foff + k)); else ins2(c, A_MOVL, areg(D_AX), amem(D_BP, boff + foff + k)); k += 4; } if (k + 2 <= ssz) { ins2(c, A_MOVW, amem(D_BP, soff + k), areg(D_AX)); if (via_ptr || is_global) ins2(c, A_MOVW, areg(D_AX), amem(D_BX, foff + k)); else ins2(c, A_MOVW, areg(D_AX), amem(D_BP, boff + foff + k)); k += 2; } if (k + 1 <= ssz) { ins2(c, A_MOVB, amem(D_BP, soff + k), areg(D_AX)); if (via_ptr || is_global) ins2(c, A_MOVB, areg(D_AX), amem(D_BX, foff + k)); else ins2(c, A_MOVB, areg(D_AX), amem(D_BP, boff + foff + k)); k += 1; } 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)); cg_dotfield_hardstop(f->type); cg_dotfield_combine(c, n->op, type_isunsigned(f->type)); } /* 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; } if (via_ptr) { if (boff == 0 && let_islet(base->str)) { /* #47 (inverse): a GLOBAL *struct * base. The pointer VALUE lives at * gp(SB), not a BP slot — load it * from the symbol. Pre-fix MOVQ * (BP),BX derefed saved BP → SEGV. * Mirrors the already-correct wwstage * global-base load; cs==ww. (Other * via_ptr field arms keep the latent * BP-deref — inverse siblings #60/#61, * ken-triage.) */ ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX)); ins1(c, A_POPQ, areg(D_AX)); } else { 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 = type_chase_named(elemt); Type *struct_t = NULL; int viaptr = 0; if (elemu && elemu->kind == TY_PTR) { Type *inner = type_chase_named(elemu->sub); 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 = type_chase_named(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; } /* #58: a TAGGED field of an indexed array * element (`xs[i].f = v`). The scalar store * below would write the raw unboxed rhs into * the TAG slot — never boxing, never writing * the payload (box-corruption, the #38a * write-twin). BOX (mirror the #24 tagged- * field-assign tag lookup, cg_tag_for_variant) * + STORE spine (mirror the co-located str/ * slice 3-word arm above): cgexpr the payload, * spill it across the index/address * computation, compute &xs[i]->BX, then store * the variant tag (constant) at foff+0 and the * scalar payload at foff+8. Only a SCALAR- * payload variant (box <=16B) store is wired * here. A >16B / multi-word / float-payload * union field IS constructible (a wide box, * built via a NARROW variant — not unbuildable * as earlier triage assumed; the #54/#23 * construction hole fires only on STRUCT- * LITERAL payloads), but its box+memcpy store * arm is not yet wired, so it LOUD-STOPS rather * than silently corrupting the box (rule 7, the * #41 untested-arm trap), byte-id-neutral. * Reachable + pinned expect-loud (test/wcc/944 * cfail rows). When #114 wires them, that commit * replaces these stops with the real str/slice/ * struct/float/>32B box+memcpy emission + value * pin rows. */ if (n->op == TK_ASSIGN && fu && fu->kind == TY_TAGGED) { int bsz = (int)fu->size; Type *st = n->rhs ? n->rhs->type : NULL; int h2_isf32 = 0; if (bsz > TUPLE_GPCAP * 8) fatal("#58: >32B tagged-" "field indexed store " "unreachable until #114"); if (bsz > 16) fatal("#58: multi-word " "tagged-field indexed " "store unreachable " "until #114"); if (fld_isfloat(st, &h2_isf32)) fatal("#58: float-payload " "tagged-field indexed " "store unreachable " "until #114"); 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)); int v58tag = cg_tag_for_variant(fu, st); ins2(c, A_MOVQ, aimm(v58tag < 0 ? 0 : v58tag), amem(D_BX, foff + 0)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + 8)); break; } /* #11: an in-cap aggregate-returning CALL into * an AGGREGATE field of an indexed element * `arr[i].f = mk()`. The scalar default below * stores only AX (eb0), dropping DX/CX — a SILENT * both-stage field-drop, the field-of-indexed twin * of C2c's whole-element arr[i]=mk() arm * (cgen.c :6764). Scratch-first materialise of the * AX/DX/CX return (not a PUSHQ spill — keeps the * CALL at the frame's 16B alignment and survives an * idx that itself contains a call), reuse the scalar * arm's &arr[i]->BX address computation verbatim, * then word-copy scratch to foff(BX). In-cap only * (cg_sret_retsize==0); over-cap sret-into-field * LOUD-STOPS (#11c/#234, task #8) and a float- * bearing aggregate LOUD-STOPS (#165/#171 — a pure- * float return eightbyte rides X0/X1 which the GP * AX/DX/CX cursor cannot read). Mirrors wwstage * cgenexpr.ww. */ /* #12: an unwrap `arr[i].f = mk()!` rides the * same {AX,DX,CX} shape (producer shift) — admit * it alongside N_CALL; the #11b non-call arm below * excludes the unwrap kinds so this arm is the * sole handler. cg_sret_retsize keys off ft (the * field = success variant type), so an over-cap * unwrap stays loud here too. */ if (n->op == TK_ASSIGN && n->rhs && (n->rhs->kind == N_CALL || n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP)) { if (cg_sret_retsize(ft) > 0) fatal("#11c/#234: over-cap " "(sret) aggregate field " "receive arr[i].f=mk() " "unwired (cs!=ww; task #8)"); /* struct_float_class mirrors the * return-side SSE routing (#171a); a * field typed DIRECTLY as a tuple misses * it yet the bare-tuple return routes * floats to tuple_sse_seq — guard it too * so neither stage silently stores X0 * garbage through the GP cursor. */ int sclass11[2]; int sse11 = struct_float_class(ft, sclass11) > 0; if (!sse11) { Type *tu11 = type_chase_named(ft); if (tu11 && tu11->kind == TY_TUPLE) for (Tparam *p11 = tu11->params; p11; p11 = p11->next) { int f32_11; if (fld_isfloat( p11->type, &f32_11)) sse11 = 1; } } if (sse11) fatal("#11/#165: float-bearing " "aggregate field receive " "arr[i].f=mk() unwired (SSE " "return eightbyte; #171)"); if (fsz > 8) { /* The sub-8 tail materialise stores the FULL * 8-byte register (MOVQ) into a ceil-8-padded * scratch (cg_tagscr_slot -> local_alloc rounds * to 8): the over-stored high bytes land in the * pad and the scratch->dest copy reads only fsz * bytes, so every in-cap tail (incl. 3/5/6/7) is * exact without an immediate-shift cascade (w6a * has no SHRQ $imm). #10. #14: folded into the * choke-point (dest_padded=1 — the scratch IS a * ceil-8 slot so the tail eightbyte over-store * lands in its pad; byte-identical to the prior * inline form). Mirrors wwstage cgenexpr.ww. */ int scr11 = cg_tagscr_slot(c, &locals, fsz); cgexpr(c, n->rhs, locals); cg_agg_reg_store(c, &locals, D_BP, scr11, fsz, 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)); } 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)); int k11 = 0; for (; k11 + 8 <= fsz; k11 += 8) { ins2(c, A_MOVQ, amem(D_BP, scr11 + k11), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + k11)); } if (k11 + 4 <= fsz) { ins2(c, A_MOVL, amem(D_BP, scr11 + k11), areg(D_AX)); ins2(c, A_MOVL, areg(D_AX), amem(D_BX, foff + k11)); k11 += 4; } if (k11 + 2 <= fsz) { ins2(c, A_MOVW, amem(D_BP, scr11 + k11), areg(D_AX)); ins2(c, A_MOVW, areg(D_AX), amem(D_BX, foff + k11)); k11 += 2; } if (k11 + 1 <= fsz) { ins2(c, A_MOVB, amem(D_BP, scr11 + k11), areg(D_AX)); ins2(c, A_MOVB, areg(D_AX), amem(D_BX, foff + k11)); k11 += 1; } break; } /* fsz<=8 in-cap aggregate returns * wholly in AX; the scalar default's * single store is the correct receive. */ } /* #11b: a NON-call AGGREGATE source into an * aggregate field of an indexed element * `arr[i].f = src` (src an ident / .g / index). * The scalar default below loads only the * source's FIRST word into AX and stores ONE * word — dropping the rest (a SILENT both-stage * member drop, the non-call twin of the #11 * in-cap CALL arm above; byte-id blind). Unlike * #11's GP AX/DX/CX cursor the source is a * MEMORY address, so the shared mem-to-mem * cg_aggcopy transports EVERY byte: a sub-8 * tail and float bits copy verbatim, so NO * tail/float/over-cap loud-stop is needed here * (those #11 stops were register-cursor * artefacts). Reuse the block's own &arr[i] * spine (proven for [N]S / *[N]S / []S by the * sibling arms) -> BX + foff, then funnel * through aggarg_srcaddr (src -> SI) + * cg_aggcopy — the ONE copy emitter the non- * indexed bases use (DRY). fsz natural * (ft->size). Mirrors wwstage cgenexpr.ww. */ /* #12: exclude the unwrap kinds — the #11 arm * above is their sole handler (they ride the * {AX,DX,CX} register cursor, NOT a source * address). */ if (n->op == TK_ASSIGN && n->rhs && n->rhs->kind != N_CALL && n->rhs->kind != N_TRYUNW && n->rhs->kind != N_TRYPROP && fu && (fu->kind == TY_STRUCT || fu->kind == TY_ARRAY || fu->kind == TY_TUPLE) && fsz > 8) { 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)); if (foff != 0) ins2(c, A_ADDQ, aimm(foff), areg(D_BX)); /* spill dest across the source-address * resolution (the #270-1b order: * aggarg_srcaddr clobbers BX). */ ins1(c, A_PUSHQ, areg(D_BX)); if (!aggarg_srcaddr(c, n->rhs, D_SI, locals)) fatal("#11b: aggregate field receive " "arr[i].f=src - source shape unwired " "(rule-7)"); ins1(c, A_POPQ, areg(D_BX)); cg_aggcopy(c, fsz); 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. */ Type *fchk33 = type_chase_named(ft); if (fchk33 && fchk33->kind == TY_TAGGED) fatal("arr[i].field compound on " "tagged field not wired (#33/rule-7)"); if (fchk33 && fchk33->kind == TY_STR) fatal("arr[i].field compound on " "str field not wired (#33/rule-7)"); if (fchk33 && fchk33->kind == TY_SLICE) fatal("arr[i].field compound on " "slice field not wired (#33/rule-7)"); if (ft && type_isfloat(ft)) fatal("arr[i].field compound on " "float field not wired (#33/rule-7)"); 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)); int unsignd33 = 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 (unsignd33) ins2(c, A_MOVQ, aimm(0), areg(D_DX)); else ins0(c, A_CQO); ins1(c, unsignd33 ? A_DIVQ : A_IDIVQ, areg(D_CX)); break; case TK_PERCENTEQ: if (unsignd33) ins2(c, A_MOVQ, aimm(0), areg(D_DX)); else ins0(c, A_CQO); ins1(c, unsignd33 ? 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, unsignd33 ? A_SHRQ : A_SARQ, areg(D_CX), areg(D_AX)); break; default: fatal("arr[i].field compound: " "unknown op tk=%d (#33/rule-7)", n->op); } ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); break; } } } } /* Chained `.field = v` where 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 `.field = v` where 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 (!global_ptr_chain_root_decline(n->lhs, locals) && 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. * #14 widened fsz<=24 from {0,1,2,4}: the choke- * point handles every tail incl. 3/5/6/7 via its * non-padded scratch detour (dest_padded=0 — a * packed chained field). */ if (fu && fu->kind == TY_STRUCT && fsz <= 24 && n->rhs && n->rhs->kind == N_CALL) { cgexpr(c, n->rhs, locals); 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; } cg_agg_reg_store(c, &locals, base_reg, base_off, fsz, 0); 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; } /* Descending 4/2/1 sized tail, not a * 4/1-then-MOVQ: ssz is the field's * natural (non-slot-padded) struct size, * so a tail of {2,3,5,6,7} packs at its * own stride (struct{u16,[8]u8}=10 has * tail 2). The old `tail==1?MOVB:MOVQ` * fell every other tail to an 8B MOVQ that * over-writes past the destination FIELD — * a @packed neighbour gets clobbered (the * depth-≥2 chained `t.m.l = s` wrote s's * slop over t.m.z, exit 0 not 222) and * cstage diverged from wwstage's sized * MOVW (gate-blind cs≠ww). Aligns cstage * UP to the wwstage 8/4/2/1 ladder * (cgenexpr.ww:10083) and cg_aggcopy. * #107 class. */ if (k + 4 <= ssz) { ins2(c, A_MOVL, amem(D_BP, soff + k), areg(D_AX)); if (via_cx) ins2(c, A_MOVL, areg(D_AX), amem(D_CX, total_off + k)); else ins2(c, A_MOVL, areg(D_AX), amem(D_BP, base_disp + total_off + k)); k += 4; } if (k + 2 <= ssz) { ins2(c, A_MOVW, amem(D_BP, soff + k), areg(D_AX)); if (via_cx) ins2(c, A_MOVW, areg(D_AX), amem(D_CX, total_off + k)); else ins2(c, A_MOVW, areg(D_AX), amem(D_BP, base_disp + total_off + k)); k += 2; } if (k + 1 <= ssz) { ins2(c, A_MOVB, amem(D_BP, soff + k), areg(D_AX)); if (via_cx) ins2(c, A_MOVB, areg(D_AX), amem(D_CX, total_off + k)); else ins2(c, A_MOVB, areg(D_AX), amem(D_BP, base_disp + total_off + k)); k += 1; } 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 = type_chase_named(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. * #20: only a PLAIN `=` widens. A COMPOUND `gs[i] OP= v` * on a tagged element is nonsense — without the * TK_ASSIGN gate it dropped the OP and plain-stored the * RHS (silent miscompile, cs!=ww). The gate lets a * compound fall to the #133 indexed-compound arm below, * whose elem_tagged guard rejects loud (the byte-id * twin of wwstage, which already rejected via #133). */ if ((is_arr || is_sl || is_ptr) && elem_tagged && n->op == TK_ASSIGN) { 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) { int boff = localfind(locals, base->str); int isglobal = (boff == 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, boff), areg(D_BX)); } else { 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; } /* C2c / #31-G: an IN-CAP aggregate-returning CALL into an * indexed element `a[i] = mk()`. The #234 arm above only * fires for an OVER-cap (sret/memory) return; the #270-1b * arm below excludes N_CALL (it copies from a source * ADDRESS, which a call result has none). An in-cap (≤24B) * struct/array/tuple return leaves AX/DX/CX per the #4 * cgreturn ABI but fell through to the 1-word scalar store * (AX only) — dropping DX/CX (a silent field-drop, the * documented-but-unfixed #31-G). Materialise the return into * a frame scratch (the AX/DX/CX receive shape at the N_LET * call-rhs site, cgen.c:3434), THEN compute &a[i] and word- * copy scratch → dest. Scratch-first (not a dest spill across * the call) keeps the call at the frame's natural alignment. * In-cap only (cg_sret_retsize==0). */ /* #12: an unwrap `mk()!` / `r?` whose success variant is * an in-cap struct/array rides the SAME {AX,DX,CX} payload * shape as the call return (the producer shift materialises * it); admit it alongside N_CALL. cg_sret_retsize(esub)==0 * holds for an in-cap variant; the float/over-cap loud- * stops live at the producer. */ 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_CALL || n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP) && cg_sret_retsize(esub) == 0) { int scr = cg_tagscr_slot(c, &locals, esz); cgexpr(c, n->rhs, locals); /* call → AX/DX/CX */ /* AX/DX/CX → scratch (mirror cgen.c:3434 receive). */ cg_agg_reg_store(c, &locals, D_BP, scr, esz, 1); /* dest &a[i] → BX (mirror #121 / #270-1b resolve) */ 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)); 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)); ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); /* word-copy scratch → dest (tail-aware, mirror * #270-1b copy). */ int k = 0; for (; k + 8 <= esz; 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)); } if (k + 4 <= esz) { ins2(c, A_MOVL, amem(D_BP, scr + 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, scr + 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, scr + k), areg(D_AX)); ins2(c, A_MOVB, areg(D_AX), amem(D_BX, k)); k += 1; } break; } /* #121 (write-face of leg-b): a tuple-LITERAL rhs into an * indexed element `a[i] = (3,4)`. A literal has no source * ADDRESS, so the ident/dot/deref copy arm below can't reach * it — it fell to the 1-word scalar store tail (word0 only; * the read-luck masked it until leg-b's correct read). The * indexed-tuple-element STORE is the write-face of leg-b's * read (one round-trip, #58/#135 precedent). Materialise the * literal into a frame scratch via the cglet in-cap cursor * fill (cg_tuple_lit_to_cursor + tuple_store), then word-copy * scratch → &a[i]. NARROW: only the N_TUPLE-literal rhs (the * rest of the #270/#31/#32 store family stays deferred). In- * cap only (cg_sret_retsize==0); over-cap tuple stores keep * the existing sret/loud paths. */ if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN && esubu && esubu->kind == TY_TUPLE && esz > 8 && n->rhs->kind == N_TUPLE && base->kind == N_IDENT && cg_sret_retsize(esub) == 0) { int scr = cg_tagscr_slot(c, &locals, esz); cg_tuple_lit_to_cursor(c, &locals, n->rhs, esubu); int gpcur = 0, ssecur = 0, eoff = 0, ef32; for (Tparam *p = esubu->params; p; p = p->next) { int isflt = fld_isfloat(p->type, &ef32); tuple_store(c, p->type, gpcur, ssecur, scr + eoff); if (isflt) ssecur++; else gpcur += tuple_eslot(p->type) / 8; eoff += tuple_eslot(p->type); } /* dest &a[i] → BX (mirror the #270-1b resolve) */ 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)); 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)); ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); /* word-copy scratch → dest (tuple esz is 8-aligned, * tuple_eslot 8B floor). */ for (int k = 0; k < esz; 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; } /* #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, * N_INDEX via cgplaceaddr, `*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_INDEX) || (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 { /* task #6: the chain walker declines an * N_INDEX link (emission-free) — its * result was unchecked here, so the copy * below read through a stale SI. Fall to * the generic place resolver. */ int dok = 0; if (n->rhs->kind == N_DOT) dok = cg_dotchain_addr(c, n->rhs, D_SI, locals); if (!dok && !cgplaceaddr(c, n->rhs, D_SI, locals)) fatal("indexed aggregate " "assignment source " "unresolved"); } 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; } } /* #21: a COMPOUND op on a whole tagged-union IDENT (`g OP= v` * with g:(int|bool)) is nonsense — the ident load-combine- * store tail below (the N_IDENT compound arm) reads and writes * one word of the {payload,tag} box, corrupting the tag. Reject * loud here, the ident twin of the #18 deref / #133 index * rejects; the byte-id twin of the wwstage guard. Plain `=` * (the tagged-ident reassign arm just below) is untouched. */ if (n->lhs && n->lhs->kind == N_IDENT && n->op != TK_ASSIGN && n->lhs->type) { Type *itu = type_chase_named(n->lhs->type); if (itu && itu->kind == TY_TAGGED) fatal("ident compound on tagged not wired " "(#21/rule-7)"); } /* 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) { /* #41 (#263): a module-global * tagged ident has no BP slot — * LEAQ g(SB),BX then store * tag+payload off BX (mirror the * global-struct-field tagged arm * above + the local arm below). * Pre-fix this break dropped the * store entirely; ww half landed * in F8-c5. */ if (let_islet(n->lhs->str)) { ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_BX)); cg_widen_tagged_store(c, &locals, lu, n->rhs, D_BX, 0, (int)lu->size); } 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); /* #17: tagged-union pointee. The scalar tail below stores * only the first 8B (fldstoreop MOVQ) — rhs lands in the * tag word and the payload is dropped, corrupting the * union. Materialise the widened value (tag + payload * words, nullable fold, tag remap) into a tag scratch via * cg_widen_tagged_store, then word-copy scratch → *p. * Mirrors the index-element tagged arm (cgen.c:6487-6531); * the dest is just the pointer, so no base-addr dance. */ if (vt && vt->kind == TY_TAGGED) { int ssz = (int)vt->size; 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, vt, n->rhs, D_BP, scr, ssz); cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */ ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); 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; } /* `*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) { /* #14 widened sz<=24 from {0,1,2,4}: the * choke-point handles every in-cap tail incl. * 3/5/6/7 via its dest_padded=0 scratch detour * (always correct; kept symmetric with the * wwstage twin's local struct/array reassign). */ if (off != 0 && au->kind != TY_TUPLE && sz <= 24) { cgexpr(c, n->rhs, locals); cg_agg_reg_store(c, &locals, D_BP, off, sz, 0); 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)); /* #14: route through the choke-point so a * 3/5/6/7 tail is no longer dropped to a lone * MOVB (was silent both-stage). A global symbol * is non-padded => dest_padded=0 (its scratch * detour copies exactly `tail` bytes). */ cg_agg_reg_store(c, &locals, D_DI, 0, sz, 0); 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 * * 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, mafn(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 = type_chase_named(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 = type_chase_named(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) { /* C7c: route the heap field-fill through the shared * structlit helper in DST_PTR_SP mode — base reloaded * from the just-pushed heap ptr at (SP). The prior * inline loop had only float/str/scalar arms, so a * nested struct/array/tuple field VALUE (an inner * N_STRUCTLIT / N_ARRLIT) fell to the scalar tail and * stored AX=0 over the whole inner slot, dropping its * leaves. The helper recurses to arbitrary depth and * reuses the tagged/call/str/slice/array/agg arms, * closing the class symmetrically with the BP-rel and * global structlit sites. Byte-identical to the old * inline loop for the float/str/scalar fields the * corpus actually allocs (same (SP) reload, disp=0). */ cg_structlit_fill(c, &locals, u, v, DST_PTR_SP, 0, NULL, 0); } 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 = type_chase_named(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 && vn->kind != N_CALL) { 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; } if (vn->kind == N_CALL) { /* #34 close: a struct CALL rvalue * element. Evaluate the call FIRST * into a fresh per-site scratch * (the STRUCTLIT arm's pre-grow * discipline: arg exprs must see * the pre-append len, and the * value must survive rt_ensure), * receiving by the N_LET matrix * (#23 sret / #171a float / #107 * odd-tail / #169 GP), then grow, * slot, sized-ladder scratch-> * slot. */ int st_scr = local_alloc(c, &locals, "@appendstructscr", esz, cg_frame); int sclass[2], snb; if (cg_sret_retsize(esub) > 0) { cg_sret_dest_off = st_scr; cgexpr(c, vn, locals); cg_sret_dest_off = 0; } else if ((snb = struct_float_class(esubu, sclass)) > 0) { cgexpr(c, vn, 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, st_scr + e * 8)); ssecur++; } else { ins2(c, A_MOVQ, areg(tuple_rseq[gpcur]), amem(D_BP, st_scr + e * 8)); gpcur++; } } } else if (esz < 8 && (esz % 8 == 3 || esz % 8 == 5 || esz % 8 == 6 || esz % 8 == 7)) { cgexpr(c, vn, locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, st_scr)); } else { cgexpr(c, vn, locals); cg_agg_reg_store(c, &locals, D_BP, st_scr, esz, 1); } cg_append_grow(c, sn_direct, sn_off, sn_scr, esz); cg_append_slot(c, sn_direct, sn_off, sn_scr, esz, D_BX); int ck = 0; for (; ck + 8 <= esz; ck += 8) { ins2(c, A_MOVQ, amem(D_BP, st_scr + ck), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, ck)); } if (ck + 4 <= esz) { ins2(c, A_MOVL, amem(D_BP, st_scr + ck), areg(D_AX)); ins2(c, A_MOVL, areg(D_AX), amem(D_BX, ck)); ck += 4; } if (ck + 2 <= esz) { ins2(c, A_MOVW, amem(D_BP, st_scr + ck), areg(D_AX)); ins2(c, A_MOVW, areg(D_AX), amem(D_BX, ck)); ck += 2; } if (ck + 1 <= esz) { ins2(c, A_MOVB, amem(D_BP, st_scr + ck), areg(D_AX)); ins2(c, A_MOVB, areg(D_AX), amem(D_BX, ck)); ck += 1; } continue; } if (vn->kind == N_STRUCTLIT) { /* #59 (#50's eval-order kin): * fill the literal into a fresh * per-SITE scratch (must survive * rt_ensure + a nested append in a * field expr; the @apptagscr * rationale, #25/#31) BEFORE the * grow, so the field exprs see the * pre-grow len. Then grow, slot, * raw-copy scratch->slot (mirror * the #50 tagged arm above). */ int st_scr = local_alloc(c, &locals, "@appendstructscr", esz, cg_frame); cg_structlit_fill_bp(c, &locals, esubu, vn, st_scr); cg_append_grow(c, sn_direct, sn_off, sn_scr, esz); cg_append_slot(c, sn_direct, sn_off, sn_scr, esz, D_BX); /* Descending 8/4/2/1 ladder, not an * 8B-word loop: a plain struct's esz * rounds to maxalign (check.c:916), * not 8, so a sub-8B / non-8B-multiple * element packs at its own stride — * an 8B copy of the last slot writes * past the slice buffer (the tagged * arm above is safe only because boxes * are 8B-padded; #59). Mirrors the * N_IDENT source arm below. */ int ck = 0; for (; ck + 8 <= esz; ck += 8) { ins2(c, A_MOVQ, amem(D_BP, st_scr + ck), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, ck)); } if (ck + 4 <= esz) { ins2(c, A_MOVL, amem(D_BP, st_scr + ck), areg(D_AX)); ins2(c, A_MOVL, areg(D_AX), amem(D_BX, ck)); ck += 4; } if (ck + 2 <= esz) { ins2(c, A_MOVW, amem(D_BP, st_scr + ck), areg(D_AX)); ins2(c, A_MOVW, areg(D_AX), amem(D_BX, ck)); ck += 2; } if (ck + 1 <= esz) { ins2(c, A_MOVB, amem(D_BP, st_scr + ck), areg(D_AX)); ins2(c, A_MOVB, areg(D_AX), amem(D_BX, ck)); ck += 1; } 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_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}; Type *argparam[64] = {0}; /* #68: declared param type per arg */ { Tparam *p = callee_params; for (int i = 0; i < argcount; i++) { if (p == NULL) break; argparam[i] = p->type; 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); } /* C default argument promotion (#14): an f32 passed in the * variadic region of a C-variadic call must widen to f64 — the * callee reads it with va_arg(double) (8B). The region is every * arg past the fixed params (the C `...` adds no Tparam). The * push promotes via CVTSS2SD and stores a full 8B double; the * pop then reloads MOVSD and counts it as exactly one SSE reg. * Fixed params keep their declared width. Ref SysV §3.5.7; * C99 §6.5.2.2p6. */ int promote_f32[64] = {0}; if (cu && cu->kind == TY_FN && cu->variadic) { int nfixed = 0; for (Tparam *p = callee_params; p; p = p->next) nfixed++; for (int i = nfixed; i < argcount; i++) if (args[i] && node_isf32(args[i])) promote_f32[i] = 1; } /* #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); /* #148: localfind→0 for a module global, but the * header lives at name(SB), not BP+0. Mirror the * N_SLICE arm's isglobal dispatch below: LEAQ the * symbol into a base reg, push 16/8/0 off it. */ if (off == 0 && let_islet(args[i]->str)) { ins2(c, A_LEAQ, masym(c, args[i]->str), areg(D_BX)); ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); continue; } /* 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 if (base->kind == N_DOT && bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) { /* slice/str FIELD base — the arg twin * of the cgexpr N_SLICE default-hi * arm above; same re-eval-for-header * rationale. */ cgexpr(c, base, locals); ins2(c, A_MOVQ, areg(D_BX), 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); /* #150: a module-global struct source — off==0 * is the localfind footgun (GAP-A.ptr/#231 * family); the BP loads below read the stack * frame, not main.g(SB). Resolve the global base * into BX (the #129-A.2 struct-global LEAQ shape) * and copy ALL eightbytes from it. */ if (off == 0 && (let_islet(args[i]->str) || def_isstructdef(args[i]->str))) { ins2(c, A_LEAQ, masym(c, args[i]->str), areg(D_BX)); if (sz > 8) { ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); } ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); continue; } 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; } /* #68: a tuple-LITERAL arg derives its DECLARED tuple * type from the callee PARAM (the #57 decl wire, * extended to call-arg send), so a declared-tagged * element's concrete rvalue widens into the box cursor; * the restage + push then key on the param tuple type, * not the literal's element-constructed type. */ Type *paramtup = NULL; if (args[i] && args[i]->kind == N_TUPLE) { Type *pcu = argparam[i] ? type_chase_named(argparam[i]) : NULL; if (pcu && pcu->kind == TY_TUPLE) paramtup = pcu; } if (paramtup) cg_tuple_lit_to_cursor(c, &locals, args[i], paramtup); else cgexpr(c, args[i], locals); Type *tuparg_push = paramtup ? paramtup : 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. * * #14: a variadic-region f32 is promoted to f64 * here (CVTSS2SD in X0) and spilled MOVSD, so the * slot holds a real 8B double for the pop. */ int fmov = promote_f32[i] ? A_MOVSD : op_for(args[i], A_MOVSD, A_MOVSS); if (promote_f32[i]) ins2(c, A_CVTSS2SD, areg(D_X0), areg(D_X0)); 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. */ /* #68: a declared-tagged element graduates * to a real widen — the decl-aware send * (cg_tuple_lit_to_cursor over the param * tuple type) already left the box words in * the cursor, so the count + tuple_store * below carry them. Only the param-decl * literal path is wired; nested tuple/ * struct/array stay rule-7 loud, as does a * tagged element with no param decl (the * cursor was filled stamped-keyed). */ 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 && !paramtup))) 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). A #14 * variadic-region f32 was promoted to f64 * at push, so its slot reloads MOVSD. */ int fmov = promote_f32[i] ? A_MOVSD : 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_decl(args[i], argparam[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). * * #68: a tuple-LITERAL arg drains over the PARAM * tuple element widths (node_tuplearg_decl), so a * declared-tagged element's box words pop into the * arg cursor together with the i64 that follows — * the literal's element-constructed types undercount * a wide box. Mirrors the param-aware send. */ 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 §3.5.7: a variadic call must set AL to the number of * vector (XMM) regs used to pass the variable float args — the * callee gates its xmm-save-area stores on `test %al,%al`, so a * wrong AL makes va_arg(double) read garbage. fi is the XMM * cursor (capped at 8 above). cstage formerly hardcoded AL=0, * correct only for zero-float variadic calls. Ref ref/qbe/ * amd64/sysv.c:384. MOVL-imm has no w6a encoding, so the imm→reg * MOVQ idiom carries it (AL = low byte, fi ≤ 8). */ if (cu && cu->kind == TY_FN && cu->variadic) { if (fi > 0) ins2(c, A_MOVQ, aimm(fi), areg(D_AX)); else 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, use_hint(c->cur_mod, 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 = type_chase_named(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 && let_islet(s->str) && !is_nullable && su && su->kind == TY_TAGGED) { /* #87: a tagged-union GLOBAL scrutinee — the box lives * in static DATA at name(SB), not the BP frame. * localfind would return 0 and the dispatch would read * saved BP as the tag (SEGV). The PLAIN-tagged twin of * #78's SB-resolution: LEAQ the address and copy the * box into a scratch slot the dispatch indexes off BP. * `is`/case-let binds read the same scratch. */ sl_off = localoff(c, &locals, "@match_spill", slot_size, cg_frame); ins2(c, A_LEAQ, mafn(c, s->str, c->cur_mod), areg(D_AX)); 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 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. #29: gated below on a * CONFIRMED-LOCAL base — a global base (`match (g.field)`) * has localfind==0, so 0+field.offset would land in the * saved-BP/return-addr region; it falls through to the * spill `else`, which resolves g(SB). */ 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; } } } int boff = f ? localfind(locals, s->lhs->str) : 0; if (f && !(boff == 0 && let_islet(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 = type_chase_named(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; u = type_chase_named(u); /* 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; r = type_chase_named(r); 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; } } /* #6 (Mech B): a slice success variant shares str's 24B * {ptr,len,cap} header in the tagged ABI {DX,CX,R8}; the same * shuffle serves both. A struct/aggregate success uses a * different {AX,DX,CX} eightbyte ABI and stays on the bare * MOVQ DX,AX fallthrough (separate latent, task #12) — keep * this gate str||slice-specific. */ int success_is_str = type_isstr(succ_t) || type_isslice(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. * * #12: a general-aggregate (struct/array) success * variant rides the SAME in-cap {AX,DX,CX} payload * shuffle — the union return packs the payload as raw * GP words past the outer tag. Pre-#12 it matched no * arm and fell to the bare MOVQ DX,AX below, * materialising only w0 (w1/w2 dropped) — a SILENT * both-stage word-drop. A float-bearing aggregate rides * X0/X1 (the SSE return-class) which this GP cursor * cannot reach, so LOUD-STOP it (mirror #11/#165). */ if (stu && ((stu->kind == TY_TAGGED && !stu->nullable) || stu->kind == TY_STRUCT || stu->kind == TY_ARRAY)) { if (agg_has_float(stu)) fatal("#12/#165: float-bearing " "aggregate success variant unwrap " "(S|e)! rides SSE X0/X1; GP cursor " "unwired"); 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; u = type_chase_named(u); /* 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; } } /* #6 (Mech B): a slice success variant shares str's 24B * {ptr,len,cap} header in the tagged ABI {DX,CX,R8}; the same * shuffle serves both. A struct/aggregate success uses a * different {AX,DX,CX} eightbyte ABI and stays on the bare * MOVQ DX,AX fallthrough (separate latent, task #12) — keep * this gate str||slice-specific. */ int success_is_str = type_isstr(succ_t) || type_isslice(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). #12: a struct/array success variant * rides the same in-cap {AX,DX,CX} shuffle; a float- * bearing aggregate LOUD-STOPS (X0/X1, GP cursor can't * reach it). Full WHY at the N_TRYPROP twin. */ if (stu && ((stu->kind == TY_TAGGED && !stu->nullable) || stu->kind == TY_STRUCT || stu->kind == TY_ARRAY)) { if (agg_has_float(stu)) fatal("#12/#165: float-bearing " "aggregate success variant unwrap " "(S|e)! rides SSE X0/X1; GP cursor " "unwired"); 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; u = type_chase_named(u); 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 = type_chase_named(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 = type_chase_named(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 (s && s->kind == N_IDENT && let_islet(s->str)) { /* #46 (#263): a module-global tagged ident — * cgexpr loads only the tag word (MOVQ g(SB),AX), * so the DX/CX spill below would write * uninitialized payload. Copy the whole box from * g(SB)+0/+8[/+16] into the spill so the * tag-check + payload load index off memory like * a local. ww half landed F8-c3. */ ins2(c, A_LEAQ, masym(c, s->str), areg(D_AX)); 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 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 = type_chase_named(tt); Type *ft = n->lhs ? n->lhs->type : NULL; Type *fu = type_chase_named(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 = type_chase_named(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 = type_chase_named(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, use_hint(c->cur_mod, 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, use_hint(c->cur_mod, 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, mafn(c, n->str, use_hint(c->cur_mod, n->lhs->str)), areg(D_AX)); } else { ins2(c, A_LEAQ, mafn(c, n->str, use_hint(c->cur_mod, 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 || fu->kind == TY_SLICE)) { /* str/slice leaf: load all three header * words into (AX=ptr, BX=len, CX=cap) * so the value follows the canonical * slice-rhs convention. str IS []u8 — * the same 24B {ptr,len,cap} header. #29: * the str arm here used to load only * ptr+len (cap dropped → a junk strlit * before the chain left CX stale); merged * so both load all three, both stages * (#263). 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) { /* The array's backing pointer = address of its * element 0 (array.ptr ≡ &A[0], drew #13). A LOCAL * array IS its frame slot — LEAQ off(BP). A module * GLOBAL (let or def array) lives at name(SB): off==0 * here would LEAQ the frame's first slot = garbage * (*p→0), the GAP-A.ptr silent miscompile. Same * off==0/global base-selection as the def-array index * base (cgen.c:4367) and #231/#48. */ if (off == 0 && (let_islet(n->lhs->str) || def_isarraydef(n->lhs->str))) ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_AX)); else 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, and a slice is the same 24B * {ptr,len,cap} header — load all three words 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). #28: * pre-fix the gate was str-only, so a SLICE tuple * element fell to the fldloadop tail (one ptr word; * len/cap stale). base is BP (frame) or CX (global * base — CX written LAST so it survives the +0/+8). */ if (fu && (fu->kind == TY_STR || fu->kind == TY_SLICE)) { 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 = type_chase_named(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); /* #15 — a module-GLOBAL ptr has no local * slot (off==0); load its pointer VALUE from * the data slot via SB, else amem(BP,0) derefs * the saved BP (read twin of the #6 store fix). */ if (off == 0 && let_islet(dot_lhs->str)) ins2(c, A_MOVQ, mafn(c, dot_lhs->str, c->cur_mod), areg(D_BX)); else 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); /* #15 — module-GLOBAL ptr base: load its pointer * VALUE from the data slot via SB (off==0 = no * local slot), else amem(BP,0) derefs the saved * BP (read twin of the #6 store fix). */ if (off == 0 && let_islet(dot_lhs->str)) ins2(c, A_MOVQ, mafn(c, dot_lhs->str, c->cur_mod), areg(D_BX)); else 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 = type_chase_named(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 = type_chase_named(lt); if (lu && lu->kind == TY_PTR && lu->sub) { Type *inner = type_chase_named(lu->sub); 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 = type_chase_named(elemt); Type *struct_t = NULL; int viaptr = 0; if (elemu && elemu->kind == TY_PTR) { Type *inner = type_chase_named(elemu->sub); 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 = type_chase_named(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; } /* #58: a TAGGED field of an indexed array * element (`xs[i].f`). AX holds &xs[i]; load * the box cursor (AX=tag, DX/CX/R8=payload) * mirroring cg_tagged_memread's ≤32B * convention, tag LAST (it clobbers the base * AX). Without this arm the field fell to the * scalar load below, reading only the tag word * and leaving the payload cursor (DX) stale * (`xs[i].f as T` read garbage; #38a INDEX- * spine residual). >32B box: a wide-box union * (largest variant >32B) IS constructible via a * NARROW variant (not unbuildable as earlier * triage assumed; #54/#23 fires only on STRUCT- * LITERAL payloads), but the mem-based read (LEAQ * foff(AX),AX, cg_tagged_memread:639) is not yet * wired here — so this arm LOUD-STOPS rather than * silently reading a truncated box (rule 7, the * #41 untested-arm trap), byte-id-neutral. * Reachable + pinned expect-loud (test/wcc/944 * cfail rows). When #114 wires it, that commit * replaces this with the LEAQ box-address * emission + a >32B value pin row. */ if (fu && fu->kind == TY_TAGGED) { int bsz = (int)fu->size; if (bsz > TUPLE_GPCAP * 8) fatal("#58: >32B tagged-field " "indexed read unreachable " "until #114"); if (bsz > 24) ins2(c, A_MOVQ, amem(D_AX, foff + 24), areg(D_R8)); if (bsz > 16) ins2(c, A_MOVQ, amem(D_AX, foff + 16), areg(D_CX)); if (bsz > 8) ins2(c, A_MOVQ, amem(D_AX, foff + 8), areg(D_DX)); 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; } } } /* #121 leg (a): `tbl[i].N` — a positional FIELD of an indexed * TUPLE element. The struct branch above handles struct / ptr-to- * struct elements; a tuple element fell through to the read- * resolver and died LOUD ("unsupported field-read shape"). * Resolve &tbl[i] via the place-spine (cgplaceaddr, the #116 * mechanism) into BX→AX, then read the field at addr+foff reusing * the per-element-kind arms the N_IDENT tuple-field block wires * (cgen.c TY_TUPLE arm): str-triple / float-X0 / fn-or-scalar * fldloadop. Narrow (rob Q3): tagged / nested-aggregate fields * stay LOUD (no fold-6 consumer; the #58/#117 untested-arm * hazard). Mirrors selfhost cgenexpr.ww's leg-(a) twin. */ if (n->lhs && n->lhs->kind == N_INDEX && n->str) { Type *eu = type_chase_named(n->lhs->type); if (eu && eu->kind == TY_TUPLE) { int idx = 0; for (const char *q = n->str; *q; q++) idx = idx * 10 + (*q - '0'); Tparam *tp = eu->params; int foff = 0; while (idx > 0 && tp) { foff += tuple_eslot(tp->type); tp = tp->next; idx--; } if (tp != NULL) { Type *fu = type_chase_named(tp->type); if (fu && (fu->kind == TY_TAGGED || fu->kind == TY_STRUCT || fu->kind == TY_TUPLE || fu->kind == TY_ARRAY)) fatal("#121: aggregate/tagged tuple-" "element field read off an " "indexed base unwired"); if (!cgplaceaddr(c, n->lhs, D_BX, locals)) fatal("#121: indexed tuple base " "not place-resolvable"); ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); int tf32 = 0; if (fld_isfloat(tp->type, &tf32)) { ins2(c, tf32 ? A_MOVSS : A_MOVSD, amem(D_AX, foff), areg(D_X0)); goto dot_done; } if (fu && (fu->kind == TY_STR || fu->kind == TY_SLICE)) { 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 fsz = (int)(tp->type ? tp->type->size : 8); int op = fldloadop(tp->type, 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 (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 = type_chase_named(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; } /* #121 leg (b): whole TUPLE element `let e = tbl[i]`. * The in-cap tuple receive (cglet) reads the SysV register * cursor (tuple_rseq words L→R); without this arm the * element fell to the scalar load below = word0 only * (str.len→0, later words garbage — silent both-wrong- * identical, #263). Fill gptotal cursor words from the * element address (BX); descending so AX (= tuple_rseq[0]) * loads last, mirroring the tagged arm. Over-cap (> the GP * cursor) leaves the ADDRESS in AX (the sret / * cg_tagged_memread convention) — the cglet over-cap arm * louds it (no live consumer). */ if (esubu && esubu->kind == TY_TUPLE) { int nw = 0; for (Tparam *p = esubu->params; p; p = p->next) nw += tuple_eslot(p->type) / 8; if (nw > TUPLE_GPCAP) { ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); break; } for (int k = nw - 1; k >= 0; k--) ins2(c, A_MOVQ, amem(D_BX, k * 8), areg(tuple_rseq[k])); 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; } /* #121 leg (b) via fallback base: whole TUPLE element. AX holds * the element address — copy to BX (the cursor fill into AX * clobbers it), then fill the gptotal cursor words. Over-cap * leaves the ADDRESS in AX (cglet louds it). Twin of the * N_IDENT-base arm above. */ if (esubu && esubu->kind == TY_TUPLE) { int nw = 0; for (Tparam *p = esubu->params; p; p = p->next) nw += tuple_eslot(p->type) / 8; if (nw > TUPLE_GPCAP) break; /* AX already = element addr */ ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); for (int k = nw - 1; k >= 0; k--) ins2(c, A_MOVQ, amem(D_BX, k * 8), areg(tuple_rseq[k])); 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 if (base && base->kind == N_DOT && bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) { /* slice/str FIELD base (#252's slice twin): the old * fall-through emitted $0 — a silent 0/negative len on * BOTH stages, byteid-blind. Re-evaluate the field * read for its header (a pure place: call inners * loud-reject upstream; cgexpr's N_DOT slice/str arms * leave AX=ptr, BX=len, CX=cap for every supported * inner — ident, chain, (*p), arr[i]) and take .len. * All scratch regs are dead at the hi step. */ cgexpr(c, base, locals); ins2(c, A_MOVQ, areg(D_BX), 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; /* #152: reserve the slot now (frame bump + nested-let * offsets stay stable) but defer linking n->str into the * lookup chain until AFTER the init emits — see letlink. */ Local *letloc = localslot(c, n->str, sz, frame); int off = letloc->off; 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)); goto letlink; } } /* 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)); goto letlink; } /* 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); } goto letlink; } /* #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); goto letlink; } 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)); goto letlink; } /* 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); goto letlink; } /* 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; goto letlink; } /* 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++; } } goto letlink; } } /* #107: a struct/array call-receive whose size is a single * eightbyte (< 8B) but NOT one of the sized-tail widths * {1,2,4} — i.e. sz ∈ {3,5,6,7}, reachable via @packed (#51, * e.g. @packed{u8,u32}=5) and natural sub-8 maxalign-1 shapes * (struct{u8,u8,u8}=3) — is ONE INTEGER eightbyte returned in * RAX (ref/qbe/amd64/sysv.c retr: the n*88) and * cstage emitted NOTHING — the CALL was silently DROPPED, while * wwstage emitted CALL + MOVQ AX via its generic let fallback * (cgenstmt.ww:3007): gate-blind cs≠ww AND a silent miscompile. * Aligns cstage UP to wwstage's SysV-correct single-MOVQ * receive. Task #107. */ if (n->rhs && n->rhs->kind == N_CALL && lu && (lu->kind == TY_STRUCT || lu->kind == TY_ARRAY) && sz < 8 && (sz % 8 == 3 || sz % 8 == 5 || sz % 8 == 6 || sz % 8 == 7)) { cgexpr(c, n->rhs, *locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); goto letlink; } 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); cg_agg_reg_store(c, locals, D_BP, off, sz, 1); goto letlink; } /* 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); goto letlink; } /* 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)); } goto letlink; } } /* #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; } goto letlink; } /* 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 > 0) { /* `let x: T;` with no rhs for a composite (str/slice/ * tuple/struct/tagged/ARRAY). Zero the slot so reads after * the bare let see {0...} rather than whatever the stack * already held. * * #84: arrays were excluded here (`!TY_ARRAY`), so a * dirtied-stack `let a: [3]int;` read garbage — BOTH * stages, both-wrong-IDENTICAL, gate-blind (#263). Go-zero * (user ruling): arrays zero-fill like every other * composite. Extent is lu->size (chased ABI size, rule-13 * — never a hardcoded count×elemsize). The unrolled * word/dword/byte run mirrors the composite path; the * largest real local array ([256]u8) is 32 MOVQs. * * #16: the gate was `sz > 8`, so a SUB-8 aggregate * (`let c: [3]u8;` = 3, a 3-byte struct, etc.) matched * neither this arm nor the `sz == 8` MOVQ-$0 arm above and * fell through to NOTHING — the exact stack-garbage read * ken's bytes verdict pinpointed (ltrim_cases' `let c: * [3]u8;`). Widening to `sz > 0` routes 1..7-byte slots * through the same MOVL/MOVB tail; the run already sizes * itself to any extent. (sz == 8 stays on the immediate * MOVQ $0 above; sz == 0 — `[0]T` — needs no stores.) */ 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; } } letlink: /* #152: link the binding into the lookup chain AFTER its * initializer emits, so a self-shadowing init (`let x = * f(x)`) resolves x in the OUTER scope. Hare evals the init * in the outer scope (harec check.c clet: cexpr before * scope_define); localslot reserved the frame slot above so * `off` and nested-let offsets are already stable. */ letloc->next = *locals; *locals = letloc; 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 && localfind(*locals, n->lhs->str) != 0) { /* 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. * * #42 (#263): only a LOCAL ident has a * BP slot. A module-global struct ident * (localfind==0) falls to the addr-copy * else below — aggarg_srcaddr lands * g(SB) in SI. Pre-fix this read frame * garbage off 0(BP). ww half landed * F8-c6. */ 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)"); /* #121 leg (c): for-range over a module-GLOBAL slice/str/array * base SEGV's today — the init + per-iteration base resolution * below assume a frame-local slot (localfind), so a global * let/def base reads saved-BP as the .ptr/.len (SEGV; divergent * cs≠ww asm). LOUD-STOP symmetric both stages (byte-id-neutral — * no asm; converges the divergence by rejecting; segfault→ * compile-error is pure improvement). The fix is the N_INDEX * isglobal base resolution (LEAQ/MOVQ name(SB)) ported into the * for-range spine — a DISTINCT mechanism from leg (a)/(b)'s * element-address machinery, with its own scalar/str/array/ * destructure global-base test surface. Filed as a #121 sibling; * off fold-6's path (fold-6 needs only leg (a)). */ if (slc && slc->kind == N_IDENT && localfind(*locals, slc->str) == 0 && (let_islet(slc->str) || def_isarraydef(slc->str))) fatal("#121: for-range over a module-global " "slice/array base unwired (global-base " "resolution gap)"); 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); } 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"); /* #42: at the cap, fail loud rather than silently push a wrong * break/continue target (the ww twin in cgenstmt.ww guards too). */ if (nloops >= LOOP_MAX) fatal("cgen: loop nesting too deep"); 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++) { /* #40 (#263): a str/slice/struct destructure * binding (24B header / aggregate, sz>8) copies * its FULL extent — the single fldloadop word * truncated a slice binding to its .ptr, dropping * .len/.cap (both stages identically, byte-id- * WRONG; F7-c4 fixed only the STRIDE). Same * word-run + sized-tail idiom as the non- * destructure aggregate copy above. */ int bsz = binds[b].sz; if (bsz > 8) { int k = 0; for (; k + 8 <= bsz; k += 8) { ins2(c, A_MOVQ, amem(D_BX, binds[b].foff + k), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, binds[b].off + k)); } if (k + 4 <= bsz) { ins2(c, A_MOVL, amem(D_BX, binds[b].foff + k), areg(D_AX)); ins2(c, A_MOVL, areg(D_AX), amem(D_BP, binds[b].off + k)); k += 4; } if (k + 2 <= bsz) { ins2(c, A_MOVW, amem(D_BX, binds[b].foff + k), areg(D_AX)); ins2(c, A_MOVW, areg(D_AX), amem(D_BP, binds[b].off + k)); k += 2; } if (k + 1 <= bsz) { ins2(c, A_MOVB, amem(D_BX, binds[b].foff + k), areg(D_AX)); ins2(c, A_MOVB, areg(D_AX), amem(D_BP, binds[b].off + k)); k += 1; } continue; } 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)); } /* #42: at the cap, fail loud rather than silently push a wrong * break/continue target (the ww twin in cgenstmt.ww guards too). */ if (nloops >= LOOP_MAX) fatal("cgen: loop nesting too deep"); 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; 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; /* #64: a tuple-LITERAL rhs carries a DECLARED tuple type (built * from the lvalue binding types) into the cursor fill, so a * declared-tagged element's concrete rvalue widens into the box * instead of riding the decl-less stamped-keyed route — the #57 * decl wire extended past N_LET/N_RETURN to destructure-reassign. * A `_` lvalue is never type-stamped (the checker skips it) — * fall back to the rhs literal element type for its cursor * stride so the next element stays aligned (harec `_` advance; * pinned by the R3 control row). */ int litrhs = (n->rhs && n->rhs->kind == N_TUPLE); Tparam *declp = NULL; if (litrhs) { Tparam **dpp = &declp; Tparam *tpw = tp0; for (Node *l = n->list; l; l = l->next) { Tparam *dpn = amalloc(c->a, sizeof *dpn); dpn->name = NULL; dpn->type = l->type ? l->type : (tpw ? tpw->type : NULL); dpn->next = NULL; dpn->variadic = 0; *dpp = dpn; dpp = &dpn->next; if (tpw) tpw = tpw->next; } Type *decltt = newtype(c->a, TY_TUPLE); decltt->params = declp; cg_tuple_lit_to_cursor(c, locals, n->rhs, decltt); } else { cgexpr(c, n->rhs, *locals); } Tparam *recvp = litrhs ? declp : tp0; 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 = recvp; 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 = recvp; 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: /* #40: at the cap, fail loud rather than silently drop the * deferred call (the ww twin in cgenstmt.ww fatals too). */ if (ndefers >= DEFER_MAX) fatal("cgen: too many defers in one function"); 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 */ 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. M1 #32: * the ROOT-unit main (imported==0) is the bare `_start` entry — emit * it bare directly, mirroring mod_collect's skip. This is irreducibly * name-based: a `package main` primary's decls are MODULED "main" * (the package clause sets cur_mod; cmd/ww/main.c:394), so main is * NOT a bare-module decl — removing this carve-out mangles it to * `main.main` (undefined `main`). #84's bare-module handling is the * orthogonal axis (a package-LESS `//ww:module-reset` fn, cur_mod * NULL), resolved in mod_lookup_for_fn, NOT here. #99: gate on * !sep_isdep — under sep a dep unit's main is imported==0 (path- * carrying `//ww:module-reset`, #57); only the root/link-entry unit * (wwiout==NULL, #69) keeps the bare label. */ if (fn->str && strcmp(fn->str, "main") == 0 && !fn->imported && !c->sep_isdep) text->to = asym("main"); else text->to = mafn(c, fn->str, c->cur_mod); text->from.offset = 0; /* framesize patched below */ emit(c, text); 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),""`. 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_fn(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); /* Forward declaration: the struct/array member emitters call the shared * tagged-payload core (#19 option A), defined alongside emit_tagged_data * further down. */ static int emit_tagged_bytes(FILE *out, Type *u, Node *rhs, int sz, 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); /* #19 option A: a non-nullable tagged-union field rides the * shared (tag,payload) core at the field slot size, mirroring the * scalar tagged global — NOT the int emitter, which mis-folds the * payload into the tag word. Wide/struct/non-foldable payload * loud-rejects (task #30 sub-item, rule 7). v is non-NULL here * (the absent-field zero-fill is handled above). */ if (fu && fu->kind == TY_TAGGED && !fu->nullable) { if (!emit_tagged_bytes(out, fu, v, fsz, 1)) fatal("emit_struct_lit_bytes: tagged-union " "struct-field '%s' static-init needs a " "zero/int payload; wide (str/slice) or struct " "payload is deferred (task #30, rule 7)", f->name ? f->name : "?"); pos = fstart + (u64)fsz; continue; } 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_fn(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 = type_chase_named(etype); int esz = etype ? (int)etype->size : 1; int alen = (int)u->alen; /* #19 option A: a non-nullable tagged-union element rides the shared * (tag,payload) core at the full slot stride esz, mirroring the scalar * tagged global — NOT the int emitter. A wide/struct/non-foldable * payload element loud-rejects (task #30 sub-item, rule 7). A nullable * `(*T|void)` element is a 1-word fold, not a tag box — left to the * existing int path (task #15). */ if (eu && eu->kind == TY_TAGGED && !eu->nullable) { int idx = 0; Node *last_ev = NULL; for (Node *e = rhs->list; e && idx < alen; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) break; if (!emit_tagged_bytes(out, eu, e, esz, 0)) fatal("emit_array_lit_bytes: tagged-union array " "element static-init needs a zero/int " "payload; wide (str/slice) or struct payload " "is deferred (task #30, rule 7)"); last_ev = e; idx++; } if (emit_phase == 0) return 1; idx = 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; } emit_tagged_bytes(out, eu, e, esz, 1); idx++; } for (; idx < alen; idx++) { if (repeat && last_ev) emit_tagged_bytes(out, eu, last_ev, esz, 1); else for (int b = 0; b < esz; b++) emit_data_byte(out, 0); } return 1; } 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. * * Always emits into DATAW (writable): A_DATAR requires its holder be a * DATAW slot (w6a asm.c:362), so both `let` and a read-only `def [N]str` * (#8/GAP-B) park their backing here — the section bit is the reloc-holder * constraint, not a mutability grant (def immutability stays checker- * enforced). 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 = type_chase_named(etype); if (eu == NULL || eu->kind != TY_STR) return 0; /* #8/GAP-B: a str-element array's backing ALWAYS lives in DATAW * (writable section), regardless of the caller's let/def directive — * each element carries an A_DATAR ptr-reloc to its _S_ rodata row, and * w6a requires a DATAR holder be a DATAW slot (asm.c:362). The passed * `directive` ("DATA" for a def, "DATAW" for a let) is therefore * IGNORED here; the emit below hardcodes DATAW. A `def [N]str` stays * immutable — the checker rejects writes to a def; DATAW is only the * reloc-holder placement, not a mutability grant (rule-8 placement * detail). Pre-fix this gate skipped the def path → no DATA block → * w6l undefined 'main.C' (#270 lineage; int-def is plain DATA, no * holder constraint, so it was unaffected). */ (void)directive; 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_fn(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_tagged_bytes — raw sz-byte static-init payload for a tagged-union * value: variant tag@+0 (8B), int payload@+8 (8B), zero-pad to sz. NO * directive open/close, NO reloc — emits exactly sz bytes via * emit_data_byte at the current cursor. Handles zero (NULL rhs) + a * foldable int payload only; a wide (str/slice) payload needs a DATAR the * raw core cannot place inside an already-open aggregate directive, and a * struct/non-foldable payload has no scalar form — both return 0 WITHOUT * emitting, and the caller loud-rejects (task #30 wide/struct sub-item). * `u` is the type-chased TY_TAGGED union type. emit_phase 0 = validate * only; 1 = emit. rob's EXTRACT ruling (#19 option A): the scalar wrapper * emit_tagged_data and the aggregate member branches (emit_array_lit_bytes * / emit_struct_lit_bytes) share this raw core so a nested tagged member * rides the SAME (tag,payload) SSoT as a scalar tagged global. Mirror of * wwstage emittaggedbytes. */ static int emit_tagged_bytes(FILE *out, Type *u, Node *rhs, int sz, int emit_phase) { if (u == NULL || u->kind != TY_TAGGED) return 0; if (rhs == NULL) { if (emit_phase) for (int i = 0; i < sz; i++) emit_data_byte(out, 0); return 1; } Node *r = rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r == NULL) return 0; int tag = cg_tag_for_variant(u, r->type); if (tag < 0) return 0; if (type_isstr(r->type) || type_isslice(r->type)) return 0; u64 v; if (!fold_int_literal(r, &v)) return 0; if (emit_phase) { for (int i = 0; i < 8; i++) emit_data_byte(out, (u8)(((u64)tag >> (i * 8)) & 0xff)); for (int i = 0; i < 8; i++) emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); for (int i = 16; i < sz; i++) emit_data_byte(out, 0); } return 1; } /* emit_tagged_data — module-level `let g: (T0 | T1 | ...) = v;` static * init (#87). The static DATA must byte-MIRROR a runtime LOCAL tagged box * of the same type (the SSoT pin, rob §3): tag word at +0 (the const- * selected variant's index, cg_tag_for_variant — the same routine the * runtime widen + match dispatch key on), the payload at +8, zero-padded * to the union's box size. So `let g: (i32|str) = 42` and a local * `let l: (i32|str) = 42` read byte-identically. int and str-literal * variants are wired (the shapes Hare stdlib uses, ref/hare/time/chrono/ * utc.ha:44); any other variant payload returns 0 and the caller loud- * stops (rule 7) — never the pre-#87 silent no-DATA + saved-BP read. */ static int emit_tagged_data(FILE *out, Cg *c, const char *name, const char *module, Type *t, Node *rhs) { Type *u = type_chase_named(t); if (u == NULL || u->kind != TY_TAGGED || u->nullable) return 0; int sz = (int)u->size; const char *sym = mod_mangle_fn(c, name, module); if (rhs == NULL) { fprintf(out, "DATAW %s(SB),\"", sym); emit_tagged_bytes(out, u, rhs, sz, 1); fputs("\"\n", out); return 1; } Node *r = rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r == NULL) return 0; int tag = cg_tag_for_variant(u, r->type); if (tag < 0) return 0; int wide = type_isstr(r->type) || type_isslice(r->type); if (wide) { /* str/slice variant: {ptr placeholder, len, cap} at +8 with a * DATAR patching the ptr word, mirroring the runtime box * (LEAQ _S_n into ptr@+8, len@+16, cap@+24). */ if (r->kind != N_STRLIT) return 0; u64 len = r->strlen; fprintf(out, "DATAW %s(SB),\"", sym); for (int i = 0; i < 8; i++) emit_data_byte(out, (u8)(((u64)tag >> (i * 8)) & 0xff)); for (int i = 0; i < 8; i++) emit_data_byte(out, 0); for (int i = 0; i < 8; i++) emit_data_byte(out, (u8)((len >> (i * 8)) & 0xff)); for (int i = 0; i < 8; i++) emit_data_byte(out, (u8)((len >> (i * 8)) & 0xff)); for (int i = 32; i < sz; i++) emit_data_byte(out, 0); fputs("\"\n", out); if (len > 0) { const char *lab = intern_strlit(c, r->str, r->strlen); fprintf(out, "DATAR %s+8(SB),%s(SB)\n", sym, lab); } return 1; } /* int/zero payload via the shared raw core; validate (phase 0) * BEFORE opening the directive so a non-foldable rhs returns 0 * without leaving a half-written DATAW. */ if (!emit_tagged_bytes(out, u, rhs, sz, 0)) return 0; fprintf(out, "DATAW %s(SB),\"", sym); emit_tagged_bytes(out, u, rhs, sz, 1); fputs("\"\n", out); return 1; } /* node_fnptr_sym — if `ev` (casts already peeled by the caller) is the * address-of a top-level fn (`&f`), return its mangled TEXT symbol; NULL * otherwise. The detect-half of the FIRST &fn→DATAR reloc machinery * (#117 slice-row + #119 scalar-global); mirrors the address-of-fn codegen * arm (TY_FN at the N_UN TK_AMP ident, cgen.c N_UN TK_AMP). */ static const char * node_fnptr_sym(Cg *c, Node *ev) { if (ev == NULL || ev->kind != N_UN || ev->op != TK_AMP) return NULL; Node *opnd = ev->lhs; /* #124: a cross-module `&mod.fn` — opnd is an N_DOT whose base is an * untyped SK_USE module ident (NULL/ty_err); the leaf chases TY_FN. * Mangle the leaf with the MODULE ident (not cur_mod) so the reloc * targets the same TEXT symbol the runtime &mod.fn / direct call emit * (cgen.c:4123-4144). The N_DOT arm of the #117/#119 reloc helper. */ if (opnd && opnd->kind == N_DOT) { if (opnd->lhs == NULL || opnd->lhs->kind != N_IDENT || (opnd->lhs->type != NULL && opnd->lhs->type != ty_err)) return NULL; Type *du = type_chase_named(opnd->type); if (du == NULL || du->kind != TY_FN) return NULL; return mod_mangle_fn(c, opnd->str, use_hint(c->cur_mod, opnd->lhs->str)); } if (opnd == NULL || opnd->kind != N_IDENT) return NULL; Type *ou = type_chase_named(opnd->type); if (ou == NULL || ou->kind != TY_FN) return NULL; return mod_mangle_fn(c, opnd->str, c->cur_mod); } /* tuple_row_foldable — validate that every cast-peeled element of `rhs` * (an N_TUPLE) reduces to a static row: an int literal (fold_int_literal) * or a str literal in a str/slice slot. A tagged element slot has no * static-init shape (tag word + payload widening) — reject so the caller * loud-stops (#22a, rule 7); pre-guard an int init would have emitted one * 8B word into the 16B+ box (silent layout skew). The validate twin of * emit_tuple_row_bytes / emit_tuple_row_relocs: two-pass keeps a partial * row out of the output (emit_array_data precedent). Factored from * emit_tuple_data so the slice-of-tuple backing (#117) shares it. */ static int tuple_row_foldable(Cg *c, Type *u, Node *rhs) { (void)c; 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; { 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; } /* #117: a `&fn` element folds to an 8B reloc slot. */ if (node_fnptr_sym(c, ev) != NULL) continue; u64 v; if (!fold_int_literal(ev, &v)) return 0; } return 1; } /* emit_tuple_row_bytes — the row's element bytes, concatenated, into the * currently-open DATAW quoted string (no DATAW wrapper, no sym). Slot * layout (C-t0): a scalar element is one 8B LE word; a str/slice element * its 24B header slot (8 zero ptr placeholder + LE len + 8 zero cap). * Caller has already proven the row foldable (tuple_row_foldable). */ static void emit_tuple_row_bytes(Cg *c, FILE *out, Type *u, Node *rhs) { (void)c; 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; 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; } /* #117: a `&fn` element is an 8B zero ptr placeholder; the * reloc is patched in emit_tuple_row_relocs. */ if (node_fnptr_sym(c, ev) != NULL) { for (int i = 0; i < 8; 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)); } } /* emit_tuple_row_relocs — the row's DATAR ptr patches, at backing-relative * ++. A str element patches the ptr word with the * interned strlit's VA; the slot stride steps by tuple_eslot. row_off lets * a slice backing place k rows contiguously (#117); emit_tuple_data passes * 0 (foff matches the absolute element offset — byte-neutral). */ static void emit_tuple_row_relocs(Cg *c, FILE *out, const char *holder, int row_off, Type *u, Node *rhs) { int foff = row_off; 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; int wide = tp && (type_isstr(tp->type) || type_isslice(tp->type)); if (wide) { if (ev->strlen > 0) { const char *lab = intern_strlit(c, ev->str, ev->strlen); fprintf(out, "DATAR %s+%d(SB),%s(SB)\n", holder, foff, lab); } } else { /* #117: the `&fn` element's reloc — the FIRST * &fn→DATAR in the emitter; patches the 8B slot at * holder+foff with the fn's TEXT VA. */ const char *fsym = node_fnptr_sym(c, ev); if (fsym != NULL) fprintf(out, "DATAR %s+%d(SB),%s(SB)\n", holder, foff, fsym); } foff += tuple_eslot(tp ? tp->type : NULL); } } /* emit_tuple_data — module-level `let g: (T0, T1, ...) = (v0, v1, ...);` * static init (C-t3, #48). One slot-laid DATAW row (+ DATAR str-element * ptr patches) via the backing-relative emit_tuple_row helpers; rhs == * NULL zero-inits. Unsupported element inits return 0 and the caller * loud-stops (rule 7 — pre-C-t3 the whole definition was SILENTLY skipped * and reads saw BP-frame garbage). */ 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_fn(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; if (!tuple_row_foldable(c, u, rhs)) return 0; fprintf(out, "DATAW %s(SB),\"", sym); emit_tuple_row_bytes(c, out, u, rhs); fputs("\"\n", out); emit_tuple_row_relocs(c, out, sym, 0, u, rhs); 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_fn(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 ".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. * * #117: a slice of (str,*fn)-style TUPLE rows builds the backing as k * tuple rows (emit_tuple_row_*) — str element = 24B header + ptr→char * DATAR, &fn element = 8B slot + fn→DATAR reloc, at tuple-slot offsets. * Bounded to TY_TUPLE rows (fold-6's charclass_map need); other aggregate * element kinds stay loud below (the honest construction boundary). * 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 = type_chase_named(etype); 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++; } const char *sym = mod_mangle_fn(c, name, module); const char *bk = aprintf(c->a, "%s.d", sym); if (eu && eu->kind == TY_TUPLE) { /* #117 aggregate-element arm: k tuple rows. Validate every row * first (two-pass, partial-row safe). The backing is ONE DATAW * (w6a ignores +off on DATAW), then per-row relocs at the * row's backing-relative offset. * * #120 (retained divergence): this cgen arm fires for ANY * foldable TY_TUPLE row, but wwstage's checker today admits * only the (str,*fn) const-slice shape (fold-6's need) and * loud-rejects the rest ("let: not assignable") before cgen. * So non-(str,*fn) foldable tuple const-slices (e.g. * `[](i64,i64)`, `[](str,i64)`) are cs-accept / ww-checker- * reject — an acceptance divergence (ww LOUD, never silent), * pre-train symmetric-loud, now backstopped only by ww's * checker. Tracked by #120 (#29-kin); align is a checker-layer * concern, out of this cgen fold's scope (rob NARROW). */ int stride = etype ? (int)etype->size : 0; for (Node *e = rhs->list; e; e = e->next) { Node *row = e; while (row && row->kind == N_CAST) row = row->lhs; if (row == NULL || row->kind != N_TUPLE || !tuple_row_foldable(c, eu, row)) fatal("emit_slice_data: tuple-row element not a " "foldable constant ((str,*fn) rows only; " "#117, rule 7)"); } fprintf(out, "DATAW %s(SB),\"", bk); for (Node *e = rhs->list; e; e = e->next) { Node *row = e; while (row && row->kind == N_CAST) row = row->lhs; emit_tuple_row_bytes(c, out, eu, row); } fputs("\"\n", out); int row_off = 0; for (Node *e = rhs->list; e; e = e->next) { Node *row = e; while (row && row->kind == N_CAST) row = row->lhs; emit_tuple_row_relocs(c, out, bk, row_off, eu, row); row_off += stride; } } else { 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 esz = etype ? (int)etype->size : 1; /* Synthesize [k]T to ride the emit_array_lit_bytes choke. */ 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)"); /* 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; /* #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is * initializer-less; emitting a DATAW for it would DUPLICATE the * definition that lives in the dep's own .o → link collision. * Gate on the explicit imported flag (NOT rhs==NULL: a package's * OWN init-less let must still zero-init). Symmetric with M2's * producer `imported==0` filter — same predicate both ways. */ if (c->sep_mode && d->imported) 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; } } /* #87: non-nullable tagged-union global — emit the box that * mirrors the runtime local (tag + payload). let_emit_size * keeps nullable at 0 so the (*T|void) one-word fold stays on * the 8B scalar arm below. */ { Type *gu = type_chase_named(d->type); if (gu != NULL && gu->kind == TY_TAGGED && !gu->nullable) { if (!emit_tagged_data(out, c, d->str, d->module, d->type, d->rhs)) fatal("global tagged let `%s`: " "unsupported variant init " "(int/str literal 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; /* #119: a scalar `&fn` global — 8B zero ptr * placeholder + the &fn->DATAR reloc (the #117 * helper at its second consumer). Pre-#119 this * fell through fold_int_literal -> continue -> no * DATA -> undefined ref / garbage deref. */ { const char *fsym = node_fnptr_sym(c, r); if (fsym != NULL) { const char *sym = mod_mangle_fn(c, d->str, d->module); fprintf(out, "DATAW %s(SB),\"", sym); for (int i = 0; i < 8; i++) emit_data_byte(out, 0); fputs("\"\n", out); fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, fsym); 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_fn(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_fn(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_fn(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; /* #22 M3: a `.wwi` dep def with DATA storage (int-fold / float / * struct / array) must NOT re-emit — the dep's own .o owns the * symbol. Str defs are inline-spliced (sdef_collect), never * emitted here, so they need no gate; the registries * (defstructs/defarrays/defall) stay populated for imported * decls so the target's LOAD paths still resolve the extern. */ if (c->sep_mode && d->imported) continue; u64 v; if (fold_int_literal(d->rhs, &v)) { fprintf(out, "DATA %s(SB),\"", mod_mangle_fn(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_strarray — SSoT for the #18 [N]str element-strlit intern * ORDER (element order, then `...` repeat-fill). Shared by let_pre_intern's * let arm and the #8/GAP-B def arm so both emit labels in the SAME order * emit_strarray_data references them by — a divergent order would mis-pair * the DATAR rows with their _S_ rodata. `u` is the chased TY_ARRAY type, * `r` the chased N_ARRLIT rhs; the caller has verified the element is str. */ static void pre_intern_strarray(Cg *c, Type *u, Node *r) { 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++; } } } /* 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; /* #49: strlit labels allocated here (static-data initialisers) must * carry the OWNING decl's module prefix, not the stale last-fn * cur_mod. Save/restore so emit_data/emit_defs/emit_lets — which read * cur_mod for fn-ptr relocs (node_fnptr_sym) — see the same value * they did before. let_pre_intern itself only interns, so driving * cur_mod here has no other effect. */ const char *save_mod = c->cur_mod; for (Node *d = file->list; d; d = d->next) { c->cur_mod = (d->module && d->module[0]) ? d->module : NULL; /* #22 M3: skip imported deps so the strlit table (and its _S_ * sequence) is a pure function of THIS package's own decls. A * dep's body initializer would intern here, but its `.wwi` * (init stripped) would not — gating on imported keeps the * bodies-vs-.wwi `.s` byte-identical for P's own symbols. The * dep's own strlits live in the dep's .o; cross-module str-def * splicing rides sdef_collect (interned at the use site, not * here), so it is unaffected. */ if (c->sep_mode && d->imported) continue; /* #8/GAP-B: a `def [N]str` needs the SAME element-strlit * pre-interning as the let [N]str arm below (the #18 ordering * contract) so emit_strarray_data's DATAR rows find their _S_ * rodata. let_pre_intern walked only N_LET; a def's labels were * allocated too late (emit_defs pass) → dangling _S_. Str-array * ONLY — def tuple/slice/tagged/scalar-str stay out of scope * (#10/#270 / inline-Sdef). */ if (d->kind == N_DEF) { Type *du = type_chase_named(d->type); Node *dr = d->rhs; while (dr && dr->kind == N_CAST) dr = dr->lhs; if (du && du->kind == TY_ARRAY && dr && dr->kind == N_ARRLIT) { Type *deu = type_chase_named(du->sub); if (deu && deu->kind == TY_STR) pre_intern_strarray(c, du, dr); } continue; /* defs ride only the str-array twin */ } 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 = type_chase_named(u->sub); if (eu != NULL && eu->kind == TY_STR) { pre_intern_strarray(c, u, r); 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; } /* #117: slice-of-tuple global — pre-intern each row's str- * element literals in row-then-element order so emit_slice_data's * per-row DATAR patches find their _S_ rodata rows (the #48 * tuple-arm pattern, walked across k rows). */ if (u != NULL && u->kind == TY_SLICE && r != NULL && r->kind == N_ARRLIT) { Type *seu = type_chase_named(u->sub); if (seu != NULL && seu->kind == TY_TUPLE) { for (Node *row = r->list; row; row = row->next) { Node *rt = row; while (rt && rt->kind == N_CAST) rt = rt->lhs; if (rt == NULL || rt->kind != N_TUPLE) continue; Tparam *tp = seu->params; for (Node *e = rt->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; } } /* #87: tagged global with a str/slice-variant literal init — * pre-intern so emit_tagged_data's DATAR (ptr@+8) finds its * _S_ rodata row (the #48 tuple-arm pattern). */ if (u != NULL && u->kind == TY_TAGGED && !u->nullable && r != NULL && r->kind == N_STRLIT && r->strlen > 0 && (type_isstr(r->type) || type_isslice(r->type))) { (void)intern_strlit(c, r->str, r->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); } c->cur_mod = save_mod; } 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; /* #22 M3: emit code ONLY for this package's own decls. A `.wwi` * dep fn is a body-less prototype that already skips (cgfn's * extern-decl early return); the explicit gate also covers a * dep fn that still carries a body in a transitional unit and * keeps the "emit iff imported==0" rule exception-free. */ if (c->sep_mode && d->imported) 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; }