/* * 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; /* 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). * cg_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 now size at first use and fatal() if a later site asks * for more (rule 7: surface, don't silently corrupt the frame — * pinned offset can't grow in place once neighbours are allocated). * Per-fn convergence completed by #15 (#26c follow-up): wwstage * dropped its scanlocals pre-pass and aligned DOWN to cstage's * first-use shape. _sz tracks cached allocation size. */ static int cg_tagbase; static int cg_tagbase_sz; static int cg_tagscr; static int cg_tagscr_sz; /* 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) { if (t == NULL) return 0; if (t->kind == TY_NAMED) t = t->under; if (t == NULL) return 0; return t->kind == TY_F32 || t->kind == TY_F64 || t->kind == TY_UNTYPED_FLOAT; } /* type_chase_named — walk the TY_NAMED.under chain to the deepest non- * named type. Chain-of-aliases (#22): `type b = a; type a = struct;` * stacks two TY_NAMED layers — a single peel leaves `t` pointing at * the inner alias (still TY_NAMED), so kind-gated arms (TY_STRUCT, * TY_SLICE, TY_TAGGED, TY_PTR) miss and the codegen silently falls * through to a scalar shape. Mirror of wwstage's structlookupchain. */ static Type * type_chase_named(Type *t) { while (t && t->kind == TY_NAMED) t = t->under; return t; } /* cg_sret_retsize — sret classifier; defined after the tuple register- * return helpers (tuple_rseq / tuple_ebytes / 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) { if (t == NULL) return 0; if (t->kind == TY_NAMED) t = t->under; 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) { if (t == NULL) return 0; if (t->kind == TY_NAMED) t = t->under; 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: scoped to an N_CALL producer — the only form * that leaves a tuple in the return-ABI cursor (AX/DX/CX/R8 + X0/X1, per * #164). A tuple ident / literal as a first-class value is a separate * unimplemented gap (`let t = (1,2)` does not materialise a slot today), * so the SEND restricts to the call form and loud-stops the rest rather * than push stale registers (rule 7, never a silent drop). */ static Type * node_tuplearg(Node *n) { if (n == NULL || n->kind != N_CALL) return NULL; Type *t = n->type; Type *u = (t && t->kind == TY_NAMED) ? t->under : t; return (u && u->kind == TY_TUPLE) ? u : NULL; } /* #83: positional tuple register-return ABI. Tuple elements ride * consecutive eightbytes over tuple_rseq[]; a slice/str rides its 3-word * {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8, ty_str->size SSoT), a * scalar rides 1. SEND (N_RETURN) and RECEIVE (N_MLET/N_MASSIGN) walk the * SAME widths so element->register agrees — mirrors harec's * create_unpack_bindings element walk (ref/harec/src/check.c:1354-1416). */ static const int tuple_rseq[] = { D_AX, D_DX, D_CX, D_R8 }; /* #164 (#107): SysV dual register-class return. A tuple (and, per #171, * a struct) return places each element by SysV class — a float rides the * SSE row [X0,X1], everything else the INTEGER row [AX,DX,CX,R8] * (tuple_rseq) — with the two rows advancing on INDEPENDENT counters, so * a float lands in the next XMM regardless of its positional slot * (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},{XMM0,XMM1}}). * ww extends the INTEGER row to 4 eightbytes; the SSE row keeps SysV's 2. * tuple_store is the shared per-element receive lowering so the struct- * return convergence (#171) is a call-site swap, not a redesign. */ static const int tuple_sse_seq[] = { D_X0, D_X1 }; /* #10: the register-return-ABI caps — the SINGLE SSoT shared by the sret * classifier (cg_sret_retsize over-cap-tuple arm) AND every emit/receive * site (N_RETURN tuple SEND, N_MLET/N_MASSIGN destructure, cgcall guard). * Classify and emit MUST agree on these, else a tuple gets classified * sret by one and in-reg by the other → corruption. */ #define TUPLE_GPCAP ((int)nelem(tuple_rseq)) #define TUPLE_SSECAP ((int)nelem(tuple_sse_seq)) static int tuple_ebytes(int wide) { return wide ? (int)(ty_str->size / 8) : 1; } static int type_isf32(Type *t) { if (t == NULL) return 0; if (t->kind == TY_NAMED) t = t->under; 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; if (t == NULL) return 0; if (t->kind == TY_NAMED) t = t->under; 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). * Everything else (in-cap tuples, 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; if (rt->kind == TY_TUPLE) { int gptotal = 0, ssecount = 0, f32; for (Tparam *p = rt->params; p; p = p->next) { Type *pu = type_chase_named(p->type); int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR); if (fld_isfloat(p->type, &f32)) ssecount++; else gptotal += tuple_ebytes(wide); } 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 = (t && t->kind == TY_NAMED) ? t->under : 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 = (n->type->kind == TY_NAMED) ? n->type->under : 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 = (bt && bt->kind == TY_NAMED) ? bt->under : bt; if (bu && bu->kind == TY_PTR) { Type *st = bu->sub; bu = (st && st->kind == TY_NAMED) ? st->under : st; } if (bu && bu->kind == TY_STRUCT) { t = n->type; } break; } default: break; } Type *u = (t && t->kind == TY_NAMED) ? t->under : 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) { if (t == NULL) return 0; if (t->kind == TY_NAMED) t = t->under; if (t == NULL || t->kind != TY_STRUCT) return 0; return (int)t->size; } /* struct_float_class — SysV per-eightbyte classification for the #165 * float-bearing-struct param case (the param twin of #171's struct * return, classifying per-eightbyte rather than #163's per-element). * Fills cls[e] = 1 (SSE) / 0 (INTEGER) for each of the struct's 1-2 * eightbytes and returns the eightbyte count, but ONLY for a qualifying * struct: every eightbyte is either pure-INTEGER or a lone f64 exactly * filling it, AND at least one is f64. Returns 0 (caller keeps the all- * GP transport, which is correct + byte-identical for those) when the * type is not a <=16B struct, has an all-integer layout (no float to * route), carries an f32 field, packs >1 float into an eightbyte, has a * float straddling the 8-byte SysV eightbyte boundary, or holds an * aggregate field (SysV would recurse — out of scope here). f32 / sub- * eightbyte packing is deferred (#165b). */ static int struct_float_class(Type *t, int *cls) { if (t == NULL) return 0; if (t->kind == TY_NAMED) t = t->under; 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 = (f->type && f->type->kind == TY_NAMED) ? f->type->under : 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; if (t->kind == TY_NAMED) t = t->under; 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; } /* type_isnullable — TY_TAGGED with the (*T | void) one-word fold. */ static int type_isnullable(Type *t) { if (t == NULL) return 0; if (t->kind == TY_NAMED) t = t->under; 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; if (t->kind == TY_NAMED) t = t->under; if (t == NULL || t->kind != TY_TAGGED) return 0; int i = 0; for (Tparam *p = t->params; p; p = p->next, i++) { 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; } /* 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. */ 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) { 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; if (t->kind == TY_NAMED) t = t->under; 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; if (t->kind == TY_NAMED) t = t->under; 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; if (t->kind == TY_NAMED) t = t->under; if (t == NULL || t->kind != TY_TAGGED) return -1; /* Pass 1: exact match (NAMED-vs-NAMED pointer-id, tagged-vs-tagged, * bare type_eq). Exact matches take precedence and need no guard — * distinct variants don't exact-match the same source. */ int idx = 0; for (Tparam *p = t->params; p; p = p->next, idx++) { if (cg_variant_match(p->type, vt)) return idx; } /* Pass 2 (#15): no exact variant matched — try a structural match of * a BARE source against a NAMED-alias variant (e.g. a bare `*vtable` * into the `stream` (= *vtable) variant of `(file | stream)`). The * bare side has no nominal identity, so structure is the only * discriminator; without this the widen found no variant and * defaulted to tag 0, miscompiling every io.write(&...vt) in cgen's * emit path. Exact-first (pass 1) keeps a bare `i64` into * `(i64 | oserror)` binding the exact `i64`, not the alias. drew's * proviso: guard the structural fallback like the #218 nested-widen * site — if a bare source structurally matches >=2 NAMED variants, * nominal layout is needed to disambiguate, so hard-error rather * than silently first-pick. */ if (vt->kind != TY_NAMED) { int found = -1, n = 0; idx = 0; for (Tparam *p = t->params; p; p = p->next, idx++) { /* One-level NAMED unwrap: a chained ptr-alias variant * (type a=*X; type b=a) isn't reached here, so it would * silently mis-tag — unexercised (zero in corpus), see * task #17. */ Type *pu = p->type; if (pu && pu->kind == TY_NAMED && pu->under && type_eq(pu->under, 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; if (t->kind == TY_NAMED) t = t->under; return t && t->kind == TY_TAGGED; } /* FFI map: ww-side ident name → linker-side symbol name. Built from * @symbol("real_name") attributes on fn declarations. */ typedef struct Ffi Ffi; struct Ffi { const char *ident; const char *symbol; Ffi *next; }; static Ffi *ffi_map; /* Def-as-string-literal map. `def NAME: str = "lit"` doesn't materialise * as a real linker symbol; instead, references to NAME load the same * (ptr, len) pair that the literal would. Avoids needing relocations * inside DATA blocks for the ptr field of a str header. */ typedef struct Sdef Sdef; struct Sdef { const char *name; const char *mod; /* raw `// MODULE:` directive on the decl, * or NULL. Mirrors cgfn's c->cur_mod which * stores the same raw form. */ const char *bytes; u64 len; Sdef *next; }; static Sdef *sdefs; /* Same-module-first match for Sdef walks. Mirrors wwstage deflookuprhs's * first pass: returns 1 iff s belongs to the fn we're emitting. Caller * still re-walks for the any-module fallback. */ static int sdef_mod_match(Cg *c, Sdef *s) { const char *a = s->mod, *b = c->cur_mod; if (a == b) return 1; if (a == NULL || b == NULL) return 0; return strcmp(a, b) == 0; } /* Explicit-hint variant for `mod.NAME` N_DOT mod-qualified Sdef walks * (sister of wwstage deflookuprhsmod). Walk #2 needs n->lhs->str — a * cross-module qualifier from a third module won't match c->cur_mod * and would fall back to head-pick, possibly inlining the wrong-module * strlit when both source modules export the same-leaf str def. */ static int sdef_mod_match_hint(Sdef *s, const char *hint) { const char *a = s->mod; if (a == hint) return 1; if (a == NULL || hint == NULL) return 0; return strcmp(a, hint) == 0; } /* Interned string literals — emitted as DATA directives after all * function bodies, so the linker lays them out alongside .text. */ typedef struct Strlit Strlit; struct Strlit { const char *label; const char *bytes; u64 len; Strlit *next; }; static Strlit *strlits; static int strlit_seq; static const char * intern_strlit(Cg *c, const char *bytes, u64 len) { for (Strlit *s = strlits; s; s = s->next) if (s->len == len && memcmp(s->bytes, bytes, len) == 0) return s->label; Strlit *s = amalloc(c->a, sizeof *s); s->label = aprintf(c->a, "_S_%d", strlit_seq++); s->bytes = bytes; s->len = len; s->next = strlits; strlits = s; return s->label; } static void emit_data(Cg *c, FILE *out) { for (Strlit *s = strlits; s; s = s->next) { fprintf(out, "DATA %s(SB),\"", s->label); for (u64 i = 0; i < s->len; i++) { unsigned char b = (unsigned char)s->bytes[i]; switch (b) { case '"': fputs("\\\"", out); break; case '\\': fputs("\\\\", out); break; case '\n': fputs("\\n", out); break; case '\t': fputs("\\t", out); break; case '\r': fputs("\\r", out); break; default: if (b < 0x20 || b >= 0x7f) fprintf(out, "\\x%02x", b); else fputc(b, out); } } /* Trailing NUL: lets `.ptr` be passed to libc / syscalls * that expect a C string. The `len` field still excludes * this byte, so iteration semantics are unchanged. */ fputs("\\x00", out); fputs("\"\n", out); } (void)c; } static const char * ffi_resolve(const char *ident) { for (Ffi *f = ffi_map; f; f = f->next) if (strcmp(f->ident, ident) == 0) return f->symbol; return ident; } static void ffi_collect(Cg *c, Node *file) { ffi_map = NULL; if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_FNDECL) continue; for (Node *a = d->attr; a; a = a->next) { if (a->kind != N_ATTR) continue; if (strcmp(a->str, "symbol") != 0) continue; if (a->list == NULL || a->list->kind != N_STRLIT) continue; Ffi *f = amalloc(c->a, sizeof *f); f->ident = d->str; f->symbol = a->list->str; f->next = ffi_map; ffi_map = f; } } } /* Module-private symbol map. Mirrors selfhost/cmd/wcc/cgen.ww. Every * non-FFI top-level fn decl is mangled to . at emission * time so two modules can each define the same fn leaf — including * exported ones (lib/os and lib/io both ship `read`/`write`/`close`) * — without colliding at link time. Non-fn decls (let/def/type) keep * the older "non-exported only" rule: their export-side namespace is * the user-facing data ABI and mangling them changes the surface. */ typedef struct Mod Mod; struct Mod { const char *name; const char *module; Mod *next; }; static Mod *mod_map; /* Top-level `let` map. Populated alongside mod_map; consulted by the * N_IDENT store path and the &-of path to route reads/writes through * a RIP-relative reference rather than dropping them as the (pre- * writable-.data) compiler did. emit_lets emits a DATAW for each. */ typedef struct LetVar LetVar; struct LetVar { const char *name; Type *type; /* #128b: imported-let type lookup for module- * qualified N_INDEX base esz dispatch. */ LetVar *next; }; static LetVar *letvars; /* #129 A.2: struct-typed defs that now have DATA storage need the * same LEAQ-and-field-offset N_DOT-load shape as struct-typed lets. * Tracked separately so let_islet's existing callers (which gate * scalar/float/str arms) don't pick up struct defs and re-route their * narrow-load logic. */ typedef struct DefStruct DefStruct; struct DefStruct { const char *name; Type *type; DefStruct *next; }; static DefStruct *defstructs; /* #129 A.3: array-typed defs now have DATA storage and need the same * LEAQ name(SB) + indexed-load shape as array-typed lets at cgindex * and N_DOT base-resolution sites. Mirrors DefStruct (A.2). */ typedef struct DefArray DefArray; struct DefArray { const char *name; Type *type; DefArray *next; }; static DefArray *defarrays; /* #149: every top-level `def`, regardless of kind. Backs the address-of * path's is-any-def check (loud error on `&`) 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. */ static int let_emit_size(Type *t) { if (t == NULL) return 0; Type *u = (t->kind == TY_NAMED) ? t->under : 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. */ default: return 0; } } /* Is the unwrapped type a str? Used by the load/store paths so the * (AX, BX) pair convention is preserved for str globals, mirroring * what we already do for str locals. */ static int let_isstr(Type *t) { if (t == NULL) return 0; Type *u = (t->kind == TY_NAMED) ? t->under : t; return u && u->kind == TY_STR; } /* Is the unwrapped type a slice? Slice globals flow as the (AX, BX, * CX) triple — same as the local ABI. */ static int let_isslice(Type *t) { if (t == NULL) return 0; Type *u = (t->kind == TY_NAMED) ? t->under : t; return u && u->kind == TY_SLICE; } /* Is the unwrapped type a struct? Struct globals only support field * access (read + plain `=` write for scalar fields). Whole-struct * by-value flow through expressions isn't wired. */ static int let_isstruct(Type *t) { if (t == NULL) return 0; Type *u = (t->kind == TY_NAMED) ? t->under : t; return u && u->kind == TY_STRUCT; } /* Is the unwrapped type a fixed-length array? Array globals are * zero-init DATAW slots; cgindex addresses them as LEAQ name(SB) * and lets the element load/store run as usual. */ static int let_isarray(Type *t) { if (t == NULL) return 0; Type *u = (t->kind == TY_NAMED) ? t->under : t; return u && u->kind == TY_ARRAY; } /* Is the unwrapped type a float (f32 or f64)? Float globals flow * through X0 — load/store goes LEAQ name(SB),CX → MOVSS/MOVSD via the * indirect, since the asm has no D_EXTERN form for SSE moves yet. */ static int let_isfloat(Type *t) { if (t == NULL) return 0; Type *u = (t->kind == TY_NAMED) ? t->under : 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 (t->kind == TY_NAMED) ? t->under : 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; Type *u = type_unwrap(t); if (u && u->kind == TY_PTR && u->sub) { Type *p = type_unwrap(u->sub); if (p && p->kind == TY_ARRAY) return p; } return u; } static int decl_has_ffisym(Node *d) { for (Node *a = d->attr; a; a = a->next) { if (a->kind != N_ATTR) continue; if (strcmp(a->str, "symbol") == 0) return 1; } return 0; } /* Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export` for fns. * Both stages must match exactly — ww2/ww3/ww4 byte-identity depends on it. */ static void mod_collect(Cg *c, Node *file) { mod_map = NULL; if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { int isfn = (d->kind == N_FNDECL); int track = isfn || (d->kind == N_TYPEDECL) || (d->kind == N_DEF) || (d->kind == N_LET); if (!track) continue; /* Non-fn decls (let/def/type) still skip exported entries — * their export-side namespace is the user-facing data ABI * and mangling them changes the surface. Fns mangle * unconditionally so cross-module same-leaf exports * (os.read vs io.read) coexist at link time. */ if (!isfn && d->export) continue; if (d->module == NULL || d->module[0] == '\0') continue; if (decl_has_ffisym(d)) continue; /* `main` is the linker entry-point convention. Even when not * marked `export`, it must keep its bare name so w6l can * resolve `_start`'s `CALL main(SB)`. */ if (d->str && strcmp(d->str, "main") == 0) continue; Mod *m = amalloc(c->a, sizeof *m); m->name = d->str; m->module = d->module; m->next = mod_map; mod_map = m; } } /* Returns the originating module for a name, or NULL if the name * isn't a registered private decl. By-name only — works for non-fn * refs (let/def/type) where the mod_collect skip rule keeps each leaf * unique across the program. Fn refs go through mod_lookup_for_fn * since multiple modules can now export the same fn leaf. */ static const char * mod_lookup(const char *name) { for (Mod *m = mod_map; m; m = m->next) if (strcmp(m->name, name) == 0) return m->module; return NULL; } /* Hint-aware variant for fn names. Walks mod_map looking for a * (name, hint) pair; returns NULL if there's no leaf-name match at * all, the hinted module if a match exists, or the first leaf match * when the caller had no hint. The hint comes from AST shape: * - N_DOT call `m.fn(...)`: hint = the SK_USE module ident's str. * - bare N_IDENT call `fn(...)`: hint = c->cur_mod (current fn's * module — bare names resolve same-module by ww's rules). * Falling back to the first leaf match preserves the legacy single- * owner shape for callers that don't (yet) thread a hint. */ static const char * mod_lookup_for_fn(const char *name, const char *hint) { const char *first = NULL; for (Mod *m = mod_map; m; m = m->next) { if (strcmp(m->name, name) != 0) continue; if (hint != NULL && m->module != NULL && strcmp(m->module, hint) == 0) return m->module; if (first == NULL) first = m->module; } return first; } /* Value-global variant: mangle ONLY on an exact (name, hint) match; * otherwise return NULL so the name stays bare. Unlike the fn variant * there is NO first-leaf-match fallback — exported value globals are * export-skipped from mod_map (mod_collect keeps their bare-name data * ABI, see the skip at `!isfn && d->export`), so a first-match fallback * would mis-mangle an exported `v` onto another module's private `v` * (#1 cgen value-global module-qualifier, the cgen residual of #55). * Bare-on-miss is correct: a missing entry means the leaf is either an * exported global (its own bare symbol) or not module-private at all. * * HONEST BOUNDARY (rule 7) — do NOT "fix" the following into a * workaround: if two modules BOTH export the same value leaf, both stay * bare and the linker sees a duplicate symbol. That is a CORRECT, loud, * link-time ABI clash (identical to C's two-extern-same-name rule), NOT * a silent miscompile. A bare reference can never legitimately resolve * to another module's PRIVATE global, so first-match is never wanted on * the value path; the only ambiguity left is genuine duplicate exports, * which belong to the linker, not to a cgen disambiguation heuristic. */ static const char * mod_lookup_value(const char *name, const char *hint) { if (hint == NULL) return NULL; for (Mod *m = mod_map; m; m = m->next) { if (strcmp(m->name, name) != 0) continue; if (m->module != NULL && strcmp(m->module, hint) == 0) return m->module; } return NULL; } /* Collect every top-level `let` whose declared type we can store * in a single .data slot. Names not in this map fall through to * the old "drop assignment" path; with a clear link-time * undefined-symbol error on any read. */ static void let_collect(Cg *c, Node *file) { letvars = NULL; defstructs = NULL; defarrays = NULL; defall = NULL; if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { if (d->kind == N_LET) { if (d->str == NULL || d->str[0] == '\0') continue; if (let_emit_size(d->type) == 0) continue; LetVar *lv = amalloc(c->a, sizeof *lv); lv->name = d->str; lv->type = d->type; lv->next = letvars; letvars = lv; continue; } if (d->kind == N_DEF) { if (d->str == NULL || d->str[0] == '\0') continue; /* #149: track every def (any kind) so the address-of * path can tell a def from an unknown ident and loud- * error on `&`. */ DefAny *dn = amalloc(c->a, sizeof *dn); dn->name = d->str; dn->type = d->type; dn->rhs = d->rhs; dn->next = defall; defall = dn; /* #129 A.2: struct-typed defs now have DATA storage * (emit_defs struct arm); register them so the N_DOT * struct-let LEAQ-and-offset shape widens to cover * them too. Other def kinds (int / float / str) * stay on their existing load paths. */ if (let_isstruct(d->type)) { DefStruct *ds = amalloc(c->a, sizeof *ds); ds->name = d->str; ds->type = d->type; ds->next = defstructs; defstructs = ds; continue; } /* #129 A.3: array-typed defs now have DATA storage * (emit_defs array arm); register them so cgindex's * `let_islet`-gated LEAQ name(SB) base-load widens * to defs too (LOAD-side twin of the struct-def * registry). */ if (let_isarray(d->type)) { DefArray *da = amalloc(c->a, sizeof *da); da->name = d->str; da->type = d->type; da->next = defarrays; defarrays = da; } } } } static int def_isstructdef(const char *name) { if (name == NULL) return 0; for (DefStruct *ds = defstructs; ds; ds = ds->next) if (strcmp(ds->name, name) == 0) return 1; return 0; } static int def_isarraydef(const char *name) { if (name == NULL) return 0; for (DefArray *da = defarrays; da; da = da->next) if (strcmp(da->name, name) == 0) return 1; return 0; } /* #149: rhs peels (N_CAST / unary ±) to a float literal — the exact * shape emit_floatlit_data (cgen.c) emits a DATA symbol for. The scalar- * def address-of gate MUST equal that emission set, or `&def` LEAQs a * symbol the data pass never wrote. Keep in sync with the peel inside * emit_floatlit_data. */ static int floatlit_leaf(Node *rhs) { Node *r = rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r != NULL && r->kind == N_UN && (r->op == TK_MINUS || r->op == TK_PLUS)) { r = r->lhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; } return r != NULL && r->kind == N_FLOATLIT; } /* #149/#147: a scalar (int/float) def is addressable iff emit_defs emits * a DATA symbol for it — int via fold_int_literal, float via the * FLOATLIT-leaf shape. Gate is held identical to emit_defs's emission * gate so the addressable set matches byte-for-byte. Computed-rhs floats * (`def NAN = 0.0/0.0`, #147) fold to no symbol and are excluded → they * route to the address-of loud error, never a LEAQ of a missing sym. */ static int def_isscalardef(const char *name) { if (name == NULL) return 0; for (DefAny *dn = defall; dn; dn = dn->next) { if (strcmp(dn->name, name) != 0) continue; if (dn->rhs == NULL) return 0; u64 v; if (fold_int_literal(dn->rhs, &v)) return 1; if (let_isfloat(dn->type) && floatlit_leaf(dn->rhs)) return 1; return 0; } return 0; } static int def_isanydef(const char *name) { if (name == NULL) return 0; for (DefAny *dn = defall; dn; dn = dn->next) if (strcmp(dn->name, name) == 0) return 1; return 0; } static int let_islet(const char *name) { if (name == NULL) return 0; for (LetVar *lv = letvars; lv; lv = lv->next) if (strcmp(lv->name, name) == 0) return 1; return 0; } /* #128b: look up a top-level let's type by leaf name. Sister of * wwstage's letvartnode (selfhost/cmd/wcc/cgen.ww:999). Used at the * cgindex / cg_dotbase_addr sites where a module-qualified base * (`mod.arr`) leaves n->lhs->type NULL (SK_USE-bound module ident), * so the imported array's element type / size must come through * this let-map lookup instead. Returns NULL if name isn't a tracked * top-level let. */ static Type * let_var_type(const char *name) { if (name == NULL) return NULL; for (LetVar *lv = letvars; lv; lv = lv->next) if (strcmp(lv->name, name) == 0) return lv->type; return NULL; } /* Glue `.` into a fresh arena buffer. */ static const char * mod_join(Cg *c, const char *mod, const char *ident) { size_t mn = strlen(mod), in = strlen(ident); char *buf = amalloc(c->a, mn + 1 + in + 1); memcpy(buf, mod, mn); buf[mn] = '.'; memcpy(buf + mn + 1, ident, in); buf[mn + 1 + in] = '\0'; return buf; } /* Mangle an AST identifier into its asm linker symbol: * - @symbol("...") binding wins (return mapped name). * - module-private decl → .. * - else → name unchanged. * Used at every CALL/MOVQ/LEAQ site that targets an AST name. Plain * `asym(s)` still emits `s` verbatim — use it for strlit labels and * hard-coded runtime symbols like "rt_streq". */ static const char * mod_mangle(Cg *c, const char *ident) { const char *resolved = ffi_resolve(ident); if (resolved != ident) return resolved; const char *mod = mod_lookup(ident); if (mod == NULL) return ident; return mod_join(c, mod, ident); } /* Fn-flavoured mangle: same shape as mod_mangle but consults * mod_lookup_for_fn so the right module wins when multiple modules * register the same fn leaf. `hint` is the explicit module from a * N_DOT call site (or c->cur_mod for bare-ident calls); pass NULL * to get the legacy first-match-wins behaviour. */ static const char * mod_mangle_fn(Cg *c, const char *ident, const char *hint) { const char *resolved = ffi_resolve(ident); if (resolved != ident) return resolved; const char *mod = mod_lookup_for_fn(ident, hint); if (mod == NULL) return ident; return mod_join(c, mod, ident); } /* Value-global flavoured mangle: same shape as mod_mangle_fn but over * mod_lookup_value (exact-(ident,hint)-or-bare, no first-match * fallback). See mod_lookup_value for why value globals can't share the * fn fallback. */ static const char * mod_mangle_value(Cg *c, const char *ident, const char *hint) { const char *resolved = ffi_resolve(ident); if (resolved != ident) return resolved; const char *mod = mod_lookup_value(ident, hint); if (mod == NULL) return ident; return mod_join(c, mod, ident); } /* Forward decl — masym below depends on asym defined further down. */ static Adr asym(const char *s); static Adr masym(Cg *c, const char *ident) { return asym(mod_mangle(c, ident)); } /* Fn-name address builder. Use at every CALL/LEAQ site whose target * is a top-level fn — passes the hint so cross-module same-leaf * exports resolve to the right module. */ static Adr mafn(Cg *c, const char *ident, const char *hint) { return asym(mod_mangle_fn(c, ident, hint)); } /* Value-global address builder. masym's non-hinted mod_lookup picks * the first leaf-name match, so two modules with a same-leaf value * global (`let v` in both) collapse onto one DATA label and a bare * cross-module read resolves to the wrong module (#1 cgen value-global * module-qualifier, the cgen residual of #55). Thread a per-site hint * the way mafn does — curmod at a bare reference, the decl's own module * at the definition label — over the same module-generic decl map. * Kept distinct from mafn (vs renamed) to leave the fn-mangle path * byte-for-byte untouched. Routes through mod_mangle_value (exact-or- * bare) so an exported global stays bare instead of mis-mangling onto * another module's same-leaf private global. */ static Adr mahint(Cg *c, const char *ident, const char *hint) { return asym(mod_mangle_value(c, ident, hint)); } void cg_init(Cg *c, Arena *a) { memset(c, 0, sizeof *c); c->a = a; } Prog * newprog(Cg *c, int op) { Prog *p = amalloc(c->a, sizeof *p); p->as = op; return p; } void emit(Cg *c, Prog *p) { if (c->head == NULL) c->head = p; else c->tail->link = p; c->tail = p; } static Adr areg(int r) { Adr a = { 0 }; a.type = r; return a; } static Adr aimm(long long v) { Adr a = { 0 }; a.type = D_CONST; a.offset = v; return a; } static Adr amem(int r, long long off) { Adr a = { 0 }; a.type = D_INDIR; a.reg = r; a.offset = off; return a; } static Adr asym(const char *s) { Adr a = { 0 }; a.type = D_EXTERN; a.sym = s; return a; } static Adr abranch(const char *s) { Adr a = { 0 }; a.type = D_BRANCH; a.sym = s; return a; } static char * mklabel(Cg *c, const char *prefix) { /* Module-qualified to avoid cross-module same-leaf collisions * (task #13). w6a accepts '.' in label-cont (lex.c:18). */ return aprintf(c->a, "%s%s%s_%s_%d", c->cur_mod ? c->cur_mod : "", c->cur_mod ? "." : "", c->fnname ? c->fnname : "_", prefix, c->labelseq++); } static void ins2(Cg *c, int op, Adr from, Adr to) { Prog *p = newprog(c, op); p->from = from; p->to = to; emit(c, p); } static void ins1(Cg *c, int op, Adr to) { Prog *p = newprog(c, op); p->to = to; emit(c, p); } /* tuple_store — store one received tuple element at BP-relative `off` * from its SysV-class register. A slice/str rides its 3-word * {ptr,len,cap} header from the INTEGER cursor 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 wide, int gp, int sse, int off) { int f32 = 0; if (wide) { ins2(c, A_MOVQ, areg(tuple_rseq[gp + 0]), amem(D_BP, off + 0)); /* .ptr */ ins2(c, A_MOVQ, areg(tuple_rseq[gp + 1]), amem(D_BP, off + 8)); /* .len */ ins2(c, A_MOVQ, areg(tuple_rseq[gp + 2]), amem(D_BP, off + 16)); /* .cap */ return; } if (fld_isfloat(t, &f32)) { ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(tuple_sse_seq[sse]), amem(D_BP, off)); return; } ins2(c, A_MOVQ, areg(tuple_rseq[gp]), amem(D_BP, off)); } static void ins0(Cg *c, int op) { emit(c, newprog(c, op)); } static void label(Cg *c, const char *s) { Prog *p = newprog(c, A_NOP); p->label = s; emit(c, p); } /* cgslicehdr — load the 24B slice/str header at `base`+0 into the * (AX=ptr, BX=len, CX=cap) triple. `base` holds the element address; * the load that targets `base` destroys it, so that word is emitted * LAST. Order otherwise mirrors the slice-FIELD arm (len, cap, ptr). * Shared by the N_INDEX str-element arms (caller does the kind-gate) * and, later, the typeassert str-variant leaf (#9). */ static void cgslicehdr(Cg *c, int base) { if (base != D_BX) ins2(c, A_MOVQ, amem(base, 8), areg(D_BX)); if (base != D_CX) ins2(c, A_MOVQ, amem(base, 16), areg(D_CX)); if (base != D_AX) ins2(c, A_MOVQ, amem(base, 0), areg(D_AX)); if (base == D_BX) ins2(c, A_MOVQ, amem(base, 8), areg(D_BX)); else if (base == D_CX) ins2(c, A_MOVQ, amem(base, 16), areg(D_CX)); else if (base == D_AX) ins2(c, A_MOVQ, amem(base, 0), areg(D_AX)); } /* ------------------------------------------------------------------ */ /* per-fn local table: name → stack offset (positive = below FP) */ typedef struct Local Local; struct Local { const char *name; int off; /* relative to BP; negative for locals */ Local *next; }; /* localoff — push a fresh stack slot for this binding and return its * BP offset. Never dedups by name (post-#27): two `let a: T` in disjoint * scopes within one fn must each get their own slot, sized to their own * declared T. Pre-fix the dedup loop returned the first-allocated slot * regardless of the new declaration's size, so an outer `let a: [128]u8` * after an inner `let a: i64` would collapse onto the 8B slot and * `a[127]` would land at +119(BP), past the saved RIP, into the * caller's frame. localfind walks from the head, so the most recent * binding still wins lookups inside its scope. */ static int localoff(Cg *c, Local **head, const char *name, int size, int *frame) { int al = 8; *frame = (*frame + size + al - 1) & ~(al - 1); int off = -*frame; Local *l = amalloc(c->a, sizeof *l); l->name = name; l->off = off; l->next = *head; *head = l; return off; } /* local_alloc — synonym for localoff. Pre-#27 localoff deduped by name * and local_alloc was the always-fresh escape hatch (match-arm bindings, * synthetic scratch slots). Post-#27 localoff is also always-fresh, so * the two are functionally identical; both names are kept so the call * sites read intentfully (let-decl vs scratch). */ static int local_alloc(Cg *c, Local **head, const char *name, int size, int *frame) { int al = 8; *frame = (*frame + size + al - 1) & ~(al - 1); int off = -*frame; Local *l = amalloc(c->a, sizeof *l); l->name = name; l->off = off; l->next = *head; *head = l; return off; } static int localfind(Local *head, const char *name) { for (Local *l = head; l; l = l->next) if (strcmp(l->name, name) == 0) return l->off; return 0; /* 0 = not found (caller must verify) */ } /* cg_base_cap — load the capacity of a sub-slice's UNDERLYING storage * into `dst` for the #20 cap = base_cap - lo formula (drew: harec * eval.c:1017 slice cap-=start / eval.c:1024 array cap=length-start; * ensure.ha:4-8 distinct capacity field). array [N]T -> N (literal); * slice/str -> the .capacity word carried in the header at +16 (the * +16 load mirrors the hi-default +8 length dispatch, but emitted * unconditionally). Returns 0 when base_cap isn't cleanly available so * the caller keeps the prior cap=len: a non-ident base (cgexpr already * discarded its header cap; recomputing would re-evaluate a possibly * side-effecting base -- #74, which also owns the pre-existing * defaulted-hi len gap there), or a GLOBAL str base (wwstage cgslice * has no global-str load, #73 -- matching it keeps the stages * byte-identical rather than introducing a fresh divergence). */ static int cg_base_cap(Cg *c, Node *base, Type *bu, Local *locals, int dst) { if (!base || base->kind != N_IDENT) return 0; if (bu && bu->kind == TY_ARRAY) { ins2(c, A_MOVQ, aimm((long long)bu->alen), areg(dst)); return 1; } if (bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) { int boff = localfind(locals, base->str); int isglobal = (boff == 0) && let_islet(base->str); if (isglobal && bu->kind == TY_STR) return 0; if (isglobal) { ins2(c, A_LEAQ, masym(c, base->str), areg(dst)); ins2(c, A_MOVQ, amem(dst, 16), areg(dst)); } else { ins2(c, A_MOVQ, amem(D_BP, boff + 16), areg(dst)); } return 1; } return 0; } /* ------------------------------------------------------------------ */ /* expressions: result lands in AX. Returns 1 on success. */ static void cgexpr(Cg*, Node*, Local*); static void cgstmt(Cg*, Node*, Local**, int*); static void cg_widen_tagged_push(Cg*, Local**, Type*, Node*, int); static void cg_widen_tagged_store(Cg*, Local**, Type*, Node*, int, int, int); static void cg_widen_tag_remap(Cg*, Type*, Type*, int); /* cg_dotbase_addr — compute &(inner.field) into `dst_reg` for a bare * N_DOT base where `inner` is an N_IDENT local (struct value OR *struct * pointer). 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). Chained N_DOT (`a.b.c.field[i]`) deferred — not in #135 * scope. * * Caller-spill contract: the helper emits at most one MOVQ + one ADDQ * (or one LEAQ); it does NOT touch AX unless dst_reg == D_AX. Safe to * call where AX holds an unrelated live value (BX dst). */ 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 || inner->kind != N_IDENT) 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 inner_off = localfind(locals, inner->str); int foff = (int)f->offset; 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 { ins2(c, A_LEAQ, amem(D_BP, inner_off + foff), areg(dst_reg)); } return 1; } /* cg_structlit_fill modes — see helper docstring. */ enum { DST_BP = 0, DST_PTR_LOCAL = 1, DST_GLOBAL = 2, }; static void cg_structlit_fill(Cg*, Local**, Type*, Node*, int, int, const char*, int); static void cg_structlit_fill_bp(Cg*, Local**, Type*, Node*, int); static void cgexpr_int(Cg *c, long long v) { ins2(c, A_MOVQ, aimm(v), areg(D_AX)); } /* Materialise a float constant in X0: MOVQ the IEEE bits into AX, PUSH, * MOVSD off the stack into X0. Shared by N_FLOATLIT and the f64/f32-typed * N_INTLIT arm (#103 FACE X): a no-decimal `0f64`/`8f64` is an N_INTLIT * carrying float TYPE, so it must reach X0 like a true float literal does * — the integer-immediate path left the value stranded in AX, so an SSE * compare/mul read a stale X0. */ static void cgexpr_float(Cg *c, double val) { union { double d; u64 u; } x; x.d = val; ins2(c, A_MOVQ, aimm((long long)x.u), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVSD, amem(D_SP, 0), areg(D_X0)); ins2(c, A_ADDQ, aimm(8), areg(D_SP)); } /* cg_widen_tag_remap — when widening from one tagged union to another, * rewrite the source's variant tag at BP+slot_off+0 to use the dst * union's variant indices. No-op when src and dst index orders coincide. * * Mirrors Hare's tagged-subset assignment: a value of type (A|B) flows * into (A|B|C) by re-tagging the discriminator to the position the * variant occupies in the wider union. Both must already match by * cg_variant_match — the checker enforces that. * * Emits a CMPQ-chain switch over the source tag because w6a has no * CMOVQ encoding. The chain is linear in nvariants; in practice tagged * unions are small. */ static void cg_widen_tag_remap(Cg *c, Type *du, Type *su, int slot_off) { if (du == NULL || du->kind != TY_TAGGED) return; if (su == NULL || su->kind != TY_TAGGED) return; int identity = 1, idx = 0; for (Tparam *p = su->params; p; p = p->next, idx++) { int di = cg_tag_for_variant(du, p->type); if (di < 0) di = 0; if (di != idx) { identity = 0; break; } } if (identity) return; const char *done = mklabel(c, "remap_done"); ins2(c, A_MOVQ, amem(D_BP, slot_off + 0), areg(D_AX)); idx = 0; for (Tparam *p = su->params; p; p = p->next, idx++) { const char *next = mklabel(c, "remap_next"); int di = cg_tag_for_variant(du, p->type); if (di < 0) di = 0; ins2(c, A_CMPQ, aimm(idx), areg(D_AX)); ins1(c, A_JNE, abranch(next)); ins2(c, A_MOVQ, aimm(di), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0)); ins1(c, A_JMP, abranch(done)); label(c, next); } label(c, done); } /* cg_widen_tagged_store — write the tagged-union slot bytes for `src` * into base_reg+slot_off, sized to `sz` (8 for nullable fold, else * 16/24+). Used by call-site widening (via cg_widen_tagged_push) and * by the let/assign/return/struct-field-init paths. * * base_reg picks the addressing root for every write: * - D_BP: function-frame slot. The original layout — callers pass * a BP-relative slot_off and the function writes directly. * - else (e.g. D_BX for a *struct field, D_CX for a top-level * struct field): pointer-rooted dst. cgexpr inside this function * trashes every GPR, so we can't carry base_reg across — instead * we route every write through a fresh BP-rooted scratch slot, * reload base_reg from a temp spill at the end, and word-copy * scratch → (base_reg, slot_off). Caller is responsible for * loading base_reg with the dst address before the call; the * function preserves it across cgexpr via the spill. * * Branches by source shape (tagged_arg_size > 0 source counts as a * tagged subset — possibly with different variant indices): * - nullable: dst is folded (*T|void); store pointer at +0. * - tagged ident: byte-copy slot words then remap tag at +0. * - tagged expression: cgexpr leaves AX=tag, DX=val0, [CX=val1] — * spill into slot then remap. * - struct ident: zero-fill, byte-copy struct words to +8. * - struct literal: zero-fill, store each field at slot+8+field_off. * - str: cgexpr leaves AX=ptr, BX=len. * - scalar: cgexpr leaves AX; store at +8 with zero pad. */ static void cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, int base_reg, int slot_off, int sz) { /* For pointer-rooted dst, materialise into a BP-rooted scratch * slot — body writes via `amem(D_BP, write_off + k)` — then copy * out. Spill base_reg first so cgexpr can clobber freely. */ int via_outer = (base_reg != D_BP); int base_spill = 0; int write_off = slot_off; if (via_outer) { if (cg_tagbase != 0) { base_spill = cg_tagbase; } else { base_spill = local_alloc(c, locals_p, "@tagbase", 8, cg_frame); cg_tagbase = base_spill; cg_tagbase_sz = 8; } ins2(c, A_MOVQ, areg(base_reg), amem(D_BP, base_spill)); if (cg_tagscr != 0) { if (sz > cg_tagscr_sz) fatal("cg_widen_tagged_store: @tagscr " "cached sz %d, need %d (pinned offset " "can't grow in place; rule 7 — #15/#26c)", cg_tagscr_sz, sz); write_off = cg_tagscr; } else { write_off = local_alloc(c, locals_p, "@tagscr", sz, cg_frame); cg_tagscr = write_off; cg_tagscr_sz = 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 = (dst && dst->kind == TY_NAMED) ? dst->under : 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; } /* `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 = (castt && castt->kind == TY_NAMED) ? castt->under : castt; Type *innert = src->lhs->type; Type *innu = (innert && innert->kind == TY_NAMED) ? innert->under : 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; Type *su = (st && st->kind == TY_NAMED) ? st->under : st; /* Tagged → tagged subset: copy slot words then tag-remap. */ if (su && su->kind == TY_TAGGED) { int ssz = (int)su->size; /* #218: is the source itself a single NESTED variant of du * (its whole tagged type matches one du variant), rather than * a flattened SUBSET whose members spread into du? If so, the * inner tagged value is the payload: store it at slot+8 with * the outer tag at slot+0, exactly like the scalar/struct/str * single-variant arms below — NOT a copy-to-+0 + sub-variant * remap. cg_tag_for_variant's structural fallback (cgen.c * cg_variant_match) is what recovers the index after the * nominal-lossy collapse. */ int nested = cg_tag_for_variant(du, st); if (nested >= 0) { /* drew collision guard: the structural fallback over- * matches if ≥2 nominally-distinct du variants share the * source's shape. Unreachable under today's nominal-lossy * model, but INVERTS when #199b/B-full lands the nominal * layer — hard-error NOW so a future collision STOPS the * compiler instead of silently mis-tagging. */ int nmatch = 0; for (Tparam *p = du->params; p; p = p->next) if (cg_variant_struct_match(p->type, st)) nmatch++; if (nmatch >= 2) fatal("cg_widen_tagged_store: structural fallback " "cannot disambiguate nominally-distinct same-" "shape variants without nominal layout " "(#218/#199b/B-full)"); ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = 0; k < sz; k += 8) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + k)); if (src->kind == N_IDENT) { int soff = localfind(*locals_p, src->str); for (int k = 0; k < ssz; k += 8) { ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8 + k)); } } else { 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 { /* 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. */ 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; } /* Struct payload: zero the whole slot, then write fields/words * at slot+8+ — keeping the tag word at slot+0 from the zero-fill, * then patch it with the variant tag. */ if (su && su->kind == TY_STRUCT) { ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = 0; k < sz; k += 8) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + k)); int tag = cg_tag_for_variant(du, st); if (src->kind == N_IDENT) { int soff = localfind(*locals_p, src->str); int ssz = (int)su->size; int k = 0; while (k + 8 <= ssz) { ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8 + k)); k += 8; } if (k < ssz) { /* Tail word: load with the right width to * avoid stepping past the source slot. The * zero-fill above means trailing slop is * already clean. */ int tail = ssz - k; int lop = (tail == 4) ? A_MOVL : (tail == 1 ? A_MOVB : A_MOVQ); ins2(c, lop, amem(D_BP, soff + k), areg(D_AX)); ins2(c, lop, areg(D_AX), amem(D_BP, write_off + 8 + k)); } } else if (src->kind == N_STRUCTLIT) { for (Node *f = src->list; f; f = f->next) { u64 foff = 0; int fsz = 8; Type *ftype = NULL; for (Tfield *fl = su->fields; fl; fl = fl->next) { if (strcmp(fl->name, f->str) == 0) { foff = fl->offset; fsz = (int)(fl->type ? fl->type->size : 8); ftype = fl->type; break; } } cgexpr(c, f->lhs, *locals_p); int sl_isf32 = 0; if (fld_isfloat(ftype, &sl_isf32)) { int mov = sl_isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, areg(D_X0), amem(D_BP, write_off + 8 + (int)foff)); continue; } Type *fu = (ftype && ftype->kind == TY_NAMED) ? ftype->under : ftype; if (fu && fu->kind == TY_STR) { /* str IS []u8: 3-word field (ptr,len,cap) * from cgexpr's AX/BX/CX (#1/Phase 3). */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8 + (int)foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 8 + (int)foff + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, write_off + 8 + (int)foff + 16)); continue; } int op = A_MOVQ; if (fsz == 1) op = A_MOVB; else if (fsz == 4) op = A_MOVL; ins2(c, op, areg(D_AX), amem(D_BP, write_off + 8 + (int)foff)); } } 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 = (dst && dst->kind == TY_NAMED) ? dst->under : 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; Type *su = (st && st->kind == TY_NAMED) ? st->under : st; int src_is_struct = su && su->kind == TY_STRUCT; int src_is_tagged = su && su->kind == TY_TAGGED; if (!src_is_struct && !src_is_tagged) { /* Direct-push fast path: str / slice / scalar / pointer. */ cgexpr(c, src, *locals_p); int tag = cg_tag_for_variant(du, st); if (tag < 0) tag = 0; if (type_isstr(st) || (su && su->kind == TY_STR)) { /* str IS []u8: slot 32 [+0]=tag, [+8]=ptr, [+16]=len, * [+24]=cap — same shape as the slice arm below. Push * cap, len, ptr, tag (high→low so pop drains tag first) * (#1/Phase 3). */ ins1(c, A_PUSHQ, areg(D_CX)); /* cap */ ins1(c, A_PUSHQ, areg(D_BX)); /* len */ ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */ ins2(c, A_MOVQ, aimm(tag), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); /* tag */ return; } if (type_isslice(st) || (su && su->kind == TY_SLICE)) { /* slot 32: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap. */ ins1(c, A_PUSHQ, areg(D_CX)); /* cap */ ins1(c, A_PUSHQ, areg(D_BX)); /* len */ ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */ ins2(c, A_MOVQ, aimm(tag), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); /* tag */ return; } /* Scalar / pointer variant. Pad with zero high words when * the slot has room for a wider variant. */ int nwords = sz / 8; for (int k = nwords - 1; k >= 2; k--) { ins2(c, A_XORQ, areg(D_DX), areg(D_DX)); ins1(c, A_PUSHQ, areg(D_DX)); } ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */ ins2(c, A_MOVQ, aimm(tag), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */ return; } int scr; if (cg_tagscr != 0) { if (sz > cg_tagscr_sz) fatal("cg_widen_tagged_push: @tagscr cached sz %d, " "need %d (pinned offset can't grow in place; " "rule 7 — #15/#26c)", cg_tagscr_sz, sz); scr = cg_tagscr; } else { scr = local_alloc(c, locals_p, "@tagscr", sz, cg_frame); cg_tagscr = scr; cg_tagscr_sz = sz; } /* Zero the scratch slot first so any pad word the store path * leaves untouched (struct payload shorter than the slot's value * area) reads as 0 on the callee. The store path then writes the * variant bytes over the zeros. */ ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = 0; k < sz; k += 8) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k)); cg_widen_tagged_store(c, locals_p, dst, src, D_BP, scr, sz); int nwords = sz / 8; for (int k = nwords - 1; k >= 0; k--) { ins2(c, A_MOVQ, amem(D_BP, scr + k * 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); } } /* cg_structlit_fill — fill a struct-typed slot from an N_STRUCTLIT * value into one of three destination flavors. Used by N_LET, N_ASSIGN * N_IDENT-lhs, N_RETURN N_STRUCTLIT (BP-rel), and N_ASSIGN N_DOT-lhs * (BP-rel / via *struct local / via struct global) at single-dot and * chained-dot sites. * * Destination modes: * DST_BP — base = BP, no reload. Stores at disp+i(BP). * srcoff/name unused. * DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before the * ELLIPSIS zero-fill loop and before EVERY field * store (cgexpr clobbers BX between fields). * Stores at disp+i(BX). name unused. * DST_GLOBAL — base = BX, reloaded via `LEAQ name(SB), BX` with * the same reload cadence as DST_PTR_LOCAL. * srcoff unused. * * Param semantics (locked in here so the recursion contract is clear): * - `disp` is the per-recursion accumulator — grows by `foff` as * we descend into a nested struct-typed structlit field. * - `srcoff` (DST_PTR_LOCAL) and `name` (DST_GLOBAL) are *constant* * across the whole call tree — they identify the root dst, which * doesn't change with depth. Recursion passes them through. * * Why a helper? The inline field-walk at each call site previously * did `cgexpr(f->lhs); store AX (sized)`. For struct-typed fields * whose value is itself a nested N_STRUCTLIT, cgexpr has no whole- * struct-in-register convention — it lands AX = first qword and the * trailing bytes silently stay zero (or stack garbage). #17 fixed * the BP-rel sites; #18 extends the same recursion to the four * N_ASSIGN N_DOT-lhs structlit walks (single-dot via_ptr/global/ * local + chained depth>=2). * * The non-BP modes emit a redundant BX reload at the start of each * recursive nested zero-fill / each recursive scalar store — this is * correctness-by-construction (BX is always freshly loaded right * before use), and the redundancy only fires on the nested-STRUCTLIT * shapes that didn't compile before. Byte-identity for the no-nested * case (the only shape selfhost source uses today) is preserved * because the existing inline code's reload-before-each-store pattern * matches the helper's per-store reload exactly. * * The scalar store dispatch stays at the explicit {1->MOVB, 4->MOVL, * else MOVQ} shape (not fieldstoreop, which emits MOVW for fsz==2) to * stay byte-identical with cstage pending task #13. */ static void cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit, int mode, int srcoff, const char *name, int disp) { int sz = (int)lu->size; int base_reg = (mode == DST_BP) ? D_BP : D_BX; if (lit->op == TK_ELLIPSIS) { /* `..., ...` autofill — zero the entire slot first so * unmentioned fields read as 0. Sized stores: 8/4/1. For * non-BP modes, reload BX once before the loop (cgexpr-free * region between iterations, so one reload is enough). */ ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); if (mode == DST_PTR_LOCAL) ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX)); else if (mode == DST_GLOBAL) ins2(c, A_LEAQ, masym(c, name), areg(D_BX)); int zi = 0; while (zi + 8 <= sz) { ins2(c, A_MOVQ, areg(D_AX), amem(base_reg, disp + zi)); zi += 8; } while (zi + 4 <= sz) { ins2(c, A_MOVL, areg(D_AX), amem(base_reg, disp + zi)); zi += 4; } while (zi < sz) { ins2(c, A_MOVB, areg(D_AX), amem(base_reg, disp + zi)); zi += 1; } } for (Node *f = lit->list; f; f = f->next) { u64 foff = 0; int fsz = 8; Type *ft = NULL; for (Tfield *fl = lu->fields; fl; fl = fl->next) { if (strcmp(fl->name, f->str) == 0) { foff = fl->offset; fsz = (int)(fl->type ? fl->type->size : 8); ft = fl->type; break; } } Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; if (fu && fu->kind == TY_TAGGED) { /* Tagged store: reload BX first (if non-BP) so the * widener sees a valid base reg. The widener itself * preserves base_reg through its internal cgexpr. */ if (mode == DST_PTR_LOCAL) ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX)); else if (mode == DST_GLOBAL) ins2(c, A_LEAQ, masym(c, name), areg(D_BX)); cg_widen_tagged_store(c, locals_p, fu, f->lhs, base_reg, disp + (int)foff, (int)fu->size); continue; } /* Nested struct-typed structlit value: recurse at the * field's offset so all inner fields land. Pre-#17/#18 the * cgexpr-then-store below would land AX = first qword and * the rest silently stayed zero. */ if (fu && fu->kind == TY_STRUCT && f->lhs && f->lhs->kind == N_STRUCTLIT) { cg_structlit_fill(c, locals_p, fu, f->lhs, mode, srcoff, name, disp + (int)foff); continue; } /* Nested struct-typed CALL value (#20). cgexpr leaves * AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23] per * #4's cgreturn ABI. Pre-#20 the cgexpr-then-AX-store * fallthrough below silently dropped past the first * qword for any fsz > 8 (only AX got stored). * * Sized stores: MOVQ for full 8B chunks plus a sized tail * (MOVL/MOVW/MOVB) by `tail = fsz%8`. Mirrors #4's receive * shape at the N_LET / N_ASSIGN call-rhs sites; the * MOVW-for-tail==2 emission only fires on shapes that * didn't compile before, so no #13 byte-identity concern. * * Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches #4's * cgreturn ABI: >24B falls through (sret deferred); * fsz%8 ∈ {3,5,6,7} would need shift-store and is also * unsupported by #4 — falls through to the existing * AX-only wrongness (consistent, tracked as follow-up). * * INVARIANT: between cgexpr(N_CALL) and the AX/DX/CX * stores below, NO instruction may touch AX/DX/CX. The * BX reload (MOVQ/LEAQ) is safe; any other emission * added here will silently corrupt the return value. */ if (fu && fu->kind == TY_STRUCT && f->lhs && f->lhs->kind == N_CALL && fsz <= 24 && (fsz % 8 == 0 || fsz % 8 == 1 || fsz % 8 == 2 || fsz % 8 == 4)) { cgexpr(c, f->lhs, *locals_p); if (mode == DST_PTR_LOCAL) ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX)); else if (mode == DST_GLOBAL) ins2(c, A_LEAQ, masym(c, name), areg(D_BX)); int regs[3] = { D_AX, D_DX, D_CX }; int full = fsz / 8; int tail = fsz % 8; for (int i = 0; i < full; i++) ins2(c, A_MOVQ, areg(regs[i]), amem(base_reg, disp + (int)foff + i * 8)); if (tail > 0) { int op = (tail == 4) ? A_MOVL : (tail == 2) ? A_MOVW : A_MOVB; ins2(c, op, areg(regs[full]), amem(base_reg, disp + (int)foff + full * 8)); } continue; } /* str IS []u8: 3-word field (ptr,len,cap). cgexpr leaves * AX/BX/CX; for non-BP modes the dst base goes in DX to dodge * BX=len / CX=cap (the generic store below reloads BX, which * would clobber len) (#1/Phase 3). */ if (fu && fu->kind == TY_STR) { cgexpr(c, f->lhs, *locals_p); if (mode == DST_BP) { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, disp + (int)foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, disp + (int)foff + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, disp + (int)foff + 16)); } else { if (mode == DST_PTR_LOCAL) ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_DX)); else ins2(c, A_LEAQ, masym(c, name), areg(D_DX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, disp + (int)foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, disp + (int)foff + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, disp + (int)foff + 16)); } continue; } cgexpr(c, f->lhs, *locals_p); /* For non-BP modes, cgexpr just clobbered BX; reload it * before the store. */ if (mode == DST_PTR_LOCAL) ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX)); else if (mode == DST_GLOBAL) ins2(c, A_LEAQ, masym(c, name), areg(D_BX)); int sl_isf32 = 0; if (fld_isfloat(ft, &sl_isf32)) { int mov = sl_isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, areg(D_X0), amem(base_reg, disp + (int)foff)); continue; } int op = A_MOVQ; if (fsz == 1) op = A_MOVB; else if (fsz == 4) op = A_MOVL; ins2(c, op, areg(D_AX), amem(base_reg, disp + (int)foff)); } } /* Thin wrapper preserving the BP-rel call shape used by N_LET, * N_ASSIGN N_IDENT-lhs, and N_RETURN. Byte-identical to the pre-#18 * helper. */ static void cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off) { cg_structlit_fill(c, locals_p, lu, lit, DST_BP, 0, NULL, bp_off); } 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) { if (node_isfloat(n)) { int op = op_for(n, A_MOVSD, A_MOVSS); ins2(c, op, amem(D_BP, off), areg(D_X0)); } else if (node_isstr(n)) { /* str IS []u8: flow as (AX=ptr, BX=len, CX=cap), * mirroring the slice local load below (#1/Phase 3). */ ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX)); } else if (node_isslice(n)) { /* slice values flow as (AX=ptr, BX=len, CX=cap) * — mirror the global-slice load so a slice * local can be reassigned, returned, or copied * with the same triple convention. */ ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX)); } else { ins2(c, localloadop(n->type), amem(D_BP, off), areg(D_AX)); } } else { /* Non-local: function symbols load by address (LEAQ), * str-typed `def`s expand to (ptr, len) of the literal, * other globals (def constants) load by value (MOVQ). */ Type *t = n->type; Type *u = (t && t->kind == TY_NAMED) ? t->under : t; if (u && u->kind == TY_FN) { /* Take the address of a function. Apply * @symbol resolution so taking the address * of a body-less FFI binding yields the C * symbol, not the ww-side ident. Hare emits * the same `$symname` for both call and * address-of via QBE; here we mirror that. * Bare ident → same-module by ww's resolver, * so c->cur_mod is the right disambiguation * hint. */ ins2(c, A_LEAQ, mafn(c, n->str, c->cur_mod), areg(D_AX)); break; } { /* Same-module-first walk over Sdef. Without * the prefer pass two modules with same-leaf * `def MSG: str = "..."` silently fold the * wrong strlit into the caller's bare-ident * load (sister callsite of cgdot's str-def * field fold + wwstage deflookuprhs #4c). */ Sdef *s; for (s = sdefs; s; s = s->next) { if (strcmp(s->name, n->str) != 0) continue; if (sdef_mod_match(c, s)) break; } if (s == NULL) { for (s = sdefs; s; s = s->next) if (strcmp(s->name, n->str) == 0) break; } if (s != NULL) { const char *lab = intern_strlit(c, s->bytes, s->len); ins2(c, A_LEAQ, asym(lab), areg(D_AX)); ins2(c, A_MOVQ, aimm((long long)s->len), areg(D_BX)); /* str IS []u8: cap = len for a static * def literal (#1/Phase 3). */ ins2(c, A_MOVQ, aimm((long long)s->len), areg(D_CX)); goto ident_done; } } if (let_islet(n->str) && (let_isstr(n->type) || let_isslice(n->type))) { /* Top-level str/slice global: load each word * via its address (the asm has no `name+8(SB)` * operand form). str IS []u8 now — both carry a * third 8B (cap); the address holder CX gets * overwritten by the cap as the last step, after * we no longer need it (#1/Phase 3). */ ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod), areg(D_CX)); ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX)); goto ident_done; } if (let_isfloat(n->type)) { /* Top-level float global (let OR def): same * LEAQ-indirect shape as str/slice, since * MOVSS/MOVSD have no D_EXTERN operand form in * w6a. Pre-#129 this gated on `let_islet` so * float defs fell through to the MOVQ-AX * integer-convention fallback below; that * load-shape mismatched the float storage emit * (#129 Phase A.1 LOAD-side twin of the * emit_floatlit_data DATA-side SSoT). */ int op = type_isf32(n->type) ? A_MOVSS : A_MOVSD; ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod), areg(D_CX)); ins2(c, op, amem(D_CX, 0), areg(D_X0)); goto ident_done; } /* Top-level lets can be the target of `*p` deref-stores * (via `&letname: *iN`), so a signed-narrow scalar let * needs MOVSXD/MOVSWQ/MOVSBQ on the read. Defs are * read-only constants — their address cannot escape, * so they keep the simpler MOVQ shape (and the wwstage * defent registry, which doesn't track the declared * type, agrees byte-for-byte). */ int gop = let_islet(n->str) ? localloadop(n->type) : A_MOVQ; if (gop == A_MOVQ) { ins2(c, A_MOVQ, mahint(c, n->str, c->cur_mod), areg(D_AX)); } else { /* w6a has no MOVSXD/MOVSWQ/MOVSBQ D_EXTERN * source form, so route through a LEAQ scratch * the same way top-level str/slice/float lets * do. */ ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod), areg(D_CX)); ins2(c, gop, amem(D_CX, 0), areg(D_AX)); } } ident_done: break; } case N_UN: /* Address-of has its own evaluation strategy — we want the * address of the operand, not its value. Special-case before * the cgexpr pre-eval below so `&arr[i]` doesn't compile the * value load and then discard it. */ if (n->op == TK_AMP) { Node *opnd = n->lhs; if (opnd && opnd->kind == N_IDENT) { int off = localfind(locals, opnd->str); Type *ot = opnd->type; Type *ou = (ot && ot->kind == TY_NAMED) ? ot->under : ot; if (off != 0) { ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX)); } else if (ou && ou->kind == TY_FN) { /* #180: address-of a top-level fn name. * Twin of the N_IDENT TY_FN read-arm at * line 2330 (mafn with c->cur_mod hint). * Previously this fell through silently — * the AX-store at the assign site picked * up whatever AX held from prior code, so * `let f = &add1; (*f)(7)` jumped through * stale AX. */ ins2(c, A_LEAQ, mafn(c, opnd->str, c->cur_mod), areg(D_AX)); } else if (let_islet(opnd->str) || def_isstructdef(opnd->str) || def_isarraydef(opnd->str) || def_isscalardef(opnd->str)) { /* #149/#147: address-of a top-level def * with DATA storage. emit_defs / emit_ * struct_data / emit_array_data all emit * to mod_mangle(name), so the address is * the same LEAQ name(SB) as a let. The * address-of twin of A.2/A.3's LOAD-side * widening. */ ins2(c, A_LEAQ, masym(c, opnd->str), areg(D_AX)); } else if (def_isanydef(opnd->str)) { /* #149/#147 rule-7: the name IS a def but * has no DATA symbol (str def inlined, or * computed-rhs float like `def NAN = * 0.0/0.0`). Loud, not a wild deref. */ fatal("cannot take address of non-" "addressable def '%s': no DATA symbol " "(str/computed-rhs def; #149/#147)", opnd->str); } break; } if (opnd && opnd->kind == N_DOT) { /* #149 Shape 2: `&mod.G` — module-qualified * address-of of an exported global (let or def). * The checker leaves SK_USE module idents untyped * (NULL/ty_err); detect that and LEAQ the leaf * symbol. Kind-agnostic (covers cross-module &let * / &def / &scalar) — the address-of twin of the * value-read mod-qual path below. A TY_FN leaf * resolves via mafn (fn address), mirroring the * read path's TY_FN branch. Placed before the * spine walk, which aborts on the untyped base * anyway. */ if (opnd->lhs && opnd->lhs->kind == N_IDENT && (opnd->lhs->type == NULL || opnd->lhs->type == ty_err)) { Type *lt = opnd->type; Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; if (lu && lu->kind == TY_FN) ins2(c, A_LEAQ, mafn(c, opnd->str, opnd->lhs->str), areg(D_AX)); else /* #229: dotted-module value * mangle (twin of the read), so * &aa.v takes aa's global, not a * same-leaf collision. */ ins2(c, A_LEAQ, mahint(c, opnd->str, opnd->lhs->str), areg(D_AX)); break; } /* Address-of through a DOT chain. The early-exit * above handled `&ident` and `&base[i]`; everything * else was silently dropped. Three shapes converge * here, all returning an 8B address (so no * fldloadop dispatch — just LEAQ). * * 1. Value-struct fields, any depth (`&o.f`, * `&o.i.a`, `&o.a.b.c`): walk the spine to a * root ident, sum field offsets, emit LEAQ at * base + sum. Mirror of the read at line 3722. * 2. Slice/str pseudo-field tail (`&s.len`, * `&b.buf.len`): folds into the spine walk * with slice_delta 0/8/16. * 3. Pointer-field (`&p.f` where p:*T): the spine * walk aborts at the *T base; the fallback * below loads p into AX and adds field_off. */ int amped = 0; /* Spine walk — same shape as the read at 3722. * Records (parent_struct, field_name) leaf-first, * then iterates root-first to sum offsets. */ struct { Type *pu; const char *name; } steps[16]; int nsteps = 0; Node *cur = opnd; int abort = 0; while (cur && cur->kind == N_DOT && cur->lhs) { Type *pt = cur->lhs->type; Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt; if (!pu) { abort = 1; break; } if (cur == opnd && (pu->kind == TY_SLICE || pu->kind == TY_STR)) { /* leaf pseudo on slice/str header */ } else if (pu->kind != TY_STRUCT) { abort = 1; break; } if (nsteps >= 16) { abort = 1; break; } steps[nsteps].pu = pu; steps[nsteps].name = cur->str; nsteps++; cur = cur->lhs; } if (!abort && cur && cur->kind == N_IDENT && nsteps > 0) { int total_off = 0; int slice_delta = -1; int ok = 1; for (int i = nsteps - 1; i >= 0; i--) { Type *pu = steps[i].pu; if (pu->kind == TY_SLICE || pu->kind == TY_STR) { if (strcmp(steps[i].name, "ptr") == 0) slice_delta = 0; else if (strcmp(steps[i].name, "len") == 0) slice_delta = 8; else if (strcmp(steps[i].name, "cap") == 0) slice_delta = 16; else { ok = 0; break; } } else { Tfield *f = NULL; for (Tfield *fl = pu->fields; fl; fl = fl->next) if (strcmp(fl->name, steps[i].name) == 0) { f = fl; break; } if (!f) { ok = 0; break; } total_off += (int)f->offset; } } if (ok) { int extra = (slice_delta >= 0) ? slice_delta : 0; int root_off = localfind(locals, cur->str); if (root_off != 0) { ins2(c, A_LEAQ, amem(D_BP, root_off + total_off + extra), areg(D_AX)); amped = 1; } else if (let_islet(cur->str)) { /* Two-step global form mirrors the * read path's `LEAQ name,CX → MOVQ * disp(CX),AX`, swapping the MOVQ * for LEAQ. */ ins2(c, A_LEAQ, masym(c, cur->str), areg(D_CX)); ins2(c, A_LEAQ, amem(D_CX, total_off + extra), areg(D_AX)); amped = 1; } } } /* Pointer-field fallback for `&p.f` where p:*T — * the spine walker aborts on the *T base. Load p * into AX, then LEAQ field_off(AX),AX. Mirror of * the read at line 4033. */ if (!amped && opnd->lhs && opnd->lhs->kind == N_IDENT) { Type *bt = opnd->lhs->type; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; if (bu && bu->kind == TY_PTR && bu->sub) { Type *inner = bu->sub; if (inner->kind == TY_NAMED) inner = inner->under; if (inner && inner->kind == TY_STRUCT) { for (Tfield *f = inner->fields; f; f = f->next) { if (strcmp(f->name, opnd->str) != 0) continue; int off = localfind(locals, opnd->lhs->str); ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); ins2(c, A_LEAQ, amem(D_AX, (int)f->offset), areg(D_AX)); amped = 1; break; } } } } if (amped) break; /* Fall through to silent-drop fallback below. */ } if (opnd && opnd->kind == N_INDEX) { /* &base[i] = base + i*esz, no dereference. */ Node *base = opnd->lhs; Node *idx = opnd->rhs; Type *bt = base ? base->type : NULL; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; int esz = (bu && bu->sub) ? (int)bu->sub->size : 1; cgexpr(c, idx, locals); /* idx → AX */ if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } if (base && base->kind == N_IDENT) { int boff = localfind(locals, base->str); int is_arr = bu && bu->kind == TY_ARRAY; if (boff != 0) { if (is_arr) { ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX)); } else { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); } } else if (let_islet(base->str)) { if (is_arr) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); } else { ins2(c, A_MOVQ, masym(c, base->str), areg(D_BX)); } } else { ins2(c, A_XORQ, areg(D_BX), areg(D_BX)); } ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; } /* Complex base: eval to AX, swap into BX, * then add the saved scaled idx. */ ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, base, locals); ins1(c, A_POPQ, areg(D_BX)); ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; } /* Other shapes (& on a complex expr): silent drop, * mirrors the pre-existing fallback. */ break; } 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. */ Type *rt = n->type; Type *ru = (rt && rt->kind == TY_NAMED) ? rt->under : rt; if (ru && ru->kind == TY_FN) break; } /* f64/f32 result rides X0 (SSE), not AX — an integer * MOVQ strands the value off the float ABI and the * caller's MOVSD X0 reads stale bits (#96). Mirrors the * float field/ident load idiom at 1462/1838. */ if (node_isfloat(n)) { ins2(c, node_isf32(n) ? A_MOVSS : A_MOVSD, amem(D_AX, 0), areg(D_X0)); } else { /* Load-twin of the signed-narrow-scalar-reads * sweep (project_cgen_int_cast_no_truncate); * TK_STAR was the omitted site, refiled as * #116. A raw MOVQ pulls 8 bytes through a * narrow `*iN` and overlaps the next element * — the `*p` value reads honest only when the * caller's sink happens to truncate (i32 store, * i32 return). Width-preserving sinks (CMPQ, * 64-bit arith) saw garbage in the high bytes. * localloadop keys MOVSXD/MOVSWQ/MOVSBQ + * MOVL/MOVZWQ/MOVZBQ off n->type, with the * TY_NAMED / TY_ENUM peel pre-folded so an * aliased narrow (`type err = !i32`) lands on * the right opcode. */ ins2(c, localloadop(n->type), amem(D_AX, 0), areg(D_AX)); } break; default: break; } break; case N_BIN: { /* Short-circuit `&&` / `||`. Operands are bool (0/1); the * type checker enforces it. Eval LHS into AX, branch over * RHS on the short-circuit polarity, otherwise eval RHS * into AX. The surviving AX is the result. Must precede * any eager-eval path below — `if (p != nil && p.x > 0)` * would segfault on a nil deref otherwise. */ if (n->op == TK_AND || n->op == TK_OR) { char *end = mklabel(c, n->op == TK_AND ? "andend" : "orend"); int jshrt = (n->op == TK_AND) ? A_JE : A_JNE; cgexpr(c, n->lhs, locals); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, jshrt, abranch(end)); cgexpr(c, n->rhs, locals); label(c, end); break; } /* str == str / str != str — delegate to rt_streq, which * does the byte-by-byte compare. */ if ((n->op == TK_EQ || n->op == TK_NEQ) && node_isstr(n->lhs) && node_isstr(n->rhs)) { /* Push rhs (len, then ptr top) */ if (n->rhs->kind == N_IDENT) { int off = localfind(locals, n->rhs->str); ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); } else { cgexpr(c, n->rhs, locals); /* AX=ptr, BX=len */ ins1(c, A_PUSHQ, areg(D_BX)); ins1(c, A_PUSHQ, areg(D_AX)); } /* Push lhs */ if (n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); } else { cgexpr(c, n->lhs, locals); ins1(c, A_PUSHQ, areg(D_BX)); ins1(c, A_PUSHQ, areg(D_AX)); } ins1(c, A_POPQ, areg(D_DI)); ins1(c, A_POPQ, areg(D_SI)); ins1(c, A_POPQ, areg(D_DX)); ins1(c, A_POPQ, areg(D_CX)); ins1(c, A_CALL, asym("rt_streq")); if (n->op == TK_NEQ) ins2(c, A_XORQ, aimm(1), areg(D_AX)); break; } /* Float comparison: operands are float but the BIN node's * type is bool, so node_isfloat(n) is false — we have to * inspect n->lhs. UCOMISD/UCOMISS sets ZF/CF as if an * unsigned compare, so the JA family is the right Jcc set * regardless of how the operand types are signed. Plan 9's * own w6c picks the same pattern (txt.c around AUCOMISD). * NaN handling: UCOMI sets PF=ZF=CF=1 on unordered (a NaN * operand). IEEE-754: any relop with a NaN operand is * unordered — `!=` true, the other five false. PF must steer * `!=`/`==`/`<`/`<=` (#97): JNE keys on ZF=0 so `nan != nan` * came out false; JE/JB/JBE all fire on the unordered ZF/CF. * `>`/`>=` (JA/JAE) need CF=0, which unordered never gives, * so they are ALREADY NaN-correct and stay byte-identical to * the pre-#97 single-template arm — no redundant PF guard. */ if (n->lhs && node_isfloat(n->lhs) && (n->op == TK_EQ || n->op == TK_NEQ || n->op == TK_LT || n->op == TK_LE || n->op == TK_GT || n->op == TK_GE)) { int isf32 = node_isf32(n->lhs); int mov = isf32 ? A_MOVSS : A_MOVSD; int ucomi = isf32 ? A_UCOMISS : A_UCOMISD; cgexpr(c, n->rhs, locals); /* rhs → X0 */ ins2(c, A_SUBQ, aimm(8), areg(D_SP)); ins2(c, mov, areg(D_X0), amem(D_SP, 0)); cgexpr(c, n->lhs, locals); /* lhs → X0 */ ins2(c, mov, amem(D_SP, 0), areg(D_X1)); ins2(c, A_ADDQ, aimm(8), areg(D_SP)); ins2(c, ucomi, areg(D_X1), areg(D_X0)); if (n->op == TK_NEQ) { /* not-equal OR unordered -> true */ char *t = mklabel(c, "ct"); char *e = mklabel(c, "ce"); ins1(c, A_JNE, abranch(t)); ins1(c, A_JP, abranch(t)); ins2(c, A_MOVQ, aimm(0), areg(D_AX)); ins1(c, A_JMP, abranch(e)); label(c, t); ins2(c, A_MOVQ, aimm(1), areg(D_AX)); label(c, e); break; } if (n->op == TK_EQ || n->op == TK_LT || n->op == TK_LE) { /* unordered -> false; otherwise the ordered Jcc decides */ int op = (n->op == TK_EQ) ? A_JE : (n->op == TK_LT) ? A_JB : A_JBE; char *fl = mklabel(c, "cf"); char *t = mklabel(c, "ct"); char *e = mklabel(c, "ce"); ins1(c, A_JP, abranch(fl)); ins1(c, op, abranch(t)); label(c, fl); ins2(c, A_MOVQ, aimm(0), areg(D_AX)); ins1(c, A_JMP, abranch(e)); label(c, t); ins2(c, A_MOVQ, aimm(1), areg(D_AX)); label(c, e); break; } /* `>`/`>=`: JA/JAE already reject unordered (CF=1), so * keep the pre-#97 single-template shape verbatim. */ int op = (n->op == TK_GT) ? A_JA : A_JAE; char *t = mklabel(c, "ct"); char *e = mklabel(c, "ce"); ins1(c, op, abranch(t)); ins2(c, A_MOVQ, aimm(0), areg(D_AX)); ins1(c, A_JMP, abranch(e)); label(c, t); ins2(c, A_MOVQ, aimm(1), areg(D_AX)); label(c, e); break; } if (node_isfloat(n)) { int isf32 = node_isf32(n); int mov = isf32 ? A_MOVSS : A_MOVSD; cgexpr(c, n->rhs, locals); /* X0 */ ins2(c, A_SUBQ, aimm(8), areg(D_SP)); ins2(c, mov, areg(D_X0), amem(D_SP, 0)); cgexpr(c, n->lhs, locals); /* X0 */ ins2(c, mov, amem(D_SP, 0), areg(D_X1)); ins2(c, A_ADDQ, aimm(8), areg(D_SP)); switch (n->op) { case TK_PLUS: ins2(c, isf32 ? A_ADDSS : A_ADDSD, areg(D_X1), areg(D_X0)); break; case TK_MINUS: ins2(c, isf32 ? A_SUBSS : A_SUBSD, areg(D_X1), areg(D_X0)); break; case TK_STAR: ins2(c, isf32 ? A_MULSS : A_MULSD, areg(D_X1), areg(D_X0)); break; case TK_SLASH: ins2(c, isf32 ? A_DIVSS : A_DIVSD, areg(D_X1), areg(D_X0)); break; default: break; } break; } cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, n->lhs, locals); ins1(c, A_POPQ, areg(D_BX)); switch (n->op) { case TK_PLUS: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; case TK_MINUS: ins2(c, A_SUBQ, areg(D_BX), areg(D_AX)); break; case TK_STAR: ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); break; case TK_SLASH: { /* Use DIV (unsigned) when either operand is an unsigned * integer type — IDIV would sign-extend a u64 with high * bit set into a negative i64 and produce wrong results * (see strconv.u64tos with v = 1 << 63). Signed IDIV * needs CQO to sign-extend RAX into RDX:RAX; zeroing * DX would treat a negative dividend as a huge unsigned * 128-bit value. */ int unsignd = (n->lhs && type_isunsigned(n->lhs->type)) || (n->rhs && type_isunsigned(n->rhs->type)); if (unsignd) ins2(c, A_MOVQ, aimm(0), areg(D_DX)); else ins0(c, A_CQO); ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX)); break; } case TK_PERCENT: { int unsignd = (n->lhs && type_isunsigned(n->lhs->type)) || (n->rhs && type_isunsigned(n->rhs->type)); if (unsignd) ins2(c, A_MOVQ, aimm(0), areg(D_DX)); else ins0(c, A_CQO); ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX)); ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); break; } case TK_AMP: ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); break; case TK_PIPE: ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break; case TK_CARET: ins2(c, A_XORQ, areg(D_BX), areg(D_AX)); break; case TK_LSHIFT: case TK_RSHIFT: { /* shift amount must be in CL. #136: signed RSHIFT uses * SAR (arithmetic, sign-extends MSB); unsigned uses SHR * (logical, zero-fill). LSHIFT is signedness-agnostic * (SHL == SAL at the encoder). */ int unsignd = (n->lhs && type_isunsigned(n->lhs->type)) || (n->rhs && type_isunsigned(n->rhs->type)); int rop = unsignd ? A_SHRQ : A_SARQ; ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); ins2(c, n->op == TK_LSHIFT ? A_SHLQ : rop, areg(D_CX), areg(D_AX)); break; } case TK_EQ: case TK_NEQ: case TK_LT: case TK_LE: case TK_GT: case TK_GE: { /* For ordered comparisons on unsigned operands we must * use the JA/JAE/JB/JBE family — signed Jcc would treat * a u64 with the high bit set as negative (e.g. the * loop guard `n > 0` in strconv.u64tos with n=1<<63). */ int unsignd = (n->lhs && type_isunsigned(n->lhs->type)) || (n->rhs && type_isunsigned(n->rhs->type)); ins2(c, A_CMPQ, areg(D_BX), areg(D_AX)); int op = A_JE; switch (n->op) { case TK_EQ: op = A_JE; break; case TK_NEQ:op = A_JNE; break; case TK_LT: op = unsignd ? A_JB : A_JL; break; case TK_LE: op = unsignd ? A_JBE : A_JLE; break; case TK_GT: op = unsignd ? A_JA : A_JG; break; case TK_GE: op = unsignd ? A_JAE : A_JGE; break; default: break; } char *t = mklabel(c, "ct"); char *e = mklabel(c, "ce"); ins1(c, op, abranch(t)); ins2(c, A_MOVQ, aimm(0), areg(D_AX)); ins1(c, A_JMP, abranch(e)); label(c, t); ins2(c, A_MOVQ, aimm(1), areg(D_AX)); label(c, e); break; } /* TK_AND / TK_OR handled with short-circuit codegen at the * top of N_BIN — they never reach this eager-eval switch. */ default: break; } break; } case N_ASSIGN: { /* Discard lvalue `_ = expr;` — evaluate rhs for side effects, * write nothing. */ if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && n->lhs->str[0] == '\0' && n->op == TK_ASSIGN) { cgexpr(c, n->rhs, locals); break; } /* p.x = v or p.x += v where p.x is a struct field * (direct or via *struct). For compound ops we read-modify- * write the field; for plain `=` we just write. The base * accepts two parser shapes: a bare IDENT (auto-deref when * the IDENT's type is *T, value-struct otherwise) and the * explicit-deref form `(*p).f = ...` where the parser emits * N_UN(STAR, IDENT(p)). For (*p).f, retarget base to the * inner IDENT so the via_ptr branch fires identically to * `p.f = v`. v1 scope: bare-IDENT inner only; (*expr).f * (non-IDENT inner) falls through to the existing drop * behaviour pending follow-up task. */ if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && (n->lhs->lhs->kind == N_IDENT || (n->lhs->lhs->kind == N_UN && n->lhs->lhs->op == TK_STAR && n->lhs->lhs->lhs && n->lhs->lhs->lhs->kind == N_IDENT))) { Node *base = n->lhs->lhs; if (base->kind == N_UN) base = base->lhs; Type *bt = base->type; /* type_chase_named (#22): a chain `type b = a; a = struct` * left u at TY_NAMED a after a single peel, missing the * TY_STRUCT field-walk gate below — the assignment * silently dropped (the `break` at the bottom of the * N_DOT-lhs arm). */ Type *u = type_chase_named(bt); int via_ptr = 0; if (u && u->kind == TY_PTR) { via_ptr = 1; u = type_chase_named(u->sub); } /* slice/str pseudo-field write (.ptr/.len/.cap) */ if (u && (u->kind == TY_SLICE || u->kind == TY_STR)) { const char *fld = n->lhs->str; int delta = -1; if (strcmp(fld, "ptr") == 0) delta = 0; else if (strcmp(fld, "len") == 0) delta = 8; else if (strcmp(fld, "cap") == 0) delta = 16; if (delta < 0) goto after_dot_assign; int boff = localfind(locals, base->str); if (n->op != TK_ASSIGN) { if (via_ptr) { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_BX)); } else { ins2(c, A_MOVQ, amem(D_BP, boff + delta), areg(D_BX)); } ins1(c, A_PUSHQ, areg(D_BX)); } cgexpr(c, n->rhs, locals); if (n->op != TK_ASSIGN) { ins1(c, A_POPQ, areg(D_BX)); switch (n->op) { case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; case TK_MINUSEQ: /* old in BX, rhs in AX; want AX = old-rhs. * SUBQ src,dst is dst -= src in Plan 9. */ ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); break; default: break; } } if (via_ptr) { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, delta)); } else { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + delta)); } break; } after_dot_assign: if (u && u->kind == TY_STRUCT) { /* find field metadata */ Tfield *f = NULL; for (Tfield *fl = u->fields; fl; fl = fl->next) if (strcmp(fl->name, n->lhs->str) == 0) { f = fl; break; } if (f == NULL) break; /* Tagged-union field: synthesise tag and store * value bytes. Compound ops on tagged fields are * not meaningful, so only plain `=` is wired. * Three base shapes: * - via_ptr: base is *struct local; address * pre-loaded into BX. Buggy with a str * variant since cgexpr will overwrite BX, * but matches the existing pre-global * behaviour. * - is_global: struct global. LEAQ after * cgexpr drops the slot address into CX * without touching AX/BX, so str variants * work cleanly. * - else: struct local, BP-relative. */ Type *ft = f->type; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : 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 = (f->type && f->type->kind == TY_NAMED) ? f->type->under : 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; } /* struct-typed field, three rhs shapes: * - N_IDENT: word-copy from the rhs slot directly * onto the destination field. cgexpr cannot * materialise a whole struct value in registers * for an arbitrary local, so we read field words * straight from the source slot. * - N_CALL (added with #5): cgexpr leaves the value * in AX/DX/CX per #4's cgreturn ABI; sized stores * write only the declared field size — MOVQ for * full 8B chunks plus MOVL/MOVW/MOVB tail. See * the N_LET receive site for the ASYMMETRY * rationale. cgreturn touches only AX/DX/CX, so * BX stays free for the dst-addr load after the * call. * - N_STRUCTLIT (added with #5): field-by-field * store; for via_ptr/is_global the dst base addr * is reloaded into BX before each store so cgexpr * can clobber AX/BX between fields. */ if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STRUCT && (int)str_fu->size <= 24 && n->rhs && n->rhs->kind == N_CALL && (str_fu->size % 8 == 0 || str_fu->size % 8 == 1 || str_fu->size % 8 == 2 || str_fu->size % 8 == 4)) { int ssz = (int)str_fu->size; cgexpr(c, n->rhs, locals); int regs[3] = { D_AX, D_DX, D_CX }; int full = ssz / 8; int tail = ssz % 8; int base_reg, base_disp; if (via_ptr || is_global) { if (via_ptr) ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); else ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); base_reg = D_BX; base_disp = foff; } else { base_reg = D_BP; base_disp = boff + foff; } for (int i = 0; i < full; i++) ins2(c, A_MOVQ, areg(regs[i]), amem(base_reg, base_disp + i * 8)); if (tail > 0) { int op = (tail == 4) ? A_MOVL : (tail == 2) ? A_MOVW : A_MOVB; ins2(c, op, areg(regs[full]), amem(base_reg, base_disp + full * 8)); } break; } if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STRUCT && n->rhs && n->rhs->kind == N_STRUCTLIT) { /* Delegate to the shared structlit fill * helper. For via_ptr/is_global, helper * reloads BX before zero-fill loop + each * field store. For local BP-rel, helper * stores direct off BP. AND nested struct- * typed structlit values recurse instead * of silently dropping trailing bytes * (#18 fix). */ int mode = via_ptr ? DST_PTR_LOCAL : is_global ? DST_GLOBAL : DST_BP; int disp = (mode == DST_BP) ? (boff + foff) : foff; cg_structlit_fill(c, &locals, str_fu, n->rhs, mode, boff, is_global ? base->str : NULL, disp); break; } if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STRUCT && n->rhs && n->rhs->kind == N_IDENT && localfind(locals, n->rhs->str) != 0) { int soff = localfind(locals, n->rhs->str); int ssz = (int)str_fu->size; if (via_ptr) ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); else if (is_global) ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); int k = 0; while (k + 8 <= ssz) { ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX)); if (via_ptr || is_global) ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + k)); else ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + k)); k += 8; } if (k < ssz) { int tail = ssz - k; int lop = (tail == 4) ? A_MOVL : (tail == 1 ? A_MOVB : A_MOVQ); ins2(c, lop, amem(D_BP, soff + k), areg(D_AX)); if (via_ptr || is_global) ins2(c, lop, areg(D_AX), amem(D_BX, foff + k)); else ins2(c, lop, areg(D_AX), amem(D_BP, boff + foff + k)); } break; } /* compound: load current value into BX */ if (n->op != TK_ASSIGN) { if (via_ptr) { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); ins2(c, load_op, amem(D_BX, foff), areg(D_BX)); } else if (is_global) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); ins2(c, load_op, amem(D_BX, foff), areg(D_BX)); } else { ins2(c, load_op, amem(D_BP, boff + foff), areg(D_BX)); } ins1(c, A_PUSHQ, areg(D_BX)); } cgexpr(c, n->rhs, locals); /* AX = rhs */ if (n->op != TK_ASSIGN) { ins1(c, A_POPQ, areg(D_BX)); switch (n->op) { case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break; case TK_MINUSEQ: /* old in BX, rhs in AX; want AX=old-rhs */ ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); break; default: break; /* others rare */ } } /* f64/f32 field, plain `=`: cgexpr left the value in * X0, not AX. Route the store via MOVSD/MOVSS. * Compound ops on float fields aren't wired here — * see CLAUDE.md #8 in examples/lisp; same in the * structlit-init path below. */ int b_isf32 = 0; if (n->op == TK_ASSIGN && fld_isfloat(f->type, &b_isf32)) { int mov = b_isf32 ? A_MOVSS : A_MOVSD; if (via_ptr) { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); ins2(c, mov, areg(D_X0), amem(D_BX, foff)); } else if (is_global) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); ins2(c, mov, areg(D_X0), amem(D_BX, foff)); } else { ins2(c, mov, areg(D_X0), amem(D_BP, boff + foff)); } break; } /* now store AX into target */ if (via_ptr) { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); } else if (is_global) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); } else { ins2(c, store_op, areg(D_AX), amem(D_BP, boff + foff)); } break; } } /* `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric * write-side of the cgdot N_INDEX-lhs branch. Compute &arr[i] * inline (LEAQ for `[N]Struct`, MOVQ-load for `[N]*Struct` / * `[]Struct` / `*Struct`), deref once when the element is * `*Struct`, then store rhs at `field.offset(addr)`. The * chained-pointer-field branch below catches `[N]*Struct` * writes via its `!= N_IDENT` guard, but `[N]Struct` value-arrays * fall through and silently drop the store. Placed before the * `!= N_IDENT` branch so both shapes share one path. */ if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && n->lhs->lhs->kind == N_INDEX) { Node *idxbase = n->lhs->lhs->lhs; Node *idx = n->lhs->lhs->rhs; if (idxbase && idxbase->kind == N_IDENT && idx) { Type *elemt = n->lhs->lhs->type; Type *elemu = (elemt && elemt->kind == TY_NAMED) ? elemt->under : elemt; Type *struct_t = NULL; int viaptr = 0; if (elemu && elemu->kind == TY_PTR) { Type *inner = elemu->sub; if (inner && inner->kind == TY_NAMED) inner = inner->under; if (inner && inner->kind == TY_STRUCT) { struct_t = inner; viaptr = 1; } } else if (elemu && elemu->kind == TY_STRUCT) { struct_t = elemu; } if (struct_t) { Tfield *f = NULL; for (Tfield *fl = struct_t->fields; fl; fl = fl->next) if (strcmp(fl->name, n->lhs->str) == 0) { f = fl; break; } Type *bt = idxbase->type; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; int is_arr = bu && bu->kind == TY_ARRAY; int is_sl = bu && bu->kind == TY_SLICE; int is_ptr = bu && bu->kind == TY_PTR; int off = localfind(locals, idxbase->str); if (f != NULL && (is_arr || is_sl || is_ptr) && off != 0) { Type *ft = f->type; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; int fsz = (int)(ft ? ft->size : 8); int store_op = fldstoreop(ft, fsz); int foff = (int)f->offset; int esz = (int)elemt->size; int h_isf32 = 0; if (n->op == TK_ASSIGN && fld_isfloat(ft, &h_isf32)) { int mov = h_isf32 ? A_MOVSS : A_MOVSD; cgexpr(c, n->rhs, locals); ins2(c, A_SUBQ, aimm(8), areg(D_SP)); ins2(c, mov, areg(D_X0), amem(D_SP, 0)); cgexpr(c, idx, locals); if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } if (is_arr) ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); else ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); if (viaptr) ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX)); ins2(c, mov, amem(D_SP, 0), areg(D_X0)); ins2(c, A_ADDQ, aimm(8), areg(D_SP)); ins2(c, mov, areg(D_X0), amem(D_BX, foff)); break; } if (n->op == TK_ASSIGN && fu && (fu->kind == TY_STR || fu->kind == TY_SLICE)) { /* str/slice: rhs leaves * AX=ptr, BX=len, CX=cap * (#1/Phase 3). Spill all * three across the index/ * address computation * (IMULQ's CX scratch * clobbers cap), stage * &arr[i] in DX off the str * AX/BX/CX convention * (mirrors s.f=v), then store * the full triple at * foff+0/+8/+16. */ cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, areg(D_CX)); ins1(c, A_PUSHQ, areg(D_BX)); ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, idx, locals); if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } if (is_arr) ins2(c, A_LEAQ, amem(D_BP, off), areg(D_DX)); else ins2(c, A_MOVQ, amem(D_BP, off), areg(D_DX)); ins2(c, A_ADDQ, areg(D_AX), areg(D_DX)); if (viaptr) ins2(c, A_MOVQ, amem(D_DX, 0), areg(D_DX)); ins1(c, A_POPQ, areg(D_AX)); ins1(c, A_POPQ, areg(D_BX)); ins1(c, A_POPQ, areg(D_CX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16)); break; } if (n->op == TK_ASSIGN) { cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, idx, locals); if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } if (is_arr) ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); else ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); if (viaptr) ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX)); ins1(c, A_POPQ, areg(D_AX)); ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); break; } /* compound: rhs→push; compute * struct addr→BX (deref if *T); * push addr; load old field→AX; * pop addr→BX, rhs→CX; combine; * store. Float/str compound * not wired. */ cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, idx, locals); if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } if (is_arr) ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); else ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); if (viaptr) ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_BX)); ins1(c, A_PUSHQ, areg(D_BX)); int load_op = fldloadop(ft, fsz); ins2(c, load_op, amem(D_BX, foff), areg(D_AX)); ins1(c, A_POPQ, areg(D_BX)); ins1(c, A_POPQ, areg(D_CX)); switch (n->op) { case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); break; case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_CX), areg(D_AX)); break; case TK_STAREQ: ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); break; case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_CX), areg(D_AX)); break; case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_CX), areg(D_AX)); break; case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break; default: break; } ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); break; } } } } /* Chained `.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; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; int fsz = (int)(ft ? ft->size : 8); int store_op = fldstoreop(ft, fsz); int foff = (int)f->offset; if (n->op == TK_ASSIGN) { 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 (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; Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : 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 = (pu->sub->kind == TY_NAMED) ? pu->sub->under : 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; } Type *fu = (leaf_type && leaf_type->kind == TY_NAMED) ? leaf_type->under : leaf_type; int fsz = (int)(leaf_type ? leaf_type->size : 8); int store_op = fldstoreop(leaf_type, fsz); if (fu && (fu->kind == TY_STR || fu->kind == TY_SLICE)) { /* str/slice: store ptr/len/cap. cgexpr * leaves CX=cap, so the via_cx base goes in * DX (not CX) to avoid clobbering it — same * as the single-dot str field store * (#1/Phase 3). */ cgexpr(c, n->rhs, locals); if (via_cx) { if (ptr_root) ins2(c, A_MOVQ, amem(D_BP, base_disp), areg(D_DX)); else ins2(c, A_LEAQ, masym(c, cur->str), areg(D_DX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, total_off + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, total_off + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, total_off + 16)); } else { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, base_disp + total_off + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, base_disp + total_off + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, base_disp + total_off + 16)); } break; } /* TY_STRUCT terminal in the chained-DOT walker: * three rhs shapes — mirror of the single-dot * branch. * - N_IDENT: word-copy from rhs local slot. * - N_CALL (added with #5): cgexpr → AX/DX/CX * per #4's cgreturn ABI; sized stores per * declared field size. cgreturn touches only * AX/DX/CX so via_cx loads the dst addr into * BX (not CX) after the call to keep CX as * the third value word. * - N_STRUCTLIT (added with #5): field-by-field * store; via_cx reloads BX before each store * so cgexpr can clobber AX/BX between fields. */ if (fu && fu->kind == TY_STRUCT && fsz <= 24 && n->rhs && n->rhs->kind == N_CALL && (fsz % 8 == 0 || fsz % 8 == 1 || fsz % 8 == 2 || fsz % 8 == 4)) { cgexpr(c, n->rhs, locals); int regs[3] = { D_AX, D_DX, D_CX }; int full = fsz / 8; int tail = fsz % 8; int base_reg, base_off; if (via_cx) { if (ptr_root) ins2(c, A_MOVQ, amem(D_BP, base_disp), areg(D_BX)); else ins2(c, A_LEAQ, masym(c, cur->str), areg(D_BX)); base_reg = D_BX; base_off = total_off; } else { base_reg = D_BP; base_off = base_disp + total_off; } for (int i = 0; i < full; i++) ins2(c, A_MOVQ, areg(regs[i]), amem(base_reg, base_off + i * 8)); if (tail > 0) { int op = (tail == 4) ? A_MOVL : (tail == 2) ? A_MOVW : A_MOVB; ins2(c, op, areg(regs[full]), amem(base_reg, base_off + full * 8)); } break; } if (fu && fu->kind == TY_STRUCT && n->rhs && n->rhs->kind == N_STRUCTLIT) { /* Delegate to the shared structlit fill * helper. For via_cx (ptr_root | is_global), * helper reloads BX before zero-fill loop + * each field store. For local through chain, * helper stores direct off BP. AND nested * struct-typed structlit values recurse * instead of silently dropping trailing * bytes (#18 fix). */ int dst_mode = ptr_root ? DST_PTR_LOCAL : is_global ? DST_GLOBAL : DST_BP; int dst_disp = (dst_mode == DST_BP) ? (base_disp + total_off) : total_off; cg_structlit_fill(c, &locals, fu, n->rhs, dst_mode, base_disp, is_global ? cur->str : NULL, dst_disp); break; } if (fu && fu->kind == TY_STRUCT && n->rhs && n->rhs->kind == N_IDENT && localfind(locals, n->rhs->str) != 0) { int soff = localfind(locals, n->rhs->str); int ssz = fsz; if (via_cx) { if (ptr_root) ins2(c, A_MOVQ, amem(D_BP, base_disp), areg(D_CX)); else ins2(c, A_LEAQ, masym(c, cur->str), areg(D_CX)); } int k = 0; while (k + 8 <= ssz) { ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX)); if (via_cx) ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, total_off + k)); else ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, base_disp + total_off + k)); k += 8; } if (k < ssz) { int tail = ssz - k; int lop = (tail == 4) ? A_MOVL : (tail == 1 ? A_MOVB : A_MOVQ); ins2(c, lop, amem(D_BP, soff + k), areg(D_AX)); if (via_cx) ins2(c, lop, areg(D_AX), amem(D_CX, total_off + k)); else ins2(c, lop, areg(D_AX), amem(D_BP, base_disp + total_off + k)); } break; } int sf32 = 0; if (fld_isfloat(leaf_type, &sf32)) { int mov = sf32 ? A_MOVSS : A_MOVSD; cgexpr(c, n->rhs, locals); if (via_cx) { if (ptr_root) ins2(c, A_MOVQ, amem(D_BP, base_disp), areg(D_CX)); else ins2(c, A_LEAQ, masym(c, cur->str), areg(D_CX)); ins2(c, mov, areg(D_X0), amem(D_CX, total_off)); } else { ins2(c, mov, areg(D_X0), amem(D_BP, base_disp + total_off)); } break; } cgexpr(c, n->rhs, locals); if (via_cx) { if (ptr_root) ins2(c, A_MOVQ, amem(D_BP, base_disp), areg(D_CX)); else ins2(c, A_LEAQ, masym(c, cur->str), areg(D_CX)); ins2(c, store_op, areg(D_AX), amem(D_CX, total_off)); } else { ins2(c, store_op, areg(D_AX), amem(D_BP, base_disp + total_off)); } break; } } } } /* float assignment to a local or top-level global. Globals * route through LEAQ+indirect (no D_EXTERN SSE in w6a). * Compound (`acc += d` etc.) loads slot into X1, combines * into X1 (Plan 9 syntax: OP src, dst), stores X1 back — * w6a's ADDSD/SUBSD/MULSD/DIVSD are register-register only, * so we can't use a direct mem-form like the integer ADDQ. */ if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) { cgexpr(c, n->rhs, locals); /* X0 */ int mvop = op_for(n, A_MOVSD, A_MOVSS); int addop = op_for(n, A_ADDSD, A_ADDSS); int subop = op_for(n, A_SUBSD, A_SUBSS); int mulop = op_for(n, A_MULSD, A_MULSS); int divop = op_for(n, A_DIVSD, A_DIVSS); int off = localfind(locals, n->lhs->str); int isglobal = (off == 0) && let_islet(n->lhs->str); if (off == 0 && !isglobal) break; 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) { Node *base = n->lhs->lhs; Type *bt = base->type; Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; int is_arr = u && u->kind == TY_ARRAY; int is_sl = u && u->kind == TY_SLICE; int is_ptr = u && u->kind == TY_PTR; /* For `*[N]T` drill through to the array so esz reflects * T, not sizeof(array). Base load still uses u (MOVQ * because is_ptr stays true). */ Type *eff = idx_eff(bt); int esz = (eff && eff->sub) ? (int)eff->sub->size : 1; int elem_is_str = eff && eff->sub && type_isstr(eff->sub); int elem_is_slice = eff && eff->sub && type_isslice(eff->sub); Type *esub = eff ? eff->sub : NULL; Type *esubu = (esub && esub->kind == TY_NAMED) ? esub->under : esub; int elem_tagged = esubu && esubu->kind == TY_TAGGED; /* Tagged-union element: route widening through a * scratch slot, then copy slot bytes to &arr[i]. * Materialising into the scratch first lets us reuse * the full cg_widen_tagged_store machinery — scalar / * str / struct / subset payloads, tag remap, nullable * fold — without duplicating it. The scratch lives in * the function frame; no cleanup needed. */ if ((is_arr || is_sl || is_ptr) && elem_tagged) { int ssz = esz; int scr; if (cg_tagscr != 0) { if (ssz > cg_tagscr_sz) fatal("N_INDEX tagged: " "@tagscr cached sz %d, " "need %d (pinned offset " "can't grow in place; " "rule 7 — #15/#26c)", cg_tagscr_sz, ssz); scr = cg_tagscr; } else { scr = local_alloc(c, &locals, "@tagscr", ssz, cg_frame); cg_tagscr = scr; cg_tagscr_sz = ssz; } ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); for (int k = 0; k < ssz; k += 8) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k)); cg_widen_tagged_store(c, &locals, esubu, n->rhs, D_BP, scr, ssz); /* Compute &arr[i] → BX. */ cgexpr(c, n->lhs->rhs, locals); if (ssz > 1) { ins2(c, A_MOVQ, aimm(ssz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } if (base->kind == N_IDENT && is_arr) { int boff = localfind(locals, base->str); ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX)); } else if (base->kind == N_IDENT) { int boff = localfind(locals, base->str); ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); } else { 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; } if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN) { cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len,CX=cap if str) */ /* str/slice: stash cap+len so all three store * (#1/Phase 3). */ if (elem_is_str || elem_is_slice) { ins1(c, A_PUSHQ, areg(D_CX)); /* cap */ ins1(c, A_PUSHQ, areg(D_BX)); /* len */ } /* Float element: spill X0 (not AX — AX is junk * for floats) across the idx/base eval. A call- * index (`a[geti()]=v`) clobbers X0 and would * otherwise lose the value. Mirrors the *p=v * float deref store at cgen.c:4187 (#125). */ int sp_isfloat = type_isfloat(esub); int sp_mov = sp_isfloat ? (type_isf32(esub) ? A_MOVSS : A_MOVSD) : 0; if (sp_isfloat) { ins2(c, A_SUBQ, aimm(8), areg(D_SP)); ins2(c, sp_mov, areg(D_X0), amem(D_SP, 0)); } else { ins1(c, A_PUSHQ, areg(D_AX)); } cgexpr(c, n->lhs->rhs, locals); /* idx → AX */ if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */ /* base address → BX. Top-level array → LEAQ * name(SB); top-level ptr → MOVQ name(SB); locals * route off BP. */ if (base->kind == N_IDENT) { int off = localfind(locals, base->str); int isglobal = (off == 0) && let_islet(base->str); if (isglobal && is_arr) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); } else if (isglobal) { ins2(c, A_MOVQ, masym(c, base->str), areg(D_BX)); } else if (is_arr) { ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); } else { ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); } } else if (cg_dotbase_addr(c, base, D_BX, locals)) { /* #135 site: N_DOT base resolved inline to * the field address; cgexpr fallback below * would auto-deref + load the field as a * VALUE (the broken shape). */ } else { cgexpr(c, base, locals); ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); } ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */ ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); /* Reload value: float reloads X0 from the spill * slot; non-float pops AX. Twin of the value-spill * site above (#125). */ if (sp_isfloat) { ins2(c, sp_mov, amem(D_SP, 0), areg(D_X0)); ins2(c, A_ADDQ, aimm(8), areg(D_SP)); } else { ins1(c, A_POPQ, areg(D_AX)); /* value (ptr if str) */ } if (elem_is_str || elem_is_slice) { /* str/slice: store ptr/len/cap (#1/Phase 3, #7). */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0)); ins1(c, A_POPQ, areg(D_CX)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8)); ins1(c, A_POPQ, areg(D_CX)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16)); break; } /* float element → store FROM X0 (MOVSS/MOVSD): cgexpr * leaves a float value in X0, and for f32 the #104 * CVTSD2SS narrowing only touches X0 — the AX path * below would store the raw double low-bits (garbage * for f32). Float-ness from esub, mirroring the read * side at cgen.c:6423 (#122). #125: the value-spill * pair above keeps X0 live across the idx/base eval * so this MOVSS/MOVSD is correct even on call-index * shapes. */ if (type_isfloat(esub)) { int mov = type_isf32(esub) ? A_MOVSS : A_MOVSD; ins2(c, mov, areg(D_X0), amem(D_BX, 0)); break; } int store_op = fldstoreop(esub, esz); ins2(c, store_op, areg(D_AX), amem(D_BX, 0)); break; } /* Compound assign on an indexed scalar element * (`arr[i] OP= v`). Pre-#133 this branch had no TK_ASSIGN * gate above and silently DEMOTED compound ops to plain * stores (no load, no op). Mirror the chained-pointer- * field compound template at cgen.c:3281-3317: same * address computation as the ASSIGN body above, then * load_op (BX)→AX, pop rhs→CX, combine, store_op. * #133-expanded: all 10 integer compound ops wired; * float/str/slice/tagged element compound HARD-ERRORS * loud (rule-7, replaces prior silent fall-through). * #136: signed RSHIFTEQ now uses A_SARQ (arithmetic * shift). */ if ((is_arr || is_sl || is_ptr) && n->op != TK_ASSIGN) { if (elem_is_str) fatal("indexed-lvalue compound on " "str element not wired " "(#133/rule-7)"); if (elem_is_slice) fatal("indexed-lvalue compound on " "slice element not wired " "(#133/rule-7)"); if (elem_tagged) fatal("indexed-lvalue compound on " "tagged element not wired " "(#133/rule-7)"); if (esub && type_isfloat(esub)) fatal("indexed-lvalue compound on " "float element not wired " "(#133/rule-7)"); cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, n->lhs->rhs, locals); if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } ins1(c, A_PUSHQ, areg(D_AX)); if (base->kind == N_IDENT) { int off = localfind(locals, base->str); int isglobal = (off == 0) && let_islet(base->str); if (isglobal && is_arr) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX)); } else if (isglobal) { ins2(c, A_MOVQ, masym(c, base->str), areg(D_BX)); } else if (is_arr) { ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); } else { ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); } } else if (cg_dotbase_addr(c, base, D_BX, locals)) { /* #135 site: N_DOT base resolved inline to the * field address. */ } else { cgexpr(c, base, locals); ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); } ins1(c, A_POPQ, areg(D_AX)); ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); int load_op = fldloadop(esub, esz); ins2(c, load_op, amem(D_BX, 0), areg(D_AX)); ins1(c, A_POPQ, areg(D_CX)); int unsignd_c = esub && type_isunsigned(esub); switch (n->op) { case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); break; case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_CX), areg(D_AX)); break; case TK_STAREQ: ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); break; case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_CX), areg(D_AX)); break; case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_CX), areg(D_AX)); break; case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break; case TK_SLASHEQ: if (unsignd_c) ins2(c, A_MOVQ, aimm(0), areg(D_DX)); else ins0(c, A_CQO); ins1(c, unsignd_c ? A_DIVQ : A_IDIVQ, areg(D_CX)); break; case TK_PERCENTEQ: if (unsignd_c) ins2(c, A_MOVQ, aimm(0), areg(D_DX)); else ins0(c, A_CQO); ins1(c, unsignd_c ? A_DIVQ : A_IDIVQ, areg(D_CX)); ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); break; case TK_LSHIFTEQ: ins2(c, A_SHLQ, areg(D_CX), areg(D_AX)); break; case TK_RSHIFTEQ: ins2(c, unsignd_c ? A_SHRQ : A_SARQ, areg(D_CX), areg(D_AX)); break; default: fatal("indexed-lvalue compound: " "unknown op tk=%d (#133/rule-7)", n->op); } int store_op_c = fldstoreop(esub, esz); ins2(c, store_op_c, areg(D_AX), amem(D_BX, 0)); break; } } /* Plain `r = expr;` where r is a tagged-union local. * Delegates to cg_widen_tagged_store: covers nullable fold, * tagged→tagged (with tag remap), struct payload (ident or * literal), str payload, and scalar payload. */ if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN && n->lhs->type) { Type *lt = n->lhs->type; Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; if (lu && lu->kind == TY_TAGGED) { int off = localfind(locals, n->lhs->str); if (off == 0) break; cg_widen_tagged_store(c, &locals, lu, n->rhs, D_BP, off, (int)lu->size); break; } } /* 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) { Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL; Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt; Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL; if (vt && vt->kind == TY_NAMED) vt = vt->under; /* `*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 = (pt && pt->kind == TY_NAMED) ? pt->under : pt; Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL; if (vt && vt->kind == TY_NAMED) vt = vt->under; 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 = (lt && lt->kind == TY_NAMED) ? lt->under : 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): `s = f();` where s is a struct * local >24B. s's slot IS the caller-prealloc dest; * the callee writes through hidden RDI. Mirrors the * cglet branch above. */ if (lu && lu->kind == TY_STRUCT && (int)lu->size > 24 && 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. */ if (let_islet(n->lhs->str)) { cg_sret_dest_sym = n->lhs->str; cgexpr(c, n->rhs, locals); cg_sret_dest_sym = NULL; break; } } /* Struct local reassignment: `s = expr;` where s is * a TY_STRUCT local of size <=24B. Two rhs shapes, * mirroring cglet's N_STRUCTLIT and the call-result * branch above: * - N_STRUCTLIT: walk fields, store at off+foff * directly (same shape as the let-init branch). * - N_CALL: cgexpr → AX/DX/CX, sized stores per the * same ASYMMETRY rules documented at the N_LET * receive site (MOVQ for full 8B chunks plus * MOVL/MOVW/MOVB tail). The struct-IDENT word-copy * rhs shape (s = p) is left unwired; #5 is scoped to * the receive side of #4's cgreturn (calls + literals). * Sizes >24B and non-{0,1,2,4}-byte tails fall through * to the existing scalar path. */ if (lu && lu->kind == TY_STRUCT && (int)lu->size <= 24) { int off = localfind(locals, n->lhs->str); if (off != 0) { int sz = (int)lu->size; if (n->rhs && n->rhs->kind == N_STRUCTLIT) { /* 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). */ cg_structlit_fill_bp(c, &locals, lu, n->rhs, off); break; } if (n->rhs && n->rhs->kind == N_CALL && (sz % 8 == 0 || sz % 8 == 1 || sz % 8 == 2 || sz % 8 == 4)) { cgexpr(c, n->rhs, locals); int regs[3] = { D_AX, D_DX, D_CX }; int full = sz / 8; int tail = sz % 8; for (int i = 0; i < full; i++) ins2(c, A_MOVQ, areg(regs[i]), amem(D_BP, off + i * 8)); if (tail > 0) { int op = (tail == 4) ? A_MOVL : (tail == 2) ? A_MOVW : A_MOVB; ins2(c, op, areg(regs[full]), amem(D_BP, off + full * 8)); } break; } } } } if (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). Names * we don't recognise as scalar lets fall through * to the existing drop behaviour, which produces * a clean link-time undefined-symbol error if * the binding was ever supposed to exist. */ if (!let_islet(n->lhs->str)) break; 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: ; } 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; Type *at = a->type; Type *u = (at && at->kind == TY_NAMED) ? at->under : at; if (u && (u->kind == TY_SLICE || u->kind == TY_STR) && a->kind == N_IDENT) { int off = localfind(locals, a->str); if (off == 0 && let_islet(a->str)) { /* #231: str/slice GLOBAL — the .len word * lives at the global's address+8, not a * BP-relative slot (off==0 → MOVQ 8(BP) * read a bogus stack slot). Route through * the post-#1 value mangle so a private * same-module same-leaf global isn't * mis-resolved. */ ins2(c, A_LEAQ, mahint(c, a->str, c->cur_mod), areg(D_CX)); ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_AX)); } else { ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); } } else if (u && u->kind == TY_ARRAY) { ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX)); } else { /* fall back: load via .len pseudo-field */ cgexpr(c, a, locals); } break; } if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && strcmp(n->lhs->str, "free") == 0 && n->list && n->list->next == NULL) { Node *p = n->list; Type *pt = p->type; Type *u = (pt && pt->kind == TY_NAMED) ? pt->under : pt; if (u && u->kind == TY_PTR && u->sub) { int sz = (int)u->sub->size; if (sz == 0) sz = 8; cgexpr(c, p, locals); ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); ins2(c, A_MOVQ, aimm(sz), areg(D_SI)); ins1(c, A_CALL, asym(ffi_resolve("free"))); } else if (u && u->kind == TY_SLICE && p->kind == N_IDENT) { int off = localfind(locals, p->str); int esz = (int)(u->sub ? u->sub->size : 1); ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX)); 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_SI)); ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_DI)); ins1(c, A_CALL, asym(ffi_resolve("free"))); } break; } if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->type == ty_err && n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 && n->list) { /* alloc(value): heap-init a fresh *T with the value's * bytes. Size comes from the value's static type. * `n->lhs->type == ty_err` gate (mirrors assert above) * — check.c only stamps ty_err when no user-scoped * `alloc` shadows the builtin (task #23). * * Result is the graduated `(*T | nomem)` tagged-pointer * ABI (AX=tag, DX=ptr) — task #30. rt_malloc returns 0 * on OOM (rt/alloc.s); we branch on AX, building tag=1 * (nomem, DX=0) on null and tag=0 (success, DX=ptr) * after the value-init stores complete. Callers wrap * with `!` / `?` to consume the union. */ Node *v = n->list; Type *t = v->type; Type *u = (t && t->kind == TY_NAMED) ? t->under : t; Type *def = type_default(t); int sz = def ? (int)def->size : 8; if (sz == 0) sz = 8; char *alloc_ok = mklabel(c, "alloc_ok"); char *alloc_done = mklabel(c, "alloc_done"); ins2(c, A_MOVQ, aimm(sz), areg(D_DI)); ins1(c, A_CALL, asym(ffi_resolve("malloc"))); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, A_JNE, abranch(alloc_ok)); ins2(c, A_MOVQ, aimm(1), areg(D_AX)); ins2(c, A_MOVQ, aimm(0), areg(D_DX)); ins1(c, A_JMP, abranch(alloc_done)); label(c, alloc_ok); ins1(c, A_PUSHQ, areg(D_AX)); /* save ptr */ if (v->kind == N_STRUCTLIT && u && u->kind == TY_STRUCT) { for (Node *f = v->list; f; f = f->next) { u64 foff = 0; int fsz = 8; Type *ftype = NULL; for (Tfield *fl = u->fields; fl; fl = fl->next) { if (strcmp(fl->name, f->str) == 0) { foff = fl->offset; fsz = (int)(fl->type ? fl->type->size : 8); ftype = fl->type; break; } } cgexpr(c, f->lhs, locals); /* AX or (AX,BX) or X0 */ int f_isf32 = 0; if (fld_isfloat(ftype, &f_isf32)) { int mov = f_isf32 ? A_MOVSS : A_MOVSD; ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX)); ins2(c, mov, areg(D_X0), amem(D_BX, (int)foff)); continue; } /* str IS []u8: cgexpr leaves (AX=ptr, BX=len, * CX=cap). Route the heap base through DX so all * three survive — CX now holds cap, BX holds len * (#1/Phase 3). */ Type *fu = (ftype && ftype->kind == TY_NAMED) ? ftype->under : ftype; if (fu && fu->kind == TY_STR) { ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_DX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, (int)foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, (int)foff + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, (int)foff + 16)); continue; } ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX)); int op = A_MOVQ; if (fsz == 1) op = A_MOVB; else if (fsz == 4) op = A_MOVL; ins2(c, op, areg(D_AX), amem(D_BX, (int)foff)); } } else { cgexpr(c, v, locals); /* AX = value */ ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX)); int op = A_MOVQ; if (sz == 1) op = A_MOVB; else if (sz == 4) op = A_MOVL; ins2(c, op, areg(D_AX), amem(D_BX, 0)); } ins1(c, A_POPQ, areg(D_DX)); /* DX = success ptr */ ins2(c, A_MOVQ, aimm(0), areg(D_AX)); label(c, alloc_done); break; } if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str && strcmp(n->lhs->str, "append") == 0 && n->list && n->list->next) { /* append(s, v) lowering — Hare's rt::ensure model. * ; AX = value * ; PUSHQ AX ; save * ; ADDQ $1, sn_off+8(BP) ; s.len += 1 * ; LEAQ sn_off(BP), DI ; arg1 = &s * ; MOVQ esz, SI ; arg2 = membsz * ; CALL rt_ensure(SB) ; may realloc s.ptr * ; MOVQ sn_off+8(BP), CX ; CX = new len * ; SUBQ $1, CX ; slot index * ; [IMULQ esz, CX] ; byte offset (esz>1) * ; MOVQ sn_off(BP), BX ; reread s.ptr * ; ADDQ CX, BX ; BX = target * ; POPQ AX ; v * ; MOV* AX, (BX) ; store (MOVB / MOVQ) * * Spread form `append(s, items...)` runs this same body * in a counted loop over items. */ Node *sn = n->list; Type *st = sn->type; Type *su = (st && st->kind == TY_NAMED) ? st->under : st; int esz = (su && su->sub) ? (int)su->sub->size : 1; Type *esub = su ? su->sub : NULL; int sn_off = (sn->kind == N_IDENT) ? localfind(locals, sn->str) : 0; int store_op = fldstoreop(esub, esz); for (Node *vn = sn->next; vn; vn = vn->next) { if (vn->kind == N_SPREAD && vn->lhs && vn->lhs->kind == N_IDENT) { int it_off = localfind(locals, vn->lhs->str); 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)); 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)); /* AX = items.ptr[i] */ 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)); ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8)); ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI)); ins2(c, A_MOVQ, aimm(esz), areg(D_SI)); ins1(c, A_CALL, masym(c, "rt_ensure")); ins2(c, A_MOVQ, amem(D_BP, sn_off + 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_BP, sn_off), areg(D_BX)); ins2(c, A_ADDQ, areg(D_CX), areg(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; } cgexpr(c, vn, locals); /* val → AX */ ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8)); ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI)); ins2(c, A_MOVQ, aimm(esz), areg(D_SI)); ins1(c, A_CALL, masym(c, "rt_ensure")); ins2(c, A_MOVQ, amem(D_BP, sn_off + 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_BP, sn_off), areg(D_BX)); ins2(c, A_ADDQ, areg(D_CX), areg(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 = (callee_t && callee_t->kind == TY_NAMED) ? callee_t->under : 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 = (vst && vst->kind == TY_NAMED) ? vst->under : 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) { 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; } } } /* 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}; { Tparam *p = callee_params; for (int i = 0; i < argcount; i++) { if (p == NULL) break; Type *at = args[i] ? args[i]->type : NULL; int psz = tagged_arg_size(p->type); if (psz > 0) { Type *pu = (p->type && p->type->kind == TY_NAMED) ? p->type->under : p->type; Type *au = (at && at->kind == TY_NAMED) ? at->under : at; int same = (pu == au) || type_eq(p->type, at); if (!same) { widen[i] = 1; widen_sz[i] = psz; widen_param[i] = p->type; } } p = p->next; } } /* 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 (!widen[i] && node_isslice(args[i]) && args[i]->kind == N_IDENT) { int off = localfind(locals, args[i]->str); /* push cap, len, ptr (top) so pops give ptr,len,cap */ ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); continue; } if (!widen[i] && args[i]->kind == N_SLICE) { Node *base = args[i]->lhs; Node *lo = args[i]->rhs; Node *hi = args[i]->cond; Type *bt = base ? base->type : NULL; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; /* N_IDENT-gated: non-ident bases stay esz=1 * (unscaled), byte-id with wwstage which has no * tnode there (rule 10) -- #76 residual, non- * ident cluster #74. */ int esz = (base && base->kind == N_IDENT && 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 { cgexpr(c, base, locals); } ins1(c, A_PUSHQ, areg(D_AX)); /* hi (default base length) → push */ if (hi) cgexpr(c, hi, locals); else if (bu && bu->kind == TY_ARRAY) cgexpr_int(c, (long long)bu->alen); else if (base->kind == N_IDENT && bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) { int boff = localfind(locals, base->str); int isglobal = (boff == 0) && let_islet(base->str); if (isglobal) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_CX)); ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_AX)); } else { ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX)); } } else { cgexpr_int(c, 0); } ins1(c, A_PUSHQ, areg(D_AX)); /* lo (default 0) → AX */ if (lo) cgexpr(c, lo, locals); else cgexpr_int(c, 0); ins1(c, A_POPQ, areg(D_BX)); /* hi */ ins1(c, A_POPQ, areg(D_CX)); /* base */ /* len = hi - lo (DX) */ ins2(c, A_MOVQ, areg(D_BX), areg(D_DX)); ins2(c, A_SUBQ, areg(D_AX), areg(D_DX)); /* ptr = base + lo*esz (#76; ensure.ha:30 * membsz-unit). BX=lo*esz; AX=lo PRESERVED * for cap. BX (dead hi) reloaded by cap below. */ if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_BX)); ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); ins2(c, A_ADDQ, areg(D_BX), areg(D_CX)); } else { ins2(c, A_ADDQ, areg(D_AX), areg(D_CX)); } /* push cap, len, ptr (top). cap = base_cap - lo * (#20); AX=lo, BX free. */ if (cg_base_cap(c, base, bu, locals, D_BX)) { ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); ins1(c, A_PUSHQ, areg(D_BX)); /* cap */ } else { ins1(c, A_PUSHQ, areg(D_DX)); /* cap = len */ } ins1(c, A_PUSHQ, areg(D_DX)); /* len */ ins1(c, A_PUSHQ, areg(D_CX)); /* ptr */ continue; } if (!widen[i] && node_isstructarg(args[i]) && args[i]->kind == N_IDENT) { /* load qword(s) directly from the struct's slot */ int off = localfind(locals, args[i]->str); int sz = struct_arg_size(args[i]->type); if (sz > 8) { ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); } ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); continue; } if (!widen[i] && node_istaggedarg(args[i]) && args[i]->kind == N_IDENT) { /* Tagged-union: push each 8B word from the slot. * High word goes first so the popper drains them * in low→high order into the arg-register class. */ int off = localfind(locals, args[i]->str); int sz = tagged_arg_size(args[i]->type); int nwords = sz / 8; for (int k = nwords - 1; k >= 0; k--) { ins2(c, A_MOVQ, amem(D_BP, off + k*8), areg(D_AX)); ins1(c, A_PUSHQ, areg(D_AX)); } continue; } if (widen[i]) { /* Concrete → tagged-union widening at the call * site. Mirrors the let/assign/return widening: * lay out the value in the parameter's slot * shape, then push high→low so pop drains tag * first. * * Branches by source shape: * - nullable (sz==8): pointer IS the disc. * - str: tag@+0, ptr@+8, len@+16. * - struct ident: copy struct words then * prepend tag, zero-pad to slot size. * - struct literal: materialise via a stack * scratch slot — store each field at its * struct-relative offset (with the +8 tag * shift), zero-fill, then push from slot. * - tagged source: load src slot words, remap * the tag word via cg_widen_tag_remap, pad * to wider dst slot, push. * - scalar: tag@+0, value@+8, optional pad. */ cg_widen_tagged_push(c, &locals, widen_param[i], args[i], widen_sz[i]); continue; } cgexpr(c, args[i], locals); Type *tuparg_push = node_tuplearg(args[i]); if (node_isfloat(args[i])) { /* f32 spills 4B (MOVSS), f64 8B (MOVSD): the SysV * float class drives the width per ref/qbe * amd64/emit.c:524 (slot-copy single→movss). The * slot is 8B either way; the pop reads the same * width back. #143. */ int fmov = op_for(args[i], A_MOVSD, A_MOVSS); ins2(c, A_SUBQ, aimm(8), areg(D_SP)); ins2(c, fmov, areg(D_X0), amem(D_SP, 0)); } else if (node_isstr(args[i])) { /* str IS []u8: cgexpr left (AX=ptr, BX=len, * CX=cap). Push the triple, same as slice * (#1/Phase 3). */ ins1(c, A_PUSHQ, areg(D_CX)); /* cap */ ins1(c, A_PUSHQ, areg(D_BX)); /* len */ ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */ } else if (node_isslice(args[i])) { /* Slice-typed arg without a fast path above * (e.g. `s: []u8` cast): cgexpr left * (AX=ptr, BX=len, CX=cap). Push the triple. */ ins1(c, A_PUSHQ, areg(D_CX)); /* cap */ ins1(c, A_PUSHQ, areg(D_BX)); /* len */ ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */ } else if (node_istaggedarg(args[i])) { /* Tagged-return ABI: AX=tag, DX=val0, * CX=val1, R8=val2. Push high-to-low so pop * drains tag first (into arg-reg[0]), then * values into arg-reg[1..]. Nullable (sz=8): * AX holds the pointer, no value-word * registers — push just AX. */ 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) { Type *pu = (p->type && p->type->kind == TY_NAMED) ? p->type->under : p->type; int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR); if (fld_isfloat(p->type, &ef32)) sstot++; else gptot += tuple_ebytes(wide); /* 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 += wide ? (int)pu->size : 8; } /* 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) { Type *pu = (p->type && p->type->kind == TY_NAMED) ? p->type->under : p->type; int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR); int isflt = fld_isfloat(p->type, &ef32); tuple_store(c, p->type, wide, gpcur, ssecur, cg_tupargscr + eoff); if (isflt) ssecur++; else gpcur += tuple_ebytes(wide); eoff += wide ? (int)pu->size : 8; } 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 (widen[i]) { /* Pop widened tagged slot into arg-register * class — sized by the parameter's tagged slot, * not the arg's static type. */ int eb = widen_sz[i] / 8; for (int k = 0; k < eb; k++) { if (ii < 6) ins1(c, A_POPQ, areg(sysv_argregs[ii++])); else stackslots++; } continue; } if (node_isfloat(args[i])) { if (fi < 8) { /* Reload the spilled f32/f64 at its class * width — MOVSS for f32, MOVSD for f64 — * matching the push above (#143). */ int fmov = op_for(args[i], A_MOVSD, A_MOVSS); ins2(c, fmov, amem(D_SP, 0), areg(sysv_fargregs[fi])); ins2(c, A_ADDQ, aimm(8), areg(D_SP)); fi++; } else { stackslots++; /* leave on stack */ } } else if (node_isstr(args[i])) { /* str IS []u8: 3-word arg, same as slice (#1/Phase 3). */ for (int k = 0; k < 3; k++) { if (ii < 6) ins1(c, A_POPQ, areg(sysv_argregs[ii++])); else stackslots++; } } else if (node_isslice(args[i])) { for (int k = 0; k < 3; k++) { if (ii < 6) ins1(c, A_POPQ, areg(sysv_argregs[ii++])); else stackslots++; } } else if (node_isstructarg(args[i])) { int sclass[2], snb; /* SSE-drain only for an ident arg: the struct push * stages raw slot words for an N_IDENT only (non- * ident struct args are a pre-existing >8B-push gap, * out of scope). Gating here keeps cstage byte-id * with wwstage, whose type lookup is ident-keyed. */ if (args[i]->kind == N_IDENT && (snb = struct_float_class(args[i]->type, sclass)) > 0) { /* #165: float-bearing struct arg — drain by * SysV eightbyte class: a lone-f64 eightbyte * MOVSD off (SP) into the next XMM (X0..X7), a * pure-INT eightbyte POPQ into the next INTEGER * arg reg (DI/SI/..). The struct-ident push * staged raw words (class-independent); only the * drain differs. Gated to qualifying floats; * all-int + f32-packed keep the all-GP pop * below. Reg overflow loud-stops (rule 7), the * partial-spill stitch out of scope (#163 twin). */ for (int e = 0; e < snb; e++) { if (sclass[e]) { if (fi >= 8) fatal("float struct arg " "eightbyte overflows SSE " "arg regs (X0..X7); stitch " "out of scope, see #165"); ins2(c, A_MOVSD, amem(D_SP, 0), areg(sysv_fargregs[fi])); ins2(c, A_ADDQ, aimm(8), areg(D_SP)); fi++; } else { if (ii >= 6) fatal("float struct arg " "eightbyte overflows " "integer arg regs (DI/SI/" "DX/CX/R8/R9); stitch out " "of scope, see #165"); ins1(c, A_POPQ, areg(sysv_argregs[ii++])); } } } else { int sz = struct_arg_size(args[i]->type); int eb = (sz > 8) ? 2 : 1; for (int k = 0; k < eb; k++) { if (ii < 6) ins1(c, A_POPQ, areg(sysv_argregs[ii++])); else stackslots++; } } } else if (node_istaggedarg(args[i])) { int sz = tagged_arg_size(args[i]->type); int eb = sz / 8; for (int k = 0; k < eb; k++) { if (ii < 6) ins1(c, A_POPQ, areg(sysv_argregs[ii++])); else stackslots++; } } else if ((tu = node_tuplearg(args[i])) != NULL) { /* #163: drain the tuple's staged words (pushed * slot+0 first) into the SysV arg cursor by SysV * class — a float MOVSD/MOVSS off (SP) into the * next XMM (X0..X7), everything else POPQ into the * next INTEGER arg reg (DI/SI/..); a slice/str its * 3-word {ptr,len,cap}. Reg overflow loud-stops * (rule 7): the partial-spill stitch is out of * scope (twin of #164's cap). */ int ef32; for (Tparam *p = tu->params; p; p = p->next) { Type *pu = (p->type && p->type->kind == TY_NAMED) ? p->type->under : p->type; int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR); 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_ebytes(wide); 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++; } } } /* sret hidden first-arg (#23): load &dest into RDI AFTER * all user-arg pops have finished — the pop loop started * its int-arg cursor at 1, so RDI was never written. * * Forwarding (task #9 follow-up): when outer's `return f();` * forwards through an sret callee, source RDI from outer's * saved @sretarg — inner writes directly into outer's * caller-prealloc dest. No temporary in outer's frame. * Post-#15 @sretscr is skipped entirely on the forwarding * branch (no allocation, no frame growth) — earlier scan- * lockstep reservation is gone. */ if (sret_call_sz > 0) { if (cg_sret_forward) { ins2(c, A_MOVQ, amem(D_BP, cg_sret_arg_off), areg(D_DI)); cg_sret_forward = 0; } else if (sret_dest_sym != NULL) { /* #220: sret into a GLOBAL — RDI = &g(SB). */ ins2(c, A_LEAQ, masym(c, sret_dest_sym), areg(D_DI)); } else { ins2(c, A_LEAQ, amem(D_BP, sret_call_off), areg(D_DI)); } } /* SysV: variadic callees require AL to hold the count of * XMM regs used in the variable portion. We don't pass * floats yet, so AL=0 covers every case we emit. */ if (cu && cu->kind == TY_FN && cu->variadic) ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); if (n->lhs->kind == N_IDENT) { /* If the callee names a local variable holding a * function pointer, load it and call indirect. Without * this check `CALL fp(SB)` is emitted as if `fp` were * a global symbol — the linker rightly fails. Hare / * QBE handles this by treating any non-`$symbol` value * as an indirect target; we get the same effect by * reusing the cgexpr path. */ int loff = localfind(locals, n->lhs->str); if (loff != 0) { ins2(c, A_MOVQ, amem(D_BP, loff), areg(D_AX)); ins1(c, A_CALL, areg(D_AX)); } else { /* Bare `f()` — same-module by ww's resolver * rules. Hint with c->cur_mod so the right * fn wins when the leaf collides with another * module's exported same-leaf fn. */ ins1(c, A_CALL, mafn(c, n->lhs->str, c->cur_mod)); } } else if (n->lhs->kind == N_DOT && n->lhs->lhs && n->lhs->lhs->kind == N_IDENT) { /* `m.fn()` is module-qualified iff the ident has no * concrete type (SK_USE leaves it ty_err). For a real * type — typically a struct or *struct holding a * function pointer — we load the field and indirect. */ Type *bt = n->lhs->lhs->type; if (bt == NULL || bt == ty_err) { /* `m.fn()` — explicit module qualifier. Pass * the bareword as the hint so cross-module * same-leaf exports resolve correctly. */ ins1(c, A_CALL, mafn(c, n->lhs->str, n->lhs->lhs->str)); } else { cgexpr(c, n->lhs, locals); /* AX = fn ptr */ ins1(c, A_CALL, areg(D_AX)); } } else { cgexpr(c, n->lhs, locals); ins1(c, A_CALL, areg(D_AX)); } /* SysV: caller cleans stack args. */ if (stackslots > 0) ins2(c, A_ADDQ, aimm(stackslots * 8), areg(D_SP)); /* str IS []u8: callee returns AX=ptr, BX=len, CX=cap — * same as a slice, no receive-side shuffle (#1/Phase 3). */ break; } case N_MATCH: { /* match on a tagged-union scrutinee. Read tag and value from * the slot. Dispatch by the resolved variant index of each * case's type pattern — case order is independent of variant * declaration order. A case with no pattern (`case =>`) is a * default arm; its body always runs. * * Slot layout: [+0]=tag, [+8]=value0, [+16]=value1. The third * word is only meaningful for variants whose payload is >8B * (e.g. str). Bindings sized 16B (str) copy two words. * * Nullable folded `(*T | void)`: slot is one 8B word holding * the pointer; null IS the void variant. Discriminator = * value, not a separate tag. */ Node *s = n->lhs; Type *st = s ? s->type : NULL; Type *su = (st && st->kind == TY_NAMED) ? st->under : st; int is_nullable = type_isnullable(st); int slot_size = (su && su->kind == TY_TAGGED) ? (int)su->size : 16; int sl_off; if (s->kind == N_IDENT) { sl_off = localfind(locals, s->str); } else if (s->kind == N_DOT && s->lhs && s->lhs->kind == N_IDENT && s->lhs->type) { /* `match (p.field)` — point sl_off at the field's slot * inside the parent struct. The slot layout (tag at +0, * value words at +8/+16) is contiguous within the struct, * so no spill is needed. */ Type *bt = s->lhs->type; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; Tfield *f = NULL; if (bu && bu->kind == TY_STRUCT) { for (Tfield *fl = bu->fields; fl; fl = fl->next) { if (strcmp(fl->name, s->str) == 0) { f = fl; break; } } } if (f) { int boff = localfind(locals, s->lhs->str); sl_off = boff + (int)f->offset; } else { /* fall back to spill — `match (h.e)` where * h is *struct. cgexpr → cgdot now leaves the * AX=tag, DX=val0, CX=val1[, R8=val2] shape * (task #28), so spill all words the variant * may carry. Pre-#28 only AX landed and the * dispatch fired on a stale slot. */ sl_off = localoff(c, &locals, "@match_spill", slot_size, cg_frame); 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); cgexpr(c, s, locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0)); if (!is_nullable) { ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, sl_off + 8)); if (slot_size > 16) ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, sl_off + 16)); if (slot_size > 24) ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, sl_off + 24)); } } char *end = mklabel(c, "match_end"); /* Push the end label as the yield target for arm bodies. */ if (nyields < YIELD_MAX) { yield_target[nyields++] = end; } for (Node *cs = n->list; cs; cs = cs->next) { char *next = mklabel(c, "match_next"); /* Per-arm scope: save the locals head, restore it * after the body runs. Mirrors check.c's saved/restore * around cstmt — the case bind (and any lets inside * the arm) shouldn't leak past the arm, where a * matching outer name would otherwise resolve to the * shadow instead of the original. */ Local *arm_locals_saved = locals; if (cs->type != NULL) { int tag = cg_tag_for_variant(su, cs->type); ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX)); if (is_nullable) { /* discriminator = pointer-vs-null. * *T variant: skip if ptr == 0. * void variant: skip if ptr != 0. */ int ptr_tag = nullable_ptr_tag(su); int want_ptr = (tag == ptr_tag); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); if (want_ptr) ins1(c, A_JE, abranch(next)); else ins1(c, A_JNE, abranch(next)); } else if (cs->list != NULL) { /* Multi-pattern `case T1 | T2 | ... =>`: * if the tag matches any of the alts, * jump to body; otherwise to the next * case. */ char *body = mklabel(c, "match_body"); ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX)); ins1(c, A_JE, abranch(body)); for (Node *alt = cs->list; alt; alt = alt->next) { int atag = cg_tag_for_variant( su, alt->type); ins2(c, A_CMPQ, aimm(atag < 0 ? 0 : atag), areg(D_AX)); ins1(c, A_JE, abranch(body)); } ins1(c, A_JMP, abranch(next)); label(c, body); } else { ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX)); ins1(c, A_JNE, abranch(next)); } } if (cs->str && cs->str[0] && cs->type) { Type *bt = cs->type; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; if (is_nullable) { /* Bind *T or void to a local. The * value IS the slot's pointer word; no * payload to copy. void binding is * unusable (size 0), so only emit for * the *T variant. local_alloc (not * localoff): the bind must NEVER reuse * an outer same-named slot. */ if (bu && bu->kind == TY_PTR) { int voff = local_alloc(c, &locals, cs->str, 8, cg_frame); ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, voff)); } } else { /* #43: route through Type.size SSoT rather * than re-asserting 16/24 for str/slice. */ int bsz = 8; if (bu) bsz = (int)bu->size; if (bsz <= 0) bsz = 8; /* local_alloc to dodge name-collision * dedup — a 16B str bind shadowing an * 8B outer would otherwise overflow * into the saved BP. */ int voff = local_alloc(c, &locals, cs->str, bsz, cg_frame); int nwords = (bsz + 7) / 8; for (int w = 0; w < nwords; w++) { ins2(c, A_MOVQ, amem(D_BP, sl_off + 8 + 8*w), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, voff + 8*w)); } } } cgstmt(c, cs->body, &locals, cg_frame); locals = arm_locals_saved; ins1(c, A_JMP, abranch(end)); label(c, next); } label(c, end); if (nyields > 0) nyields--; break; } case N_TRYPROP: { /* Evaluate tagged value: AX=tag, DX=value0[, CX=value1]. * If the tag matches an error variant, propagate as the * current function's return (with a tag remap to the * enclosing fn's variant order). On success, unwrap to the * success-variant ABI: ≤8B values in AX; str values in * (AX=ptr, BX=len). * * Nullable: AX is the pointer; *T variant is the success * (any non-null), void variant is the error (null). The * enclosing fn's null encoding is the same — RET with AX=0 * if propagating; otherwise leave AX as-is on success. */ cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; Type *r = cg_ret_type; if (r && r->kind == TY_NAMED) r = r->under; if (u && u->kind == TY_TAGGED && u->nullable) { char *cont = mklabel(c, "tryprop_ok"); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, A_JNE, abranch(cont)); /* null = error: propagate. AX already 0; matches * the enclosing nullable encoding if it has one. */ ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); ins1(c, A_POPQ, areg(D_BP)); ins0(c, A_RET); label(c, cont); break; } int s_tag = cg_tagged_success_tag(u); Type *succ_t = NULL; if (u && u->kind == TY_TAGGED) { int i = 0; for (Tparam *p = u->params; p; p = p->next, i++) if (i == s_tag) { succ_t = p->type; break; } } int success_is_str = type_isstr(succ_t); char *cont = mklabel(c, "tryprop_ok"); ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX)); ins1(c, A_JE, abranch(cont)); if (u && r && r->kind == TY_TAGGED && u->params) { /* Same-shape unions remap every variant to itself, so * the loop emits no JMPs. Skip propret entirely then — * wwstage doesn't emit a dead label either (CLAUDE.md * rule 10, task #18). */ char *propret = NULL; int i = 0; for (Tparam *p = u->params; p; p = p->next, i++) { if (!cg_variant_is_error(u, i)) continue; int j = cg_tag_for_variant(r, p->type); if (j < 0) j = 0; if (j == i) continue; char *skip = mklabel(c, "tryprop_skip"); ins2(c, A_CMPQ, aimm(i), areg(D_AX)); ins1(c, A_JNE, abranch(skip)); ins2(c, A_MOVQ, aimm(j), areg(D_AX)); if (propret == NULL) propret = mklabel(c, "tryprop_ret"); ins1(c, A_JMP, abranch(propret)); label(c, skip); } if (propret != NULL) label(c, propret); } ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); ins1(c, A_POPQ, areg(D_BP)); ins0(c, A_RET); label(c, cont); 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). */ cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; if (u && u->kind == TY_TAGGED && u->nullable) { char *cont = mklabel(c, "tryunw_ok"); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, A_JNE, abranch(cont)); ins2(c, A_MOVQ, aimm(1), areg(D_DI)); ins2(c, A_MOVQ, aimm(60), areg(D_AX)); ins0(c, A_SYSCALL); label(c, cont); break; } int s_tag = cg_tagged_success_tag(u); Type *succ_t = NULL; if (u && u->kind == TY_TAGGED) { int i = 0; for (Tparam *p = u->params; p; p = p->next, i++) if (i == s_tag) { succ_t = p->type; break; } } int success_is_str = type_isstr(succ_t); char *cont = mklabel(c, "tryunw_ok"); ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX)); ins1(c, A_JE, abranch(cont)); ins2(c, A_MOVQ, aimm(1), areg(D_DI)); ins2(c, A_MOVQ, aimm(60), areg(D_AX)); ins0(c, A_SYSCALL); label(c, cont); 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. */ cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; Type *vt = n->rhs ? n->rhs->type : NULL; char *ne = mklabel(c, "is_ne"); char *done = mklabel(c, "is_done"); if (u && u->kind == TY_TAGGED && u->nullable) { int tag = cg_tag_for_variant(u, vt); int ptr_tag = nullable_ptr_tag(u); int want_ptr = (tag == ptr_tag); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); if (want_ptr) ins1(c, A_JE, abranch(ne)); else ins1(c, A_JNE, abranch(ne)); } else { int tag = cg_tag_for_variant(u, vt); ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX)); ins1(c, A_JNE, abranch(ne)); } ins2(c, A_MOVQ, aimm(1), areg(D_AX)); ins1(c, A_JMP, abranch(done)); label(c, ne); ins2(c, A_MOVQ, aimm(0), areg(D_AX)); label(c, done); break; } case N_TYPEASSERT: { /* `e as T` — abort if tag != T's variant index; otherwise * unwrap value to T's ABI: scalar/ptr variants land in AX; * 16B str variants in AX:BX. * * We need both tag *and* value words. For an N_IDENT local * the value lives at slot+8/+16 — cgexpr's single-MOVQ path * does not load it. Mirror match's pattern: resolve a slot * offset (existing local or a fresh @asrt_spill) and index * out tag/value from memory. * * Nullable: the slot's word IS the pointer. *T variant * asserts non-null; void variant asserts null. The value * left in AX after the check is the pointer itself. */ Node *s = n->lhs; Type *st = s ? s->type : NULL; Type *u = (st && st->kind == TY_NAMED) ? st->under : st; Type *vt = n->type; /* Enum ↔ integer: reinterpret-only. The value already lives * in AX after evaluating the LHS; no tag/unwrap needed. */ { Type *vu = (vt && vt->kind == TY_NAMED) ? vt->under : vt; if ((u && u->kind == TY_ENUM) || (vu && vu->kind == TY_ENUM)) { cgexpr(c, s, locals); break; } } int slot_size = (u && u->kind == TY_TAGGED) ? (int)u->size : 16; 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); cgexpr(c, s, locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0)); if (!(u && u->kind == TY_TAGGED && u->nullable)) { ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, sl_off + 8)); if (slot_size > 16) ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, sl_off + 16)); } } char *ok = mklabel(c, "asrt_ok"); if (u && u->kind == TY_TAGGED && u->nullable) { int tag = cg_tag_for_variant(u, vt); int ptr_tag = nullable_ptr_tag(u); int want_ptr = (tag == ptr_tag); ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX)); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); if (want_ptr) ins1(c, A_JNE, abranch(ok)); else ins1(c, A_JE, abranch(ok)); ins2(c, A_MOVQ, aimm(1), areg(D_DI)); ins2(c, A_MOVQ, aimm(60), areg(D_AX)); ins0(c, A_SYSCALL); label(c, ok); /* AX already holds the pointer (or 0 for void * variant, where the result type has size 0 and * no consumer reads it). */ break; } int tag = cg_tag_for_variant(u, vt); ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX)); ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX)); ins1(c, A_JE, abranch(ok)); ins2(c, A_MOVQ, aimm(1), areg(D_DI)); ins2(c, A_MOVQ, aimm(60), areg(D_AX)); ins0(c, A_SYSCALL); label(c, ok); ins2(c, A_MOVQ, amem(D_BP, sl_off + 8), areg(D_AX)); if (type_isstr(vt)) ins2(c, A_MOVQ, amem(D_BP, sl_off + 16), areg(D_BX)); break; } case N_CAST: { int from_f = node_isfloat(n->lhs); int to_f = cg_isfloat(n->type); int from_f32 = node_isf32(n->lhs); int to_f32 = type_isf32(n->type); cgexpr(c, n->lhs, locals); /* AX or X0 depending */ if (from_f && !to_f) { int op = from_f32 ? A_CVTTSS2SI : A_CVTTSD2SI; ins2(c, op, areg(D_X0), areg(D_AX)); } else if (!from_f && to_f) { int op = to_f32 ? A_CVTSI2SS : A_CVTSI2SD; ins2(c, op, areg(D_AX), areg(D_X0)); } else if (from_f && to_f && from_f32 != to_f32) { int op = to_f32 ? A_CVTSD2SS : A_CVTSS2SD; ins2(c, op, areg(D_X0), areg(D_X0)); } /* str → []u8 (or any []T): cgexpr left (AX=ptr, BX=len). * Slice register convention is (AX=ptr, BX=len, CX=cap); * synthesise cap = len so downstream arg-push / let-init * paths see the canonical triple. Without this, the cap * register stays whatever cgexpr happened to leave there * and the receiver reads a stale value. */ { Type *tt = n->type; Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt; Type *ft = n->lhs ? n->lhs->type : NULL; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; if (tu && tu->kind == TY_SLICE && fu && fu->kind == TY_STR) { ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); } } /* Narrowing integer cast: clamp AX to the target width so * downstream 64-bit ops see a value within the declared * range. Hare semantics: `expr: T` truncates to T's bit * width (mod 2^n). Without this, `(big_u64): u32` left the * upper 32 bits intact and CMPQ/DIVQ misread the value. * * Unsigned targets use MOVL/ANDQ to clear the high bits. * Signed-narrow targets (i8/i16/i32) sign-extend via * MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates; * this is what lets `(0xFF80i64): i8` compare equal to * -128i64 after a widening read-back. Symmetric on signed * vs unsigned: both branches gate on `type_isint(tu) && * size<8`, then dispatch on type_isunsigned(tu). The * recursion through TY_ENUM in type_isunsigned (task #5) * is what lets an enum-aliased narrow (`type myflag = i8`) * pick up the right MOVS*Q. Wwstage's cgcast keys off the * resolved type-name through the same shape. TY_RUNE is * unsigned (Unicode scalar) and lands on the MOVL path. */ if (!from_f && !to_f && n->type) { Type *tt = n->type; Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt; /* Identity-width identity-sign cast is a no-op at the * machine-int level: src and dst share both width and * signedness, so the natural slot/load already carries * the right canonical 64-bit shape and the narrow-clamp * is dead. Replaces b5632b1's single-site `!dst_is_enum` * gate (task #25) which mirrored wwstage's N_TENUM * lacuna; the lacuna is fixed there too, so this gate * stays symmetric across both stages (#33). Source side * uses `castsrcprim` (a structural walk matching * wwstage's exprprimresolved exactly), NOT n->lhs->type * — cstage's checker has richer type info than wwstage * can derive without a checker, and the asymmetric * coverage broke 995_self_rebuild's byte-id. The cost * is that some casts (`.len: i32`, N_BIN result, call * return, match-bound payload) still emit a redundant * clamp on both stages; closing those gaps is a * sibling task that extends wwstage's type inference. * Incidentally fixes a silent miscompile #25's * dst-kind-only skip left in place: u32→enum-u8 (and * similar narrow-to-enum casts) was suppressing the * clamp, so the upper bits of the source value leaked * through register-chained downstream uses. Caveat: * removing the defensive MOVL exposes any upstream * cgen path that leaves garbage in upper RAX when * producing a sub-word value — the contract is * producers leave the value in canonical width- * extended form. */ int src_w = 0, src_unsignd = 0; castsrcprim(n->lhs, &src_w, &src_unsignd); int dst_w = (tu && type_isint(tu)) ? (int)tu->size : 0; int identity = dst_w > 0 && src_w == dst_w && src_unsignd == type_isunsigned(tu); if (tu && type_isint(tu) && tu->size > 0 && tu->size < 8 && !identity) { if (type_isunsigned(tu)) { if (tu->size == 4) { ins2(c, A_MOVL, areg(D_AX), areg(D_AX)); } else { u64 mask = ((u64)1 << (tu->size * 8)) - 1; ins2(c, A_ANDQ, aimm((i64)mask), areg(D_AX)); } } else { int op = A_MOVSXD; if (tu->size == 1) op = A_MOVSBQ; else if (tu->size == 2) op = A_MOVSWQ; ins2(c, op, areg(D_AX), areg(D_AX)); } } /* TY_BOOL is size 1 too; clamp to a single byte so * `(u32_val): bool` produces 0 or a low-byte value * instead of leaking the upper bits. type_isint(bool) * is false, so the symmetric narrow above misses it * — this dedicated branch covers the bool case. */ if (tu && tu->kind == TY_BOOL) { ins2(c, A_ANDQ, aimm(0xFF), areg(D_AX)); } } break; } case N_DOT: { /* slice/str pseudo-fields: .ptr (offset 0), .len (8), .cap (16). * Arrays don't carry a header; .len uses the static size and * .ptr is the address of the first element. */ /* `(*p).f` read retarget: parser produces n->lhs = N_UN(STAR, * IDENT(p)) with type T (post-deref struct). Pull the inner * IDENT in as dot_lhs so bt resolves to *T and the pointer- * auto-deref branch below fires (mirror of the N_ASSIGN * N_DOT lhs retarget). v1 scope: N_IDENT inner only; * (*expr).f follow-up task pending. Branches that gate on * `n->lhs->kind == N_DOT/N_INDEX/...` keep checking the raw * n->lhs since (*p) isn't either of those shapes. */ Node *dot_lhs = n->lhs; if (dot_lhs && dot_lhs->kind == N_UN && dot_lhs->op == TK_STAR && dot_lhs->lhs && dot_lhs->lhs->kind == N_IDENT) dot_lhs = dot_lhs->lhs; Type *bt = dot_lhs ? dot_lhs->type : NULL; /* type_chase_named (#22): `type b = a; type a = struct;` stacks * two TY_NAMED layers; single peel left `u` still TY_NAMED, * missing the TY_STRUCT field-walk gate below and collapsing * `s.field` to a base-only MOVQ read (offset 0 instead of * the field's declared offset). */ Type *u = type_chase_named(bt); /* Module-qualified value reference: `mod.name`. The checker * leaves SK_USE idents untyped (NULL/ty_err); detect that and * look up the leaf in the flat (driver-concatenated) sym/def * maps the same way a bare N_IDENT would. */ if (n->lhs && n->lhs->kind == N_IDENT && (bt == NULL || bt == ty_err)) { Type *t = n->type; Type *tu = (t && t->kind == TY_NAMED) ? t->under : t; if (tu && tu->kind == TY_FN) { /* `mod.fn` address-of via N_DOT — pass the * module bareword as the disambiguation hint. */ ins2(c, A_LEAQ, mafn(c, n->str, n->lhs->str), areg(D_AX)); break; } { /* Same-module-first walk using n->lhs->str as * the explicit module hint (sister of wwstage * deflookuprhsmod). The TY_FN branch above * already uses n->lhs->str via mafn for the * cross-module qualifier disambiguation; this * walk mirrors that polarity so `alpha.MSG` * from a third module beats a head-of-sdefs * beta.MSG collision (#11, sister of #4c). */ Sdef *s; for (s = sdefs; s; s = s->next) { if (strcmp(s->name, n->str) != 0) continue; if (sdef_mod_match_hint(s, n->lhs->str)) break; } if (s == NULL) { for (s = sdefs; s; s = s->next) if (strcmp(s->name, n->str) == 0) break; } if (s != NULL) { const char *lab = intern_strlit(c, s->bytes, s->len); ins2(c, A_LEAQ, asym(lab), areg(D_AX)); ins2(c, A_MOVQ, aimm((long long)s->len), areg(D_BX)); goto dot_done; } } /* Same gating as the bare-ident catch-all: lets route * through localloadop (their slot can be the target of * a narrow deref-store via `&letname: *iN`); defs and * unresolved symbols stay on MOVQ so wwstage's defent- * registry-without-tnode shape agrees byte-for-byte. */ int mqop = let_islet(n->str) ? localloadop(n->type) : A_MOVQ; /* #229: thread the DOTTED module (the `m` in `m.x`) into * the value mangle, not cur_mod — masym's non-preferring * leaf lookup mis-mangled `aa.v` onto another module's * same-leaf global (read the WRONG global). The TY_FN * branch above already uses n->lhs->str via mafn. */ if (mqop == A_MOVQ) { ins2(c, A_MOVQ, mahint(c, n->str, n->lhs->str), areg(D_AX)); } else { ins2(c, A_LEAQ, mahint(c, n->str, n->lhs->str), areg(D_CX)); ins2(c, mqop, amem(D_CX, 0), areg(D_AX)); } goto dot_done; } /* Chained N_DOT spine through value-struct fields. Handles any * depth `root.f0.f1.…leaf` where every intermediate field is a * value struct, plus the slice/str pseudo-field tail (`s.buf.len`) * where the innermost field is a slice/str header. Walks inward * collecting (parent_struct, field_name); reverses to sum field * offsets; emits one load at (base + total_off). Placed BEFORE * the slice/str pseudo-field branch so its else-arm (cgexpr lhs * + shuffle BX→AX) doesn't mis-handle `b.buf.len` — cgexpr on a * value-struct→slice chain only loads .ptr into AX, leaving BX * stale. Sibling of the pointer-chain branch further down. */ if (n->lhs && n->lhs->kind == N_DOT) { Type *lt0 = n->lhs->type; Type *lu0 = (lt0 && lt0->kind == TY_NAMED) ? lt0->under : 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 = (pt && pt->kind == TY_NAMED) ? pt->under : 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 = (pu->sub->kind == TY_NAMED) ? pu->sub->under : 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; } Type *fu = (leaf_type && leaf_type->kind == TY_NAMED) ? leaf_type->under : leaf_type; if (fu && fu->kind == TY_STR) { ins2(c, A_MOVQ, amem(base_reg, base_disp + total_off + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(base_reg, base_disp + total_off + 8), areg(D_BX)); goto dot_done; } if (fu && fu->kind == TY_SLICE) { /* Slice leaf: load all three header * words into (AX=ptr, BX=len, CX=cap) * so the value follows the canonical * slice-rhs convention. base_reg may * be CX (global / `*T` root); load * .cap LAST so the base survives the * earlier reads. */ ins2(c, A_MOVQ, amem(base_reg, base_disp + total_off + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(base_reg, base_disp + total_off + 8), areg(D_BX)); ins2(c, A_MOVQ, amem(base_reg, base_disp + total_off + 16), areg(D_CX)); goto dot_done; } int g_isf32 = 0; if (fld_isfloat(leaf_type, &g_isf32)) { int mov = g_isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, amem(base_reg, base_disp + total_off), areg(D_X0)); goto dot_done; } int fsz = (int)(leaf_type ? leaf_type->size : 8); int op = fldloadop(leaf_type, fsz); ins2(c, op, amem(base_reg, base_disp + total_off), areg(D_AX)); goto dot_done; } } } } } int lenfld = (n->str && strcmp(n->str, "len") == 0); int capfld = (n->str && strcmp(n->str, "cap") == 0); int ptrfld = (n->str && strcmp(n->str, "ptr") == 0); if (u && (u->kind == TY_SLICE || u->kind == TY_STR) && (lenfld || capfld || ptrfld)) { if (n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); if (off == 0) { /* Not a local — could be `def NAME: str * = "lit"`. Sdef-backed strs aren't laid * out in memory; emit .ptr/.len from the * literal directly, mirroring the bare * N_IDENT branch above. Without this we'd * load BP+8 (return-address slot) as the * "len". */ { /* Same-module-first walk: two * same-leaf `def MSG: str = ...` * across modules would otherwise * fold the wrong strlit's length / * label into `MSG.len` / `MSG.ptr` * (sister of wwstage deflookuprhs * #4c). */ Sdef *s; for (s = sdefs; s; s = s->next) { if (strcmp(s->name, n->lhs->str) != 0) continue; if (sdef_mod_match(c, s)) break; } if (s == NULL) { for (s = sdefs; s; s = s->next) if (strcmp(s->name, n->lhs->str) == 0) break; } if (s != NULL) { if (ptrfld) { const char *lab = intern_strlit(c, s->bytes, s->len); ins2(c, A_LEAQ, asym(lab), areg(D_AX)); } else { ins2(c, A_MOVQ, aimm((long long) s->len), areg(D_AX)); } goto dot_done; } } /* Top-level str/slice `let` — load * the field through &name(SB). Same * pattern as the bare N_IDENT load. */ if (let_islet(n->lhs->str)) { int delta = ptrfld ? 0 : (lenfld ? 8 : 16); ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX)); ins2(c, A_MOVQ, amem(D_CX, delta), areg(D_AX)); goto dot_done; } } int delta = ptrfld ? 0 : (lenfld ? 8 : 16); ins2(c, A_MOVQ, amem(D_BP, off + delta), areg(D_AX)); } else { /* Evaluate the str/slice expression — leaves * (AX=ptr, BX=len) for str. .ptr returns AX, * .len shuffles BX→AX. */ cgexpr(c, n->lhs, locals); if (lenfld) ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); } break; } if (u && u->kind == TY_ARRAY && n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); if (lenfld) { ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX)); break; } if (ptrfld) { ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX)); break; } } /* tuple positional field access: t.0, t.1, ... */ 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) { if (tp->type) foff += (int)tp->type->size; tp = tp->next; idx--; } if (tp != NULL) { int fsz = (int)(tp->type ? tp->type->size : 8); Type *fu = (tp->type && tp->type->kind == TY_NAMED) ? tp->type->under : tp->type; int op = fldloadop(tp->type, fsz); int off = localfind(locals, n->lhs->str); /* 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(D_BP, off + foff), areg(D_X0)); break; } /* str IS []u8 — load (ptr, len, cap) into * (AX, BX, CX), the canonical slice-header ABI, * so chains like `t.1.len` propagate through the * slice-rhs convention (#1/Phase 3 collapse). * UNLIKE the field arms there is no slice-element * sibling here, so the triple is hand-authored; * base is BP (frame, not a target reg) so the * canonical ptr/len/cap order has no clobber risk. */ if (fu && fu->kind == TY_STR) { ins2(c, A_MOVQ, amem(D_BP, off + foff + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off + foff + 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_BP, off + foff + 16), areg(D_CX)); break; } ins2(c, op, amem(D_BP, off + foff), areg(D_AX)); } break; } /* real struct field: load at struct_base + field_off. * Base is either a local frame slot (off(BP)) or a top- * level let global (&name(SB) into CX); we resolve which * once and then share the field-walk code. */ if (u && u->kind == TY_STRUCT && n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); int is_global = 0; int base_reg = D_BP; int base_disp = off; /* #129 A.2: struct-typed defs now also resolve via * LEAQ name(SB) (paralleling lets). Pre-A.2 the * `def_isstructdef` arm fell through to the default * BP-relative path with off=0, emitting `MOV (BP),` * which reads the stack frame's first slot instead * of the def's data section. */ if (off == 0 && (let_islet(n->lhs->str) || def_isstructdef(n->lhs->str))) { ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX)); is_global = 1; base_reg = D_CX; base_disp = 0; } for (Tfield *f = u->fields; f; f = f->next) { if (strcmp(f->name, n->str) != 0) continue; /* tagged-union field: load AX=tag, DX=val0, * CX=val1, R8=val2 (CX last, since for globals * CX is also the base addr; load R8 before CX * so the base address survives the +24 read). * Mirrors the tagged-return ABI so the let-init * / match dispatch shapes just work. The val2 * word fires for slice-variant tagged-unions * (slot = 8 tag + 24 slice header = 32B). */ Type *tag_fu = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if (tag_fu && tag_fu->kind == TY_TAGGED) { int fo = base_disp + (int)f->offset; 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. */ Type *str_fu = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if ((str_fu && str_fu->kind == TY_SLICE) || type_isstr(f->type)) { ins2(c, A_MOVQ, amem(base_reg, base_disp + (int)f->offset + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(base_reg, base_disp + (int)f->offset + 8), areg(D_BX)); ins2(c, A_MOVQ, amem(base_reg, base_disp + (int)f->offset + 16), areg(D_CX)); break; } /* f64/f32 field: route through X0 (MOVSD/MOVSS). * Loading via MOVQ AX would put the bits in the * integer reg, and any downstream consumer that * reads X0 (arg pass, return, arithmetic) would see * stale data. */ int e_isf32 = 0; if (fld_isfloat(f->type, &e_isf32)) { int mov = e_isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, amem(base_reg, base_disp + (int)f->offset), areg(D_X0)); break; } int fsz = (int)(f->type ? f->type->size : 8); int op = fldloadop(f->type, fsz); ins2(c, op, amem(base_reg, base_disp + (int)f->offset), areg(D_AX)); break; } break; } /* pointer-to-slice/str field: deref and read pseudo-field. * Used by helpers like rt_appendu8(s: *[]u8, v: u8). dot_lhs * gates the N_IDENT check so `(*p).len` (parser N_UN(STAR, * IDENT)) emits the same load as `p.len` after the case-top * retarget. */ if (u && u->kind == TY_PTR && u->sub) { Type *inner = type_chase_named(u->sub); if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR) && (lenfld || capfld || ptrfld) && dot_lhs && dot_lhs->kind == N_IDENT) { int off = localfind(locals, dot_lhs->str); ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); int delta = ptrfld ? 0 : (lenfld ? 8 : 16); ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_AX)); break; } } /* pointer-to-struct field: deref and load. Common pattern: * fn move(p: *point) ... { p.x += dx; ... } * dot_lhs gates this branch so both `p.f` (n->lhs is IDENT) * and `(*p).f` (n->lhs is N_UN(STAR, IDENT), retargeted to * inner IDENT at case-top) emit the same load sequence. * * type_chase_named (#22): `type b = a;` inside the pointer * (`*b`) leaves a single peel still at TY_NAMED. Bites the * strings.tokenize wrapper shape — caller signature * `next_token(s: *strings.tokenizer)` where strings.tokenizer * aliases bytes.tokenizer. */ if (u && u->kind == TY_PTR && u->sub) { Type *inner = type_chase_named(u->sub); if (inner && inner->kind == TY_STRUCT && dot_lhs && dot_lhs->kind == N_IDENT) { int off = localfind(locals, dot_lhs->str); ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); for (Tfield *f = inner->fields; f; f = f->next) { if (strcmp(f->name, n->str) != 0) continue; /* tagged-union field through *struct: BX * already holds the *struct pointer. Load * the four payload regs from (BX, f->offset) * — BX is not a target (AX/DX/CX/R8), so * load order is harmless. Mirrors the direct- * struct branch above so consumers see the * same tagged-return register shape * regardless of pointer rooting. Pre-#28 fell * through to fldloadop and dropped the * payload words. */ Type *ptag_fu = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if (ptag_fu && ptag_fu->kind == TY_TAGGED) { int fo = (int)f->offset; 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). */ Type *str_fu = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if ((str_fu && str_fu->kind == TY_SLICE) || type_isstr(f->type)) { ins2(c, A_MOVQ, amem(D_BX, (int)f->offset + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_BX, (int)f->offset + 16), areg(D_CX)); ins2(c, A_MOVQ, amem(D_BX, (int)f->offset + 8), areg(D_BX)); break; } /* f64/f32 field via *struct: load into X0. * BX already holds the struct pointer from * the MOVQ amem(D_BP,off) above. */ int f_isf32 = 0; if (fld_isfloat(f->type, &f_isf32)) { int mov = f_isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, amem(D_BX, (int)f->offset), areg(D_X0)); break; } int fsz = (int)(f->type ? f->type->size : 8); int op = fldloadop(f->type, fsz); ins2(c, op, amem(D_BX, (int)f->offset), areg(D_AX)); break; } break; } } /* Chained N_DOT through a *struct field. cgexpr lhs leaves * AX = the inner *struct pointer; load the requested field * with a single MOVQ. Without this, returning `o.p.val` * silently leaves AX = o.p (the pointer) and the outer * cast/use sees the pointer instead of the dereferenced * field. (Surfaced building ww-w6l.) */ if (n->lhs->kind == N_DOT) { Type *lt = n->lhs->type; Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; if (lu && lu->kind == TY_PTR && lu->sub) { Type *inner = lu->sub; if (inner->kind == TY_NAMED) inner = inner->under; if (inner && inner->kind == TY_STRUCT) { for (Tfield *f = inner->fields; f; f = f->next) { if (strcmp(f->name, n->str) != 0) continue; cgexpr(c, n->lhs, locals); /* AX = inner ptr */ Type *ft = f->type; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; /* str IS []u8 — same 3-word {ptr,len,cap} as a * slice field: load (ptr, len, cap) into * (AX, BX, CX). AX is the *struct base, so * load .ptr (which targets AX) LAST. str folds * onto the slice arm (#1/Phase 3 collapse). */ if ((fu && fu->kind == TY_SLICE) || type_isstr(ft)) { ins2(c, A_MOVQ, amem(D_AX, (int)f->offset + 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_AX, (int)f->offset + 16), areg(D_CX)); ins2(c, A_MOVQ, amem(D_AX, (int)f->offset + 0), areg(D_AX)); goto dot_done; } /* f64/f32 chained field: read into X0. */ int g_isf32 = 0; if (fld_isfloat(ft, &g_isf32)) { int mov = g_isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, amem(D_AX, (int)f->offset), areg(D_X0)); goto dot_done; } int fsz = (int)(ft ? ft->size : 8); int op = fldloadop(ft, fsz); ins2(c, op, amem(D_AX, (int)f->offset), areg(D_AX)); goto dot_done; } } } } /* `arr[i].field` — element-then-field through a `[N]*S` / * `[N]S` (and slice/`*[N]S`) base. One branch covers both * shapes: compute `&arr[i]` into BX, then either deref * (`*Struct` element) or move-to-AX (value `Struct` element), * so the leaf load is `(field.offset)(AX)` either way. * Bypasses cgindex deliberately — cgindex's final MOVQ * would truncate a value-struct element to 8 bytes. Mirrors * selfhost/cmd/wcc/cgenexpr.ww's cgdot N_INDEX-lhs branch. */ if (n->lhs && n->lhs->kind == N_INDEX && n->lhs->lhs && n->lhs->lhs->kind == N_IDENT) { Node *idxbase = n->lhs->lhs; Type *elemt = n->lhs->type; Type *elemu = (elemt && elemt->kind == TY_NAMED) ? elemt->under : elemt; Type *struct_t = NULL; int viaptr = 0; if (elemu && elemu->kind == TY_PTR) { Type *inner = elemu->sub; if (inner && inner->kind == TY_NAMED) inner = inner->under; if (inner && inner->kind == TY_STRUCT) { struct_t = inner; viaptr = 1; } } else if (elemu && elemu->kind == TY_STRUCT) { struct_t = elemu; } if (struct_t) { Tfield *f = NULL; for (Tfield *fl = struct_t->fields; fl; fl = fl->next) if (strcmp(fl->name, n->str) == 0) { f = fl; break; } Type *bt = idxbase->type; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; int is_arr = bu && bu->kind == TY_ARRAY; int is_sl = bu && bu->kind == TY_SLICE; int is_ptr = bu && bu->kind == TY_PTR; int off = localfind(locals, idxbase->str); if (f != NULL && (is_arr || is_sl || is_ptr) && off != 0) { 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 (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 = (ft && ft->kind == TY_NAMED) ? ft->under : ft; if (fu && (fu->kind == TY_STR || fu->kind == TY_SLICE)) { /* str/slice: the 3-word {ptr,len,cap} * slice header (#1). AX holds the * element base, so load .ptr (which * targets AX) LAST. Matches the * caseB *struct slice arm and * cgslicehdr(D_AX). */ ins2(c, A_MOVQ, amem(D_AX, foff + 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_AX, foff + 16), areg(D_CX)); ins2(c, A_MOVQ, amem(D_AX, foff + 0), areg(D_AX)); goto dot_done; } int g_isf32 = 0; if (fld_isfloat(ft, &g_isf32)) { int mov = g_isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, amem(D_AX, foff), areg(D_X0)); goto dot_done; } int fsz = (int)(ft ? ft->size : 8); int op = fldloadop(ft, fsz); ins2(c, op, amem(D_AX, foff), areg(D_AX)); goto dot_done; } } } /* Nested module-qualified field where the chain didn't fold to * a known shape (typical when w6c runs on a single file with * `use mod;` but no driver concatenation — the body's enum / * struct hasn't been seen). Emit `MOVQ (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. */ if (n->lhs && n->lhs->kind == N_DOT && n->str) { ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); break; } /* fall through to base evaluation; result placeholder */ cgexpr(c, n->lhs, locals); 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 = (bt && bt->kind == TY_NAMED) ? bt->under : bt; Type *eff = idx_eff(bt); int esz = 1; if (eff && eff->sub) esz = (int)eff->sub->size; Type *esub = eff ? eff->sub : NULL; Type *esubu = (esub && esub->kind == TY_NAMED) ? esub->under : esub; int elem_tagged = esubu && esubu->kind == TY_TAGGED; if (n->lhs->kind == N_IDENT && u) { int off = localfind(locals, n->lhs->str); /* #129 A.3: array-typed defs now have DATA storage; the * LEAQ name(SB) base-load must fire for them too, not * just let_islet. Parallel to A.2's def_isstructdef * gate at the N_DOT direct-struct-ident arm. */ int isglobal = (off == 0) && (let_islet(n->lhs->str) || def_isarraydef(n->lhs->str)); cgexpr(c, n->rhs, locals); /* idx → AX */ if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } /* base address into BX. Top-level array → LEAQ * name(SB); top-level ptr → MOVQ name(SB) (the symbol * holds the pointer); locals route off BP. */ if (isglobal && u->kind == TY_ARRAY) { ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_BX)); } else if (isglobal) { ins2(c, A_MOVQ, masym(c, n->lhs->str), areg(D_BX)); } else if (u->kind == TY_ARRAY) { ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); } else { /* slice/str/ptr: ptr field is at off+0 */ ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); } ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); /* #156 (PREREQ-1 read-half): element is itself an array * ([N][M]T → element [M]T). This index yields the sub- * array's ADDRESS, not a loaded value — the outer index * adds its own offset and only the final scalar element * dereferences. Sister of #135 (N_DOT-base-on-[N]T-field * needs ADDRESS). BX holds base+idx*esz; move it to AX (the * value-result reg). Gated on TY_ARRAY element so 1D arrays * are byte-identical (no 2D consumer pre-#156). */ if (esubu && esubu->kind == TY_ARRAY) { ins2(c, A_MOVQ, areg(D_BX), areg(D_AX)); break; } /* str/slice element: load the full (ptr, len, cap) header * into (AX, BX, CX) — both are 24B since #1, so the cap * word must survive. Kind-gate on type_isstr||type_isslice, * never size==24: a >16B struct is 24B+ too but takes the * struct-copy path, not this 3-word header load (#10). * Base is BX. */ if (u->sub && (type_isstr(u->sub) || type_isslice(u->sub))) { 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; if (ssz > 24) ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8)); if (ssz > 16) ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX)); if (ssz > 8) ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_DX)); ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); break; } /* float element → MOVSS/MOVSD into X0: the consumer's * ADDSD/MOVSD spill machinery already expects X0, but the * integer fldloadop below would leave it in AX and the SSE * side reads stale (#119). Float-ness from esub — the same * type the esz above reads. Twin of the scalar-float global * load at cgen.c:2014. */ if (type_isfloat(esub)) { int op = type_isf32(esub) ? A_MOVSS : A_MOVSD; ins2(c, op, amem(D_BX, 0), areg(D_X0)); break; } int load_op = fldloadop(esub, esz); ins2(c, load_op, amem(D_BX, 0), areg(D_AX)); break; } /* Fallback: evaluate base (treat as plain pointer) and * dereference at base+idx. Pick the load opcode by element * size — `b.data[i]` on a *u8 must read 1 byte, not 8. * * Scale the index in a register before pushing, because * IMULQ on a memory operand isn't currently encoded by w6a * (modrm bits use mod=3 register form). * * #135: N_DOT base on a `[N]T`-typed field needs the field's * ADDRESS, not its value. cgexpr on N_DOT would auto-deref and * load the field's 8-byte value as if it were a pointer — the * symmetric READ-side of the LHS bug at the cgassign sites. * cg_dotbase_addr emits the address inline. */ cgexpr(c, n->rhs, locals); if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } ins1(c, A_PUSHQ, areg(D_AX)); if (!cg_dotbase_addr(c, n->lhs, D_AX, locals)) cgexpr(c, n->lhs, locals); ins1(c, A_POPQ, areg(D_BX)); ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); /* #156 (PREREQ-1 read-half): array element → AX already holds * &elem (base+idx*esz); a nested index adds its offset and * dereferences. See the N_IDENT arm above. */ if (esubu && esubu->kind == TY_ARRAY) break; /* str/slice element via fallback base: load the full (ptr, len, * cap) header into (AX, BX, CX). Kind-gate on type_isstr|| * type_isslice, never size==24 (see Site A). Base is AX. */ if (u && u->sub && (type_isstr(u->sub) || type_isslice(u->sub))) { 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; ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); if (ssz > 24) ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8)); if (ssz > 16) ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX)); if (ssz > 8) ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_DX)); ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); break; } /* float element via fallback base → X0 (see Site A, #119). The * base address is in AX; MOVSS/MOVSD reads the element into X0. */ if (type_isfloat(esub)) { int op = type_isf32(esub) ? A_MOVSS : A_MOVSD; ins2(c, op, amem(D_AX, 0), areg(D_X0)); break; } { int load_op = fldloadop(esub, esz); ins2(c, load_op, amem(D_AX, 0), areg(D_AX)); } break; } case N_SLICE: { /* base[lo:hi] as a slice value. Leaves the triple in * (AX=base+lo*esz, BX=hi-lo, CX=base_cap-lo) so callers can * route to a slice slot, return, or arg with the same ABI. * cap is the storage remaining to the base's end (#20, * Go/Hare-identical), via cg_base_cap. ptr advances by BYTES * (lo*esz, #76; ref/hare/rt/ensure.ha:30 membsz-unit); esz * from the type table, mirroring the N_INDEX idiom. */ Node *base = n->lhs; Node *lo = n->rhs; Node *hi = n->cond; Type *bt = base ? base->type : NULL; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; /* N_IDENT-gated: non-ident bases stay esz=1 (unscaled), * byte-id with wwstage which has no tnode there to resolve * esz (rule 10) -- #76 residual, non-ident cluster #74. */ int esz = (base && base->kind == N_IDENT && 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) { cgexpr(c, base, locals); } ins1(c, A_PUSHQ, areg(D_AX)); if (lo) cgexpr(c, lo, locals); else cgexpr_int(c, 0); ins1(c, A_PUSHQ, areg(D_AX)); if (hi) { cgexpr(c, hi, locals); } else if (bu && bu->kind == TY_ARRAY) { cgexpr_int(c, (long long)bu->alen); } else if (base && base->kind == N_IDENT && bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) { int boff = localfind(locals, base->str); int isglobal = (boff == 0) && let_islet(base->str); if (isglobal) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_CX)); ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_AX)); } else { ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX)); } } else { cgexpr_int(c, 0); } ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); ins1(c, A_POPQ, areg(D_CX)); ins1(c, A_POPQ, areg(D_AX)); /* ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit). * DX=lo*esz; CX=lo PRESERVED for len + cap (#20). */ if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_DX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_DX)); ins2(c, A_ADDQ, areg(D_DX), areg(D_AX)); } else { ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); } ins2(c, A_SUBQ, areg(D_CX), areg(D_BX)); /* cap = base_cap - lo (#20); CX=lo, BX=len here. */ if (cg_base_cap(c, base, bu, locals, D_DX)) { ins2(c, A_SUBQ, areg(D_CX), areg(D_DX)); ins2(c, A_MOVQ, areg(D_DX), areg(D_CX)); } else { ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); } break; } default: cgexpr_int(c, 0); break; } } static void cgstmt(Cg *c, Node *n, Local **locals, int *frame) { if (n == NULL) return; switch (n->kind) { case N_BLOCK: { /* Save/restore the locals head across the block (post-#27). * Inner-scope `let` bindings prepend to *locals via localoff; * without this restore, the prepended stubs leak into sibling * and ancestor scopes, and localfind (head-first) returns the * inner binding's offset for an identifier that semantically * belongs to the outer scope. The frame is left grown — slot * lifetimes don't overlap with later siblings observably (the * popped stubs' offsets are no longer reachable by name), but * we don't reclaim the frame bytes; that's the conservative * choice C compilers make for simple lowering. * * cgfn iterates fn->body->list directly to bypass this * save/restore at the function's outermost block — defers * (and the implicit-return epilogue) need locals intact. */ Local *saved = *locals; for (Node *s = n->list; s; s = s->next) cgstmt(c, s, locals, frame); *locals = saved; break; } case N_EXPRSTMT: cgexpr(c, n->lhs, *locals); break; case N_LET: { Type *lt = n->type; /* type_chase_named (#22): a chain `type a = struct{...}; * type b = a;` stacks two TY_NAMED layers. A single peel * left `lu` pointing at the inner alias (still TY_NAMED), * collapsed the struct/slice/tagged sizing arms to the 8B * fallback, and the slot under-allocated the local. */ Type *lu = type_chase_named(lt); /* #43: every composite kind already has its byte size cached in * lu->size; route through it instead of re-asserting 16/24 for * str/slice and re-reading for the others. */ int sz = 8; if (lu && (lu->kind == TY_ARRAY || lu->kind == TY_SLICE || lu->kind == TY_STR || lu->kind == TY_STRUCT || lu->kind == TY_TUPLE || lu->kind == TY_TAGGED)) sz = (int)lu->size; int off = localoff(c, locals, n->str, sz, frame); int isf = cg_isfloat(lt); int isf32 = type_isf32(lt); /* alloc([], n) initialiser for a slice local: allocate * n*esize bytes, build the {ptr, 0, n} header in the slot. * Element size comes from the declared slice type. * * Task #30 graduated the builtin to `([]T | nomem)`. The let * declares a bare `[]T`, so the canonical idiom wraps in `!` * (abort on OOM) or `?` (propagate nomem to the enclosing * fn's tagged return). Task #45 extends the shortcut to also * match N_TRYPROP and emit the propret pattern. */ { Node *call = NULL; int via_tryunw = 0; int via_tryprop = 0; if (n->rhs && n->rhs->kind == N_TRYUNW && n->rhs->lhs && n->rhs->lhs->kind == N_CALL) { call = n->rhs->lhs; via_tryunw = 1; } else if (n->rhs && n->rhs->kind == N_TRYPROP && n->rhs->lhs && n->rhs->lhs->kind == N_CALL) { call = n->rhs->lhs; via_tryprop = 1; } if (call && lu && lu->kind == TY_SLICE && sz == 24 && call->lhs && call->lhs->kind == N_IDENT && strcmp(call->lhs->str, "alloc") == 0 && call->list && call->list->kind == N_ARRLIT && call->list->list == NULL && call->list->next && call->list->next->next == NULL) { Node *count = call->list->next; int esz = (lu->sub) ? (int)lu->sub->size : 1; cgexpr(c, count, *locals); /* AX = n */ ins1(c, A_PUSHQ, areg(D_AX)); /* save count */ if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_BX)); ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); } ins2(c, A_MOVQ, areg(D_AX), areg(D_DI)); ins1(c, A_CALL, asym(ffi_resolve("malloc"))); if (via_tryunw) { char *ok = mklabel(c, "tryunw_ok"); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, A_JNE, abranch(ok)); ins2(c, A_MOVQ, aimm(1), areg(D_DI)); ins2(c, A_MOVQ, aimm(60), areg(D_AX)); ins0(c, A_SYSCALL); label(c, ok); } else if (via_tryprop) { /* #45: null = nomem; propagate to the * enclosing fn's tagged return. AX = tag * of nomem variant in cg_ret_type; epilogue * RETs to caller. */ char *ok = mklabel(c, "tryprop_ok"); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, A_JNE, abranch(ok)); Type *r = cg_ret_type; if (r && r->kind == TY_NAMED) r = r->under; int nidx = cg_tag_for_variant(r, ty_nomem); if (nidx < 0) nidx = 1; ins2(c, A_MOVQ, aimm(nidx), areg(D_AX)); ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); ins1(c, A_POPQ, areg(D_BP)); ins0(c, A_RET); label(c, ok); } ins1(c, A_POPQ, areg(D_BX)); /* count */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); break; } } /* str IS []u8: cgexpr produces (AX=ptr, BX=len, CX=cap); * store all three, same as the slice initialiser below. * #43 gate via ty_str->size already tracks the 24B bump * (#1/Phase 3). */ if (n->rhs && type_isstr(lt) && sz == (int)ty_str->size) { cgexpr(c, n->rhs, *locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); break; } /* Tuple initialiser from a function call (#105 / #164/#107), * 16B (two eightbytes) or 32B (scalar/float + slice/str header). * 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. */ if (n->rhs && lu && lu->kind == TY_TUPLE && (sz == 16 || sz == 32)) { cgexpr(c, n->rhs, *locals); int gpcur = 0, ssecur = 0, eoff = 0, ef32; for (Tparam *p = lu->params; p; p = p->next) { Type *pu = (p->type && p->type->kind == TY_NAMED) ? p->type->under : p->type; int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR); int isflt = fld_isfloat(p->type, &ef32); tuple_store(c, p->type, wide, gpcur, ssecur, off + eoff); if (isflt) ssecur++; else gpcur += tuple_ebytes(wide); eoff += wide ? (int)pu->size : 8; } break; } /* 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). */ if (n->rhs && lu && lu->kind == TY_TAGGED) { cg_widen_tagged_store(c, locals, lu, n->rhs, D_BP, off, sz); break; } /* Every slice initialiser routes here — fn-return, slice * ident, slice param, and sub-slice `buf[lo:hi]`. cgexpr * leaves (AX=ptr, BX=len, CX=cap); store all three. The * sub-slice case once had a vestigial inline builder that * duplicated cgexpr's N_SLICE path and mishandled global * bases; dropping it aligns cstage onto wwstage's shared * store path (find-4). Runs after the alloc specialisation * above so that keeps its direct {ptr,0,n} shape. */ if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24) { cgexpr(c, n->rhs, *locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); break; } /* struct literal initialiser: field-by-field store via the * shared cg_structlit_fill_bp helper. The literal carries * op == TK_ELLIPSIS when the source ends in `..., ...` — * helper zero-fills the slot first so unmentioned fields * read as 0. Nested struct-typed structlit field values * recurse into the helper at the correct offset instead of * landing AX = first-qword via cgexpr (#17 silent zero). */ if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu && lu->kind == TY_STRUCT) { cg_structlit_fill_bp(c, locals, lu, n->rhs, off); break; } /* sret receive (#23): plain TY_STRUCT >24B. 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. */ if (n->rhs && n->rhs->kind == N_CALL && lu && lu->kind == TY_STRUCT && sz > 24) { cg_sret_dest_off = off; cgexpr(c, n->rhs, *locals); cg_sret_dest_off = 0; break; } /* Whole-struct receive for sizes <=24B (call-result rhs). * Counterpart of #4's cgreturn ABI: cgexpr leaves * AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23], zero- * padded to 24B by the producer. * * ASYMMETRY (do NOT mirror the sender): producer emits three * uniform MOVQs into a zero-padded 24B scratch slot; the * receiver must write only `sz` bytes — MOVQ for full 8B * chunks plus a sized tail (MOVL/MOVW/MOVB) by the *declared* * struct size. Otherwise a trailing 1..7-byte chunk would * overrun into the next local slot. * * Tail chunks in {3,5,6,7} (would need shift-and-store from * the register) are unreachable under WW struct alignment * rules (field aligns force size%align==0); the guard * excludes them so they fall through to the existing scalar * path rather than emit a stomping MOVQ tail. Sizes >24B also * fall through (sret deferred, same constraint as #4). */ /* #171a: float-bearing struct RECEIVE (the return twin of * #165's param recv). cgexpr leaves each float eightbyte in * its SSE return reg (X0,X1 = tuple_sse_seq) and each INT * eightbyte in its INTEGER return reg (AX,DX = tuple_rseq), * on INDEPENDENT cursors per SysV (ref/qbe/amd64/sysv.c retr) * — so a float is read from the next XMM regardless of its * positional eightbyte (struct{f64,i32}: e0←X0, e1←AX). A * qualifying struct's size is maxalign-rounded to a multiple * of 8 (an f64 forces align 8), so every eightbyte is a full * word — the #169 sized tail (MOVL/MOVB) is unreachable here. * struct_float_class gates to qualifying structs; all-int + * f32 fall through to the GP recv below (byte-id / #171b). */ if (n->rhs && n->rhs->kind == N_CALL && lu && lu->kind == TY_STRUCT) { int sclass[2], snb; if ((snb = struct_float_class(lu, sclass)) > 0) { cgexpr(c, n->rhs, *locals); int gpcur = 0, ssecur = 0; for (int e = 0; e < snb; e++) { if (sclass[e]) { ins2(c, A_MOVSD, areg(tuple_sse_seq[ssecur]), amem(D_BP, off + e * 8)); ssecur++; } else { ins2(c, A_MOVQ, areg(tuple_rseq[gpcur]), amem(D_BP, off + e * 8)); gpcur++; } } break; } } if (n->rhs && n->rhs->kind == N_CALL && lu && lu->kind == TY_STRUCT && sz <= 24 && (sz % 8 == 0 || sz % 8 == 1 || sz % 8 == 2 || sz % 8 == 4)) { cgexpr(c, n->rhs, *locals); int regs[3] = { D_AX, D_DX, D_CX }; int full = sz / 8; int tail = sz % 8; for (int i = 0; i < full; i++) ins2(c, A_MOVQ, areg(regs[i]), amem(D_BP, off + i * 8)); if (tail > 0) { int op = (tail == 4) ? A_MOVL : (tail == 2) ? A_MOVW : A_MOVB; ins2(c, op, areg(regs[full]), amem(D_BP, off + full * 8)); } break; } /* array literal initialiser: `let xs: [N]T = [a, b, c];`. * Walk elements in declaration order, store each at off + i*esz * using the right width for the element type. The trailing * `...` repeat marker (an N_FIELD with str=="...") fills the * remaining slots with the last value. * * str element (16B = ptr+len) needs both halves stored: cgexpr * leaves a str as (AX=ptr, BX=len), and a single MOVQ from AX * would leave .len as whatever the stack held — silent * miscompile. The per-element store branches on TY_STR before * falling through to the scalar MOVB/MOVL/MOVQ path. Slice * (24B) and struct/tuple/tagged element arrays land in the * same multi-word-store gap; the read side (cgindex of an * [N]slice) has its own truncating-to-ptr bug, so slice * end-to-end repros surface sub-issues — both halves of the * slice-element fix are tracked as a follow-up. */ if (n->rhs && n->rhs->kind == N_ARRLIT && lu && lu->kind == TY_ARRAY) { Type *esub = lu->sub; int esz = esub ? (int)esub->size : 1; int is_str_el = type_isstr(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 = n->rhs->list; e; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) { repeat = 1; break; } cgexpr(c, e, *locals); int base = off + idx * esz; if (is_str_el) { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, base)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, base + 8)); } 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 && 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) { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, base)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, base + 8)); } 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++; } } break; } /* Struct ident copy: `let p2: T = p1;` where T is a struct * >8B and rhs is a local ident. Pre-fix the path fell * through to the `sz == 8` test (false) and emitted * nothing — the dst slot read whatever the stack held, * presenting as a silent zero copy on a fresh frame. * Per-qword MOVQ from src slot to dst slot, with a sized * tail (MOVL/MOVB) for natural sizes that aren't * 8-aligned (e.g. `struct { i32, i32, i32 }` is 12B). * Mirrors the slot-to-slot copy in cg_widen_tagged_store * for a TY_STRUCT payload (Task #32). */ if (n->rhs && n->rhs->kind == N_IDENT && lu && lu->kind == TY_STRUCT && sz > 8) { Local *src_l = NULL; for (Local *l = *locals; l; l = l->next) if (strcmp(l->name, n->rhs->str) == 0) { src_l = l; break; } if (src_l) { int soff = src_l->off; int k = 0; while (k + 8 <= sz) { ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + k)); k += 8; } if (k < sz) { int tail = sz - k; int lop = (tail == 4) ? A_MOVL : (tail == 1) ? A_MOVB : A_MOVQ; ins2(c, lop, amem(D_BP, soff + k), areg(D_AX)); ins2(c, lop, areg(D_AX), amem(D_BP, off + k)); } break; } } if (n->rhs && sz == 8) { cgexpr(c, n->rhs, *locals); if (isf) { int mov = isf32 ? A_MOVSS : A_MOVSD; ins2(c, mov, areg(D_X0), amem(D_BP, off)); } else { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); } } else if (sz == 8) { ins2(c, A_MOVQ, aimm(0), amem(D_BP, off)); } else if (!n->rhs && sz > 8 && lu && lu->kind != TY_ARRAY) { /* `let x: T;` with no rhs for a multi-word composite * (str/slice/tuple/struct/tagged). Zero the slot so * reads after the bare let see {0...} rather than * whatever the stack already held. Arrays keep the * per-index-write contract — leave them uninit. */ ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); int zi = 0; while (zi + 8 <= sz) { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + zi)); zi += 8; } while (zi + 4 <= sz) { ins2(c, A_MOVL, areg(D_AX), amem(D_BP, off + zi)); zi += 4; } while (zi < sz) { ins2(c, A_MOVB, areg(D_AX), amem(D_BP, off + zi)); zi += 1; } } /* arrays left uninitialised — caller writes via index */ break; } case N_RETURN: /* run all defers in reverse before the actual return */ for (int di = ndefers - 1; di >= 0; di--) cgexpr(c, defers[di], *locals); /* If the function returns a tagged union and the value is * one of the variant types, wrap into (tag, value). If rhs * already produces a tagged union (e.g. forwarding another * fallible call), pass it through unchanged. * * Tagged-return ABI: AX=tag, DX=value0[, CX=value1]. CX is * only meaningful when the union has a >8B variant (e.g. * str, where ptr→DX and len→CX). * * Bare `return;` from a tagged-union-returning function: this * is producing the void variant. Emit its tag; the payload is * undefined (void has size 0). */ if (n->lhs == NULL && cg_ret_type) { Type *rt = cg_ret_type; if (rt->kind == TY_NAMED) rt = rt->under; if (rt && rt->kind == TY_TAGGED) { 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) { Type *rt = cg_ret_type; if (rt->kind == TY_NAMED) rt = rt->under; if (rt && rt->kind == TY_TAGGED) { Type *vt = n->lhs->type; Type *vu = (vt && vt->kind == TY_NAMED) ? vt->under : vt; int istagged = vu && vu->kind == TY_TAGGED; int passthrough = istagged && (vu == rt || type_eq(vt, cg_ret_type)); int isstruct = vu && vu->kind == TY_STRUCT; 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) { /* 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): * `return f();` where outer + inner both return the * same >24B struct shape. 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 (rt && rt->kind == TY_STRUCT && (int)rt->size > 24 && 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 && (int)rt->size > 24 && (n->lhs->kind == N_IDENT || n->lhs->kind == N_STRUCTLIT)) { /* 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. */ int sz = 0; 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 { /* 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; } } /* 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); if (rt && rt->kind == TY_STRUCT && rt->size <= 24 && (n->lhs->kind == N_IDENT || n->lhs->kind == N_STRUCTLIT)) { 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 { /* N_IDENT: word-copy rhs slot into * scratch. Whole 8B words via MOVQ; * trailing partial word via MOVL/MOVB * so we read no further than the * source slot's declared size. */ int rhsoff = localfind(*locals, n->lhs->str); int k = 0; while (k + 8 <= sz) { ins2(c, A_MOVQ, amem(D_BP, rhsoff + k), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k)); k += 8; } while (k + 4 <= sz) { ins2(c, A_MOVL, amem(D_BP, rhsoff + k), areg(D_AX)); ins2(c, A_MOVL, areg(D_AX), amem(D_BP, scr + k)); k += 4; } while (k < sz) { ins2(c, A_MOVB, amem(D_BP, rhsoff + k), areg(D_AX)); ins2(c, A_MOVB, areg(D_AX), amem(D_BP, scr + k)); k += 1; } } /* #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; } } 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; for (Node *e = n->lhs->list; e; e = e->next) { if (fld_isfloat(e->type, &f32)) ssecount++; else gptotal += tuple_ebytes(node_isstr(e) || node_isslice(e)); } if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { /* #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). */ int foff = 0; for (Node *e = n->lhs->list; e; e = e->next) { int isflt = fld_isfloat(e->type, &f32); int wide = node_isstr(e) || node_isslice(e); int esz = e->type ? (int)e->type->size : 8; 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)); foff += esz; } 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; } 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; for (Node *e = n->lhs->list; e; e = e->next) { int isflt = fld_isfloat(e->type, &f32); cgexpr(c, e, *locals); /* scalar=AX; slice/str=AX,BX,CX; float=X0 */ if (isflt) { ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0), amem(D_BP, fscr + sseidx * 8)); sseidx++; continue; } ins1(c, A_PUSHQ, areg(D_AX)); /* scalar / .ptr */ if (node_isstr(e) || node_isslice(e)) { ins1(c, A_PUSHQ, areg(D_BX)); /* .len */ ins1(c, A_PUSHQ, areg(D_CX)); /* .cap */ } } for (int i = gptotal - 1; i >= 0; i--) ins1(c, A_POPQ, areg(tuple_rseq[i])); int j = 0; for (Node *e = n->lhs->list; e; e = e->next) { if (!fld_isfloat(e->type, &f32)) continue; ins2(c, f32 ? A_MOVSS : A_MOVSD, amem(D_BP, fscr + j * 8), areg(tuple_sse_seq[j])); j++; } } 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; Type *u = (st && st->kind == TY_NAMED) ? st->under : st; int esz = (u && u->sub) ? (int)u->sub->size : 1; Type *etu = (u && u->sub && u->sub->kind == TY_NAMED) ? u->sub->under : (u ? u->sub : NULL); int destruct = (n->list != NULL); /* allocate temp slots: _i (8B), _len (8B) */ 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); /* allocate per-name slots */ struct { int off, sz, foff; Type *ftype; } binds[8] = {0}; int nbinds = 0; if (destruct) { Tparam *tp = (etu && etu->kind == TY_TUPLE) ? etu->params : NULL; int field_off = 0; for (Node *nm = n->list; nm && nbinds < 8; nm = nm->next) { int fsz = tp && tp->type ? (int)tp->type->size : 8; int slot_sz = (fsz < 8) ? 8 : fsz; binds[nbinds].sz = fsz; binds[nbinds].foff = field_off; binds[nbinds].ftype = tp ? tp->type : NULL; binds[nbinds].off = localoff(c, locals, nm->str, slot_sz, frame); field_off += fsz; nbinds++; if (tp) tp = tp->next; } } else { int slot_sz = (esz < 8) ? 8 : esz; binds[0].off = localoff(c, locals, n->str, slot_sz, frame); binds[0].sz = esz; binds[0].foff = 0; binds[0].ftype = u ? u->sub : NULL; nbinds = 1; } ins2(c, A_MOVQ, aimm(0), amem(D_BP, ioff)); if (u && (u->kind == TY_SLICE || u->kind == TY_STR) && slc->kind == N_IDENT) { int boff = localfind(*locals, slc->str); ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff)); } else if (u && u->kind == TY_ARRAY) { ins2(c, A_MOVQ, aimm((long long)u->alen), amem(D_BP, loff)); } else { cgexpr(c, slc, *locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff)); } char *loop = mklabel(c, "rloop"); char *end = mklabel(c, "rend"); char *natural_exit = end; if (n->els) natural_exit = mklabel(c, "relseloop"); /* #138 (range form): `continue` must run the implicit `i+=1` * post-step before re-testing the loop bound. Pre-fix the * cont-target was `loop` (top), skipping the ADDQ $1, ioff * below the body — infinite loop on the value that triggered * continue. Dedicated `rpost` label; bootstrap-NEUTRAL (no * range-form continue callers in lib/ or selfhost/). */ char *rpost = mklabel(c, "rpost"); if (nloops < LOOP_MAX) { loop_cont[nloops] = rpost; loop_brk[nloops] = end; nloops++; } label(c, loop); ins2(c, A_MOVQ, amem(D_BP, ioff), areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, loff), areg(D_BX)); ins2(c, A_CMPQ, areg(D_BX), areg(D_AX)); ins1(c, A_JGE, abranch(natural_exit)); /* compute element base: s.ptr + i*esz → BX */ if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); } if (slc->kind == N_IDENT && u && u->kind == TY_ARRAY) { int boff = localfind(*locals, slc->str); ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX)); } else if (slc->kind == N_IDENT) { int boff = localfind(*locals, slc->str); ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); } ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); /* load each binding from BX + foff into its slot */ for (int b = 0; b < nbinds; b++) { int op = fldloadop(binds[b].ftype, binds[b].sz); ins2(c, op, amem(D_BX, binds[b].foff), areg(D_AX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, binds[b].off)); } cgstmt(c, n->body, locals, frame); label(c, rpost); ins2(c, A_ADDQ, aimm(1), amem(D_BP, ioff)); ins1(c, A_JMP, abranch(loop)); if (n->els) { label(c, natural_exit); cgstmt(c, n->els, locals, frame); } label(c, end); if (nloops > 0) nloops--; break; } case N_FOR: { char *loop = mklabel(c, "loop"); char *end = mklabel(c, "endloop"); /* `else` runs at normal cond-false exit; break skips it. * Separate the natural exit label from the break target so * the else block sits between them. */ char *natural_exit = end; if (n->els) natural_exit = mklabel(c, "elseloop"); /* #138: `continue` in a 3-clause `for (init; cond; post)` must * run the post-step before re-testing cond. Pre-fix the * continue-target was `loop` (top), which SKIPPED post → state * never advanced → infinite loop. Allocate a dedicated `post` * label only when there IS a post-step (`n->rhs`); else keep * continue → loop-top, byte-id with 1-clause for. */ char *cont_target = loop; if (n->rhs) cont_target = mklabel(c, "post"); if (n->lhs) cgstmt(c, n->lhs, locals, frame); label(c, loop); if (n->cond) { cgexpr(c, n->cond, *locals); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, A_JE, abranch(natural_exit)); } if (nloops < LOOP_MAX) { loop_cont[nloops] = cont_target; loop_brk[nloops] = end; nloops++; } cgstmt(c, n->body, locals, frame); if (nloops > 0) nloops--; if (n->rhs) { label(c, cont_target); cgexpr(c, n->rhs, *locals); } ins1(c, A_JMP, abranch(loop)); if (n->els) { label(c, natural_exit); cgstmt(c, n->els, locals, frame); } label(c, end); break; } case N_MLET: { /* #83 / #164 (#107): positional per-element destructure store. * The rhs left each tuple element in its SysV-class register * (see N_RETURN / harec create_unpack_bindings, ref/harec/src/ * check.c:1354-1416); walk the bindings over the SAME dual * cursor and store each at its own width — a slice/str's 3-word * {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8) into a * header-sized slot (sized from u->size so #1 propagates), a * float from X0/X1 (SSE cursor), a scalar's 1 word from the * INTEGER cursor into an 8B slot. Both rows loud-stop at their * cap. */ cgexpr(c, n->rhs, *locals); int gpcap = TUPLE_GPCAP; int ssecap = TUPLE_SSECAP; int gptotal = 0, ssetotal = 0, lf32; for (Node *l = n->list; l; l = l->next) { Type *t = l->type; Type *u = (t && t->kind == TY_NAMED) ? t->under : t; if (fld_isfloat(t, &lf32)) ssetotal++; else gptotal += tuple_ebytes(u && (u->kind == TY_SLICE || u->kind == TY_STR)); } 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; Type *u = (t && t->kind == TY_NAMED) ? t->under : t; int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR); int isflt = fld_isfloat(t, &lf32); int sz = wide ? (int)u->size : 8; int off = localoff(c, locals, l->str, sz, frame); tuple_store(c, t, wide, gpcur, ssecur, off); if (isflt) ssecur++; else gpcur += tuple_ebytes(wide); } 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. */ cgexpr(c, n->rhs, *locals); Type *rt = n->rhs ? n->rhs->type : NULL; Type *ru = (rt && rt->kind == TY_NAMED) ? rt->under : rt; Tparam *tp0 = (ru && ru->kind == TY_TUPLE) ? ru->params : NULL; int gpcap = TUPLE_GPCAP; int ssecap = TUPLE_SSECAP; int gptotal = 0, ssetotal = 0, mf32; for (Tparam *tp = tp0; tp; tp = tp->next) { Type *u = (tp->type && tp->type->kind == TY_NAMED) ? tp->type->under : tp->type; if (fld_isfloat(tp->type, &mf32)) ssetotal++; else gptotal += tuple_ebytes(u && (u->kind == TY_SLICE || u->kind == TY_STR)); } if (gptotal > gpcap) fatal("tuple destructure exceeds integer register-return " "ABI capacity (%d eightbytes: AX,DX,CX,R8); " "see return-ABI #10", gpcap); if (ssetotal > ssecap) fatal("tuple destructure exceeds SSE register-return ABI " "capacity (%d eightbytes: X0,X1); see return-ABI #10", ssecap); int gpcur = 0, ssecur = 0; Tparam *tp = tp0; for (Node *l = n->list; l; l = l->next) { Type *et = tp ? tp->type : NULL; Type *u = (et && et->kind == TY_NAMED) ? et->under : et; int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR); 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, wide, gpcur, ssecur, off); if (isflt) ssecur++; else gpcur += tuple_ebytes(wide); if (tp) tp = tp->next; } break; } case N_DEFER: if (ndefers < DEFER_MAX) { defers[ndefers++] = n->lhs; } break; case N_YIELD: /* Evaluate the value into AX, then jump to the enclosing * match's end label. str-typed yields land in (AX, BX); * the consumer's let-init or call-arg site reads both. */ if (n->lhs) cgexpr(c, n->lhs, *locals); if (nyields > 0) ins1(c, A_JMP, abranch(yield_target[nyields - 1])); break; case N_BREAK: if (nloops > 0) ins1(c, A_JMP, abranch(loop_brk[nloops - 1])); break; case N_CONTINUE: if (nloops > 0) ins1(c, A_JMP, abranch(loop_cont[nloops - 1])); break; case N_SWITCH: { /* Lower to a chain of compares. Scrutinee lands in a fresh * local slot so case bodies can spill through SP without * losing it. Cases are tried top-to-bottom; a `case:` arm * with no exprs is the default and runs after all named * arms fail. */ char *swname = aprintf(c->a, ".sw_%d", c->labelseq++); int sloff = localoff(c, locals, swname, 8, frame); cgexpr(c, n->lhs, *locals); /* AX = scrutinee */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sloff)); char *end = mklabel(c, "swend"); Node *defcase = NULL; for (Node *cs = n->list; cs; cs = cs->next) { if (cs->list == NULL) { defcase = cs; /* save for last */ continue; } char *body = mklabel(c, "swcase"); char *next = mklabel(c, "swnext"); for (Node *e = cs->list; e; e = e->next) { cgexpr(c, e, *locals); /* AX = case-expr */ ins2(c, A_MOVQ, amem(D_BP, sloff), areg(D_BX)); ins2(c, A_CMPQ, areg(D_BX), areg(D_AX)); ins1(c, A_JE, abranch(body)); } ins1(c, A_JMP, abranch(next)); label(c, body); cgstmt(c, cs->body, locals, frame); ins1(c, A_JMP, abranch(end)); label(c, next); } if (defcase) cgstmt(c, defcase->body, locals, frame); label(c, end); break; } default: break; } } static void cgfn(Cg *c, FILE *out, Node *fn) { if (fn->body == NULL) return; /* extern decl, no body */ /* fresh per-fn state */ c->head = c->tail = NULL; c->fnname = fn->str; c->cur_mod = (fn->module && fn->module[0]) ? fn->module : NULL; c->labelseq = 0; cg_stack_arg_cursor = 0; ndefers = 0; nloops = 0; cg_ret_type = fn->type ? fn->type->ret : NULL; cg_retscr = 0; cg_tupfscr = 0; cg_tupargscr = 0; cg_tupargscr_sz = 0; cg_tagbase = 0; cg_tagbase_sz = 0; cg_tagscr = 0; cg_tagscr_sz = 0; cg_sret_arg_off = 0; cg_sret_dest_off = 0; cg_sret_dest_sym = NULL; cg_sretscr_off = 0; cg_sretscr_sz = 0; cg_sret_forward = 0; int frame = 0; Local *locals = NULL; cg_frame = &frame; /* TEXT directive comes first; framesize is filled at the end. */ Prog *text = newprog(c, A_TEXT); /* Mangle the label using the fn's own module as the hint — picks * the right entry when multiple modules export the same leaf. */ text->to = mafn(c, fn->str, c->cur_mod); text->from.offset = 0; /* framesize patched below */ emit(c, text); /* prologue */ ins1(c, A_PUSHQ, areg(D_BP)); ins2(c, A_MOVQ, areg(D_SP), areg(D_BP)); Prog *subsp = newprog(c, A_SUBQ); subsp->from = aimm(0); subsp->to = areg(D_SP); emit(c, subsp); /* sret discipline (#23): plain TY_STRUCT return > 24B consumes * RDI as a hidden first-arg dest pointer. Spill it to @sretarg * before the user-param loop so cgreturn can write through it, * and start the user-arg register counter at 1 to shift every * declared arg right by one (SI/DX/CX/R8/R9/+stack). */ if (cg_sret_retsize(cg_ret_type) > 0) { cg_sret_arg_off = local_alloc(c, &locals, "@sretarg", 8, &frame); ins2(c, A_MOVQ, areg(D_DI), amem(D_BP, cg_sret_arg_off)); } /* spill incoming arg registers to local slots. Slice params * occupy 24 bytes; float params land in XMM0..7 (counted * separately from integer DI/SI/DX/CX/R8/R9). */ int argi = (cg_sret_arg_off != 0) ? 1 : 0; int fargi = 0; 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; Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : 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; int tagged_sz = tagged_arg_size(pt); int is_tagged = tagged_sz > 0; int isf = cg_isfloat(pt); /* #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) { Type *teu = (te->type && te->type->kind == TY_NAMED) ? te->type->under : te->type; int wide = teu && (teu->kind == TY_SLICE || teu->kind == TY_STR); 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_ebytes(wide); 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 += wide ? (int)teu->size : 8; } 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_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_tagged ? tagged_sz : 8)); int off = localoff(c, &locals, p->str, sz, &frame); if (slice || is_str || is_struct || 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_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_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; } /* 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_value(c, name, module)); /* IEEE-754 sign-bit XOR for negation happens INSIDE the emit * loop on the top byte only — semantically identical to a whole- * u64 XOR with 2^63 (or 2^31 for f32) but never materialises * that constant. Mirrors the wwstage helper's shape so the * cgen.ww self-rebuild stays cs==ww byte-identical. */ for (int i = 0; i < sz; i++) { u8 b = (u8)((v >> (i * 8)) & 0xff); if (neg && i == sz - 1) b = (u8)(b ^ 0x80); emit_data_byte(out, b); } fputs("\"\n", out); return 1; } /* Forward declaration: emit_struct_lit_bytes recurses into * emit_array_lit_bytes for nested array fields (#129 A.3 closes the * A.2 shape-15 park). Defined further down. */ static int emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase); /* emit_struct_lit_bytes — emit the byte sequence for a struct-typed * top-level let/def whose rhs is an N_STRUCTLIT (or NULL for bare * no-rhs). Walks Tfield list in declaration order, zero-fills padding * gaps via the offset table (rule 13), and dispatches per field type: * integer/bool/nil via fold_int_literal, float via emit_floatlit_data's * peel+bitcast core inlined, nested struct via recursion (the per-field * inner literal lookup; nested-struct field-name-leak is a separate * #145 bug filed against the parser/checker — the recursion is * unblocked because emit-time field resolution goes through the type * table, not the parser's symbol table). Array / str / slice / ptr- * with-address fields are out of #129 A.2 scope — fatals loudly per * rule-7 so a future consumer gets a precise stop rather than a * silent zero-emit. * * Shared by emit_struct_data (#129 Phase A.2) below; broken out so the * recursive call can recurse on the inner field bytes without re- * opening the "DIR name(SB),\"" prefix. */ static int emit_struct_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, u64 base) { Type *u = (t && t->kind == TY_NAMED) ? t->under : 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 = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if (fu && fu->kind == TY_STRUCT) { /* Recurse into nested struct lit. Pre-#145 the parser/ * checker has its own gap on inner-N_STRUCTLIT field * name resolution; this emit recursion goes through * the type table so it's correct in isolation. */ if (vr == NULL || vr->kind != N_STRUCTLIT) fatal("emit_struct_lit_bytes: nested struct " "field '%s' rhs is not N_STRUCTLIT " "(#129 A.2)", f->name ? f->name : "?"); (void)emit_struct_lit_bytes(out, c, f->type, vr, fstart); pos = fstart + (u64)fsz; continue; } /* #129 A.3: array-typed field with N_ARRLIT rhs (the shape * parked in A.2). Calls emit_array_lit_bytes which dispatches * by element kind (int/float/struct). Returns 0 if the rhs * shape can't reduce — fatal here per rule-7 since the field * is declared array-typed and a non-reducible inner rhs is * a real bug surface, not a fall-through. */ if (fu && fu->kind == TY_ARRAY) { if (vr == NULL || vr->kind != N_ARRLIT) fatal("emit_struct_lit_bytes: array field " "'%s' rhs is not N_ARRLIT (#129 A.3)", f->name ? f->name : "?"); if (!emit_array_lit_bytes(out, c, f->type, vr, 1)) fatal("emit_struct_lit_bytes: array field " "'%s' rhs has non-reducible elements " "(#129 A.3)", f->name ? f->name : "?"); pos = fstart + (u64)fsz; continue; } if (type_isfloat(f->type)) { int isf32 = type_isf32(f->type); u64 fv = 0; int neg = 0; Node *fr = vr; if (fr != NULL && fr->kind == N_UN && (fr->op == TK_MINUS || fr->op == TK_PLUS)) { if (fr->op == TK_MINUS) neg = 1; fr = fr->lhs; while (fr != NULL && fr->kind == N_CAST) fr = fr->lhs; } if (fr == NULL || fr->kind != N_FLOATLIT) fatal("emit_struct_lit_bytes: float field " "'%s' rhs not foldable FLOATLIT (#129 A.2)", f->name ? f->name : "?"); if (isf32) { union { float f; u32 u; } x; x.f = (float)fr->fval; fv = (u64)x.u; } else { union { double d; u64 u; } x; x.d = fr->fval; fv = x.u; } for (int i = 0; i < fsz; i++) { u8 b = (u8)((fv >> (i * 8)) & 0xff); if (neg && i == fsz - 1) b = (u8)(b ^ 0x80); emit_data_byte(out, b); } pos = fstart + (u64)fsz; continue; } u64 iv = 0; if (!fold_int_literal(vr, &iv)) fatal("emit_struct_lit_bytes: field '%s' rhs not a " "foldable literal (str/slice/ptr/array fields " "are out of #129 A.2 scope)", f->name ? f->name : "?"); for (int i = 0; i < fsz; i++) emit_data_byte(out, (u8)((iv >> (i * 8)) & 0xff)); pos = fstart + (u64)fsz; } /* Tail padding to t->size. */ u64 end = base + t->size; while (pos < end) { emit_data_byte(out, 0); pos++; } return 1; } /* emit_struct_data — top-level wrapper that opens the DATA/DATAW * directive and delegates the byte payload to emit_struct_lit_bytes. * Shared SSoT between emit_lets's struct arm and emit_defs's struct * arm (#129 Phase A.2, rule-12 sea-of-stars). Returns 1 on emit, 0 if * the type isn't a struct. */ static int emit_struct_data(FILE *out, Cg *c, const char *directive, const char *name, const char *module, Type *t, Node *rhs) { Type *u = (t && t->kind == TY_NAMED) ? t->under : t; if (u == NULL || u->kind != TY_STRUCT) return 0; fprintf(out, "%s %s(SB),\"", directive, mod_mangle_value(c, name, module)); emit_struct_lit_bytes(out, c, t, rhs, 0); fputs("\"\n", out); return 1; } /* emit_array_lit_bytes — emit alen * esz bytes for an [N]T top-level * let/def with N_ARRLIT rhs. Per-element dispatch: * - int element (covers bool/rune/typed-int/N_UN-int): fold_int_literal * per element, emit LE bytes. Existing pre-#129-A.3 emit_lets array * arm logic preserved byte-for-byte so the bootstrap consumers in * lib/os, lib/bufio, lib/strings, lib/encoding/utf8, lib/strconv/ * stof_data don't shift. * - float element (f32/f64): peel N_CAST/N_UN(±), bitcast magnitude * via union (mirrors emit_floatlit_data), sign-XOR top byte of each * element inline. NO 2^63 immediate. * - struct element: per element call emit_struct_lit_bytes (#129 A.2 * helper). * - other element kinds (str/slice/ptr-with-address/nested-array): * return 0 — caller falls through to zero-init (str/slice accepts * no-rhs already). * * Trailing `...` repeat marker fills remaining slots with the last * value (mirrors the scalar repeat path). Returns 1 on emit, 0 if the * rhs shape can't reduce to a foldable literal — caller MUST then * fall back to zero-init / skip path; the caller opens the DATA/DATAW * directive AFTER a successful validate-only call. Two-call pattern * keeps emit-on-failure from emitting partial bytes. * * `emit_phase = 0` runs validate-only (returns 1 if ok); `emit_phase * = 1` actually emits. */ static int emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase) { Type *u = type_unwrap(t); if (u == NULL || u->kind != TY_ARRAY) return 0; Type *etype = u->sub; Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype; int esz = etype ? (int)etype->size : 1; int alen = (int)u->alen; if (eu && eu->kind == TY_STRUCT) { /* Validate: every element must be N_STRUCTLIT (after N_CAST * peel). */ int idx = 0; Node *last_ev = NULL; int repeat = 0; for (Node *e = rhs->list; e && idx < alen; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) { repeat = 1; break; } Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; if (ev == NULL || ev->kind != N_STRUCTLIT) return 0; last_ev = ev; idx++; } if (!emit_phase) return 1; idx = 0; repeat = 0; for (Node *e = rhs->list; e && idx < alen; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) { repeat = 1; break; } Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; emit_struct_lit_bytes(out, c, etype, ev, 0); idx++; } while (idx < alen) { if (repeat && last_ev != NULL) emit_struct_lit_bytes(out, c, etype, last_ev, 0); else for (int b = 0; b < esz; b++) emit_data_byte(out, 0); idx++; } return 1; } /* #129 A.3 capstone (PREREQ-1, #156): nested-array element [M]T * inside [N][M]T. Mirror of the TY_STRUCT-element arm above and of * the TY_ARRAY-field-in-struct arm in emit_struct_lit_bytes — recurse * into emit_array_lit_bytes per element; recursion bottoms out at * scalar (int/float) elements. esz = etype->size gives the per- * element stride (rule 13, no manual stride math). The `...` repeat * marker with nested-array elements is rejected loud (rule 7): no * consumer needs it (powers_of_ten is fully enumerated) and the * scalar-repeat byte-fill cannot reduce a nested N_ARRLIT. */ if (eu && eu->kind == TY_ARRAY) { int idx = 0; for (Node *e = rhs->list; e && idx < alen; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) fatal("emit_array_lit_bytes: '...' repeat with " "nested-array elements unsupported " "(#129 A.3, rule 7)"); Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; if (ev == NULL || ev->kind != N_ARRLIT) return 0; if (!emit_array_lit_bytes(out, c, etype, ev, 0)) return 0; idx++; } if (!emit_phase) return 1; idx = 0; for (Node *e = rhs->list; e && idx < alen; e = e->next) { Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; emit_array_lit_bytes(out, c, etype, ev, 1); idx++; } while (idx < alen) { for (int b = 0; b < esz; b++) emit_data_byte(out, 0); idx++; } return 1; } if (type_isfloat(etype)) { int isf32 = type_isf32(etype); /* Validate: every element must be N_FLOATLIT (after N_CAST * + optional N_UN(±) peel). */ int idx = 0; for (Node *e = rhs->list; e && idx < alen; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) break; Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; if (ev != NULL && ev->kind == N_UN && (ev->op == TK_MINUS || ev->op == TK_PLUS)) { ev = ev->lhs; while (ev && ev->kind == N_CAST) ev = ev->lhs; } if (ev == NULL || ev->kind != N_FLOATLIT) return 0; idx++; } if (!emit_phase) return 1; idx = 0; u64 last_bits = 0; int last_neg = 0; int repeat = 0; for (Node *e = rhs->list; e && idx < alen; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) { repeat = 1; break; } Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; int neg = 0; if (ev != NULL && ev->kind == N_UN && (ev->op == TK_MINUS || ev->op == TK_PLUS)) { if (ev->op == TK_MINUS) neg = 1; ev = ev->lhs; while (ev && ev->kind == N_CAST) ev = ev->lhs; } u64 bits = 0; if (isf32) { union { float f; u32 u; } x; x.f = (float)ev->fval; bits = (u64)x.u; } else { union { double d; u64 u; } x; x.d = ev->fval; bits = x.u; } for (int b = 0; b < esz; b++) { u8 byt = (u8)((bits >> (b * 8)) & 0xff); if (neg && b == esz - 1) byt = (u8)(byt ^ 0x80); emit_data_byte(out, byt); } last_bits = bits; last_neg = neg; idx++; } while (idx < alen) { if (repeat) { for (int b = 0; b < esz; b++) { u8 byt = (u8)((last_bits >> (b * 8)) & 0xff); if (last_neg && b == esz - 1) byt = (u8)(byt ^ 0x80); emit_data_byte(out, byt); } } else { for (int b = 0; b < esz; b++) emit_data_byte(out, 0); } idx++; } return 1; } /* Int-element path — preserved BYTE-FOR-BYTE from the pre-A.3 * emit_lets in-place array arm so the bootstrap consumers (u8 / * i8 / u16 arrays in lib/os, lib/bufio, lib/strings, lib/ * encoding/utf8, lib/strconv/stof_data) don't shift. */ u64 *vals = amalloc(c->a, sizeof(u64) * (size_t)alen); int idx = 0; int ok = 1; u64 last = 0; int repeat = 0; for (Node *e = rhs->list; e && idx < alen; e = e->next) { if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0) { repeat = 1; break; } Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; if (ev == NULL) { ok = 0; break; } if (!fold_int_literal(ev, &last)) { ok = 0; break; } vals[idx++] = last; } if (!ok) return 0; if (!emit_phase) return 1; if (repeat) { while (idx < alen) vals[idx++] = last; } else { while (idx < alen) vals[idx++] = 0; } for (int i = 0; i < alen; i++) { u64 v = vals[i]; for (int b = 0; b < esz; b++) emit_data_byte(out, (u8)((v >> (b * 8)) & 0xff)); } return 1; } /* emit_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_unwrap(t); if (u == NULL || u->kind != TY_ARRAY) return 0; if (!emit_array_lit_bytes(out, c, t, rhs, 0)) return 0; fprintf(out, "%s %s(SB),\"", directive, mod_mangle_value(c, name, module)); emit_array_lit_bytes(out, c, t, rhs, 1); fputs("\"\n", out); return 1; } static void emit_lets(Cg *c, FILE *out, Node *file) { for (Node *d = file->list; d; d = d->next) { if (d->kind != N_LET) continue; if (d->str == NULL || d->str[0] == '\0') continue; int sz = let_emit_size(d->type); if (sz == 0) continue; if (let_isfloat(d->type)) { (void)emit_floatlit_data(out, c, "DATAW", d->str, d->module, d->type, d->rhs); continue; } /* #129 A.2: gate `!let_isstruct` so an 8B struct lit * (`struct { i32, i32 }`, `struct { f32, f32 }`, …) does * NOT short-circuit through the scalar 8B `fold_int_literal` * arm — fold-fail-`continue` would otherwise drop the let * entirely, emitting no DATA and diverging from wwstage's * emitletdataw (which gates its 8B scalar with `!issg`). * Symmetric ordering with the wwstage struct arm. */ if (sz == 8 && !let_isarray(d->type) && !let_isstruct(d->type)) { u64 v = 0; if (d->rhs != NULL) { Node *r = d->rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r == NULL) continue; /* Same helper as emit_defs (#24): widens * the gate to cover N_UN(TK_MINUS/TILDE/PLUS, * leaf) so `let x: i8 = -1i8;` and friends * encode as sign-extended two's-complement * bytes. emit_data_row writes 8 LE bytes * so narrow signed types just naturally * round-trip via the sign-extended u64. */ if (!fold_int_literal(r, &v)) continue; } emit_data_row(out, "DATAW", mod_mangle_value(c, d->str, d->module), v); continue; } /* Strip leading casts on the rhs so a `nil: str` etc. * reads the same as a bare nil. */ Node *r = NULL; if (d->rhs != NULL) { r = d->rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r == NULL) continue; } /* str literal init: bake the interned label's address * into the ptr half via a DATAR reloc, set the len half * inline. */ /* #43: gate via ty_str->size so #1 propagates. */ if (sz == (int)ty_str->size && r != NULL && r->kind == N_STRLIT && r->strlen > 0) { const char *lab = intern_strlit(c, r->str, r->strlen); const char *sym = mod_mangle_value(c, d->str, d->module); u64 v = r->strlen; /* 16-byte payload: 8 zero placeholder + LE len. */ fprintf(out, "DATAW %s(SB),\"", sym); for (int i = 0; i < 8; i++) emit_data_byte(out, 0); for (int i = 0; i < 8; i++) emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); fputs("\"\n", out); fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, lab); continue; } /* Array literal init: `let xs: [N]T = [v0, v1, ...];`. The * helper dispatches per element kind (int/float/struct). * Int-element path preserved BYTE-FOR-BYTE from pre-A.3 so * bootstrap consumers (lib/os, lib/bufio, lib/strings, lib/ * encoding/utf8, lib/strconv/stof_data) don't shift. Float * + struct elements gain emit; ptr / nested-array fall * through to zero-init (existing path below). */ if (r != NULL && r->kind == N_ARRLIT && let_isarray(d->type)) { if (emit_array_data(out, c, "DATAW", d->str, d->module, d->type, r)) continue; /* fall through to zero-init */ } /* Otherwise: zero-init. str accepts nil / ""; struct * accepts no rhs at all; slice accepts nil; array with no * literal init (or a non-constant one) zero-fills. */ if (r != NULL) { int is_struct = let_isstruct(d->type); int is_array = let_isarray(d->type); int empty_str = (r->kind == N_STRLIT && r->strlen == 0); /* #129 A.2: struct-typed let with N_STRUCTLIT rhs * routes through the emit_struct_data SSoT. Pre-#129 * this fell through to `continue` and emit-NOTHING, * so the link surfaced an undefined ref. */ if (is_struct && r->kind == N_STRUCTLIT) { if (emit_struct_data(out, c, "DATAW", d->str, d->module, d->type, r)) continue; } if (is_struct) continue; if (is_array) continue; if (r->kind != N_NIL && !empty_str) continue; } emit_data_row_zero(out, "DATAW", mod_mangle_value(c, d->str, d->module), sz); } } /* Emit DATA directives for top-level `def` constants whose value * folds to an integer literal. The w6a side stores the bytes inside * .text and accesses are RIP-relative. * * fold_int_literal (cmd/wcc/check.c) gates: int/rune literal, * true/false/nil, and a unary +/-/~ over the same. `def NEG: i32 = * -100;` arrives as N_UN(TK_MINUS, N_INTLIT) — the unary peel is * exactly what the gate is for. Anything richer (sibling refs, * arithmetic) falls through; emit_defs has no scope to resolve * names. */ static void emit_defs(Cg *c, FILE *out, Node *file) { for (Node *d = file->list; d; d = d->next) { if (d->kind != N_DEF || d->rhs == NULL) continue; u64 v; if (fold_int_literal(d->rhs, &v)) { fprintf(out, "DATA %s(SB),\"", mod_mangle_value(c, d->str, d->module)); for (int i = 0; i < 8; i++) { unsigned b = (unsigned)((v >> (i * 8)) & 0xff); if (b == '"' || b == '\\') fprintf(out, "\\%c", b); else if (b < 0x20 || b >= 0x7f) fprintf(out, "\\x%02x", b); else fputc(b, out); } fputs("\"\n", out); continue; } /* Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) rhs. * Routes through the same SSoT helper as emit_lets's float * arm — pre-#129 this fell through to no-emit + undef-ref * at link. */ if (let_isfloat(d->type)) { (void)emit_floatlit_data(out, c, "DATA", d->str, d->module, d->type, d->rhs); continue; } /* #129 A.2: struct-typed def with N_STRUCTLIT rhs. Parallel * to emit_lets's struct arm; uses DATA (read-only) directive. * Without the LOAD-side widening below the def's address * still wouldn't be reachable, but storage is the precondition * for the LOAD path to find something. */ if (let_isstruct(d->type) && d->rhs->kind == N_STRUCTLIT) { (void)emit_struct_data(out, c, "DATA", d->str, d->module, d->type, d->rhs); continue; } /* #129 A.3: array-typed def with N_ARRLIT rhs. Parallel to * emit_lets's array arm; uses DATA (read-only). LOAD-side * widening at cgindex/cgdot resolves the def's address via * LEAQ name(SB). */ if (let_isarray(d->type) && d->rhs->kind == N_ARRLIT) { (void)emit_array_data(out, c, "DATA", d->str, d->module, d->type, d->rhs); continue; } } (void)c; } /* Collect str-typed `def`s so cgexpr N_IDENT can splice them inline. * Walks past any leading cast on the rhs (e.g. `def x: error = "x": error;` * shows up as N_CAST wrapping an N_STRLIT). */ static void sdef_collect(Cg *c, Node *file) { (void)file; sdefs = NULL; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_DEF || d->rhs == NULL) continue; Node *r = d->rhs; while (r && r->kind == N_CAST) r = r->lhs; if (r == NULL || r->kind != N_STRLIT) continue; Sdef *s = amalloc(c->a, sizeof *s); s->name = d->str; s->mod = (d->module && d->module[0]) ? d->module : NULL; s->bytes = r->str; s->len = r->strlen; s->next = sdefs; sdefs = s; } } /* Pre-intern strlits referenced from top-level `let` initialisers * (e.g. `let g: str = "hello";`). Interning has to happen before * emit_data walks the strlit list, but we don't want to reorder * emit_data after emit_lets (the (DATA strlits, DATAW lets) section * order is part of the byte-identity contract with the selfhost * cgen). So this pass populates the strlit table; emit_lets later * just looks up the label. */ static void let_pre_intern(Cg *c, Node *file) { if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_LET) continue; if (let_emit_size(d->type) != (int)ty_str->size) continue; Node *r = d->rhs; while (r != NULL && r->kind == N_CAST) r = r->lhs; if (r == NULL || r->kind != N_STRLIT) continue; if (r->strlen == 0) continue; (void)intern_strlit(c, r->str, r->strlen); } } void cg_file(Cg *c, FILE *out, Node *file) { if (file == NULL || file->kind != N_FILE) return; ffi_collect(c, file); mod_collect(c, file); sdef_collect(c, file); let_collect(c, file); strlits = NULL; strlit_seq = 0; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_FNDECL) continue; cgfn(c, out, d); } let_pre_intern(c, file); emit_data(c, out); emit_defs(c, out, file); emit_lets(c, out, file); } void peephole(Cg *c) { (void)c; } void regalloc_init(Cg *c) { (void)c; }