/* * 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 @-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; 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; } /* cg_sret_retsize — if `rt` is a plain TY_STRUCT > 24B, return its * natural size (the sret threshold); else 0. Tagged unions, tuples, * str, and slices route through their existing register-return ABIs * regardless of size. Task #23. */ static int cg_sret_retsize(Type *rt) { if (rt == NULL) return 0; if (rt->kind == TY_NAMED) rt = rt->under; if (rt == NULL || rt->kind != TY_STRUCT) return 0; if ((int)rt->size <= 24) return 0; return (int)rt->size; } 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); } 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; } /* 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; } /* 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) return 0; return type_eq(vt, src); } /* 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; int idx = 0; for (Tparam *p = t->params; p; p = p->next, idx++) { if (cg_variant_match(p->type, vt)) return idx; } 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; LetVar *next; }; static LetVar *letvars; /* 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_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: return 16; /* {ptr, len}; literal-strlit init NYI. */ case TY_SLICE: return 24; /* {ptr, len, cap}; no init only. */ 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; } /* 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; if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_LET) continue; 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->next = letvars; letvars = lv; } } 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; } /* 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); } /* Forward decl — masym below depends on asym defined further down. */ static Adr asym(const char *s); static Adr masym(Cg *c, const char *ident) { return asym(mod_mangle(c, ident)); } /* Fn-name address builder. Use at every CALL/LEAQ site whose target * is a top-level fn — passes the hint so cross-module same-leaf * exports resolve to the right module. */ static Adr mafn(Cg *c, const char *ident, const char *hint) { return asym(mod_mangle_fn(c, ident, hint)); } void cg_init(Cg *c, Arena *a) { memset(c, 0, sizeof *c); c->a = a; } Prog * newprog(Cg *c, int op) { Prog *p = amalloc(c->a, sizeof *p); p->as = op; return p; } void emit(Cg *c, Prog *p) { if (c->head == NULL) c->head = p; else c->tail->link = p; c->tail = p; } static Adr areg(int r) { Adr a = { 0 }; a.type = r; return a; } static Adr aimm(long long v) { Adr a = { 0 }; a.type = D_CONST; a.offset = v; return a; } static Adr amem(int r, long long off) { Adr a = { 0 }; a.type = D_INDIR; a.reg = r; a.offset = off; return a; } static Adr asym(const char *s) { Adr a = { 0 }; a.type = D_EXTERN; a.sym = s; return a; } static Adr abranch(const char *s) { Adr a = { 0 }; a.type = D_BRANCH; a.sym = s; return a; } static char * mklabel(Cg *c, const char *prefix) { return aprintf(c->a, "%s_%s_%d", 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); } 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); } /* ------------------------------------------------------------------ */ /* 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) */ } /* ------------------------------------------------------------------ */ /* 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_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)); } /* 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; 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) { 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)); 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 payload: AX=ptr, BX=len from cgexpr. */ if (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)); 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; } /* Slice payload: cgexpr leaves (AX=ptr, BX=len, CX=cap). The * slot layout is tag@+0, ptr@+8, len@+16, cap@+24 — requires the * destination tagged-union slot be at least 32B. */ if (type_isslice(st) || (su && su->kind == TY_SLICE)) { 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)); 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. The high slot word (when sz > 16) is * left untouched here — match dispatches on the tag word first * and only the str branch reads slot+16, so leaving the pad * uninitialised in let/assign matches the pre-refactor asm. * cg_widen_tagged_push pre-zeroes the scratch slot before * calling us, so the call-site push still sees clean pad. */ cgexpr(c, src, *locals_p); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8)); 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)) { /* slot 24: [+0]=tag, [+8]=ptr, [+16]=len. Push len, * ptr, tag (high→low so pop drains tag first). */ 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; } 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: cgexpr_int(c, (long long)n->uval); break; case N_FLOATLIT: { union { double d; u64 u; } x; x.d = n->fval; 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)); break; } case N_STRLIT: { /* result lives as the (ptr, len) pair: ptr in AX, len in BX. * Call sites that pass a str arg pick these up directly. */ 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)); 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 values flow as (AX=ptr, BX=len) so they * can be returned in AX:DX or pushed to the * call-arg stack uniformly. */ ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX)); } 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)); goto ident_done; } } if (let_islet(n->str) && (let_isstr(n->type) || let_isslice(n->type))) { /* Top-level str/slice global: load each half * via its address (the asm has no `name+8(SB)` * operand form). Slice has a third 8B (cap) * — the address holder CX gets overwritten by * the cap as the last step, after we no longer * need it. */ int is_slice = let_isslice(n->type); ins2(c, A_LEAQ, masym(c, n->str), 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)); if (is_slice) ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX)); goto ident_done; } if (let_islet(n->str) && let_isfloat(n->type)) { /* Top-level float global: same LEAQ-indirect * shape as str/slice, since MOVSS/MOVSD have * no D_EXTERN operand form in w6a. */ int op = type_isf32(n->type) ? A_MOVSS : A_MOVSD; ins2(c, A_LEAQ, masym(c, n->str), 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, masym(c, n->str), 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, masym(c, n->str), 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); if (off != 0) { ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX)); } else if (let_islet(opnd->str)) { ins2(c, A_LEAQ, masym(c, opnd->str), areg(D_AX)); } break; } if (opnd && opnd->kind == N_DOT) { /* 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; if (bu && bu->kind == TY_STR) esz = 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 */ ins2(c, A_MOVQ, 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 on unordered; we ignore it, * which means NaN compares behave like Hare's default. */ 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)); 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 = A_JB; break; case TK_LE: op = A_JBE; break; case TK_GT: op = A_JA; break; case TK_GE: op = A_JAE; 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; } 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 */ ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); ins2(c, n->op == TK_LSHIFT ? A_SHLQ : A_SHRQ, 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 *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; int via_ptr = 0; if (u && u->kind == TY_PTR) { via_ptr = 1; u = u->sub; if (u && u->kind == TY_NAMED) u = u->under; } /* 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; /* str-typed field: rhs cgexpr leaves (AX=ptr, BX=len); * store both halves at field+0 and field+8. The 8/16 * trailing-padding bytes are left untouched, which * matches the let-init shape elsewhere in cgen. Only * plain `=` is wired; compound on a str field is not * meaningful. */ Type *str_fu = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STR) { cgexpr(c, n->rhs, locals); if (via_ptr) { ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_CX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8)); } else if (is_global) { ins2(c, A_LEAQ, masym(c, base->str), areg(D_CX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8)); } 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)); } break; } /* slice-typed field: rhs cgexpr leaves (AX=ptr, * BX=len, CX=cap); store all three at * field+0/+8/+16. Address scratch must dodge CX * (holds cap), so via_ptr/is_global stage the * struct base in DX. Without this branch the * generic store_op below writes only AX, silently * dropping .len and .cap. */ if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_SLICE) { 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) { /* str rhs: AX=ptr, BX=len. * Stash both, compute addr * in CX so the pop pair * restores AX/BX cleanly. */ cgexpr(c, n->rhs, locals); 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_CX)); else ins2(c, A_MOVQ, amem(D_BP, off), areg(D_CX)); ins2(c, A_ADDQ, areg(D_AX), areg(D_CX)); if (viaptr) ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_CX)); ins1(c, A_POPQ, areg(D_AX)); ins1(c, A_POPQ, areg(D_BX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8)); 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 *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) { 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) { /* str rhs: (AX=ptr, BX=len). Stash * both, then load the struct ptr * into CX and write both halves. */ cgexpr(c, n->rhs, locals); 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_CX)); ins1(c, A_POPQ, areg(D_AX)); ins1(c, A_POPQ, areg(D_BX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8)); } 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. Float/str compound on a * chained pointer-field is not wired. */ 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)); 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 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) { 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 + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, total_off + 8)); } 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)); } 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); 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) { cgexpr(c, n->rhs, locals); /* AX (and BX if str) */ /* str element: also stash len so we can store both */ if (elem_is_str) ins1(c, A_PUSHQ, areg(D_BX)); 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 { cgexpr(c, base, locals); ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); } ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */ ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); ins1(c, A_POPQ, areg(D_AX)); /* value (ptr if str) */ if (elem_is_str) { 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)); break; } int store_op = fldstoreop(esub, esz); ins2(c, store_op, 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 = value (BX too if str) */ ins1(c, A_PUSHQ, areg(D_AX)); if (vt && vt->kind == TY_STR) ins1(c, A_PUSHQ, areg(D_BX)); cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */ ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); if (vt && vt->kind == TY_STR) { 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: ins2(c, A_SHRQ, 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; } } /* Plain `name = strexpr;` for a str-typed local. cgexpr leaves * (AX=ptr, BX=len); store both halves at off+0 and off+8. * Mirrors the let-init shape so reassignment doesn't truncate. * Top-level str globals follow the same shape but go through * &name(SB) since the asm has no `name+8(SB)` operand form. */ 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_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)); break; } if (let_islet(n->lhs->str)) { cgexpr(c, n->rhs, locals); 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)); break; } break; } /* Slice reassignment: cgexpr produces (AX=ptr, BX=len, * CX=cap). Store all three at off+0/+8/+16 (local) or * via &name(SB) → DI scratch (global — CX holds the * cap, so we need a different address register). */ if (lu && lu->kind == TY_SLICE) { 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; } } /* 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: ins2(c, A_MOVQ, areg(D_AX), areg(D_CX)); ins2(c, A_SHRQ, 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: ins2(c, A_MOVQ, areg(D_AX), areg(D_CX)); ins2(c, A_SHRQ, 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); 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->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. */ 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; /* call os.alloc(sz) */ ins2(c, A_MOVQ, aimm(sz), areg(D_DI)); ins1(c, A_CALL, asym(ffi_resolve("alloc"))); 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-typed field: cgexpr leaves (AX=ptr, BX=len). * Route the heap base through CX so both halves * survive — using BX would clobber len. */ 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_CX)); ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, (int)foff + 0)); ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, (int)foff + 8)); 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_AX)); /* return the ptr */ 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"); int sloff = localoff(c, &locals, slname, 24, 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; /* 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 */ ins2(c, A_ADDQ, areg(D_AX), areg(D_CX)); /* push cap, len, ptr (top) */ ins1(c, A_PUSHQ, areg(D_DX)); /* cap */ 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); if (node_isfloat(args[i])) { ins2(c, A_SUBQ, aimm(8), areg(D_SP)); ins2(c, A_MOVSD, areg(D_X0), amem(D_SP, 0)); } else if (node_isstr(args[i])) { 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 { 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; { Type *ret = (cu && cu->kind == TY_FN) ? cu->ret : NULL; sret_call_sz = cg_sret_retsize(ret); } 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; 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) { ins2(c, A_MOVSD, 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])) { for (int k = 0; k < 2; 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 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 (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 { 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)); /* If callee returns a str (16B → AX:DX per SysV), shuffle * len from DX into BX so str values stay in (AX, BX). */ if (node_isstr(n)) ins2(c, A_MOVQ, areg(D_DX), areg(D_BX)); 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 { int bsz = 8; if (bu && bu->kind == TY_STR) bsz = 16; else if (bu && bu->kind == TY_SLICE) bsz = 24; else 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) { char *propret = mklabel(c, "tryprop_ret"); 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)); ins1(c, A_JMP, abranch(propret)); label(c, skip); } 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) ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); 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) ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); 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 *u = (bt && bt->kind == TY_NAMED) ? bt->under : 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; if (mqop == A_MOVQ) { ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); } else { ins2(c, A_LEAQ, masym(c, n->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); if (!root_resolved && let_islet(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); /* str element: load (ptr, len) into (AX, BX) so chains * like `t.1.len` propagate through the str-rhs * convention. Without this we'd MOVQ 8B and the .len * shuffle (BX→AX) would surface garbage. */ 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)); 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; if (off == 0 && let_islet(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 field: load (ptr, len) into (AX, BX) so the * value flows through the str-rhs convention. */ Type *str_fu = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if (str_fu && str_fu->kind == TY_STR) { 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)); break; } /* slice field: load (ptr, len, cap) into (AX, BX, CX) * so the value flows through the slice-rhs convention. * base_reg may be CX for globals; load .cap LAST so * the base survives the earlier reads. */ if (str_fu && str_fu->kind == TY_SLICE) { 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 = u->sub; if (inner->kind == TY_NAMED) inner = inner->under; 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. */ if (u && u->kind == TY_PTR && u->sub) { Type *inner = u->sub; if (inner->kind == TY_NAMED) inner = inner->under; 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 field through *struct: read len into a * scratch first (it's at +8) so loading ptr * into AX last leaves (AX=ptr, BX=len). We * use CX as the scratch, then move CX→BX. */ Type *str_fu = (f->type && f->type->kind == TY_NAMED) ? f->type->under : f->type; if (str_fu && str_fu->kind == TY_STR) { ins2(c, A_MOVQ, amem(D_BX, (int)f->offset + 8), areg(D_CX)); ins2(c, A_MOVQ, amem(D_BX, (int)f->offset + 0), areg(D_AX)); ins2(c, A_MOVQ, areg(D_CX), areg(D_BX)); break; } /* 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. */ if (str_fu && str_fu->kind == TY_SLICE) { 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 field: load (ptr, len) into (AX, BX). */ if (fu && fu->kind == TY_STR) { ins2(c, A_MOVQ, amem(D_AX, (int)f->offset + 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_AX, (int)f->offset + 0), areg(D_AX)); goto dot_done; } /* slice field: load (ptr, len, cap) into * (AX, BX, CX). AX is the *struct base, so * load .ptr (which targets AX) LAST. */ if (fu && fu->kind == TY_SLICE) { 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) { ins2(c, A_MOVQ, amem(D_AX, foff + 8), areg(D_BX)); 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; 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; if (u && u->kind == TY_STR) esz = 1; 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); int isglobal = (off == 0) && let_islet(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)); /* str element: load (ptr, len) into (AX, BX) so the * value flows through the str-rhs convention. */ if (u->sub && type_isstr(u->sub)) { ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_CX)); ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX)); ins2(c, A_MOVQ, areg(D_CX), areg(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; } 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). */ 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)); cgexpr(c, n->lhs, locals); ins1(c, A_POPQ, areg(D_BX)); ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); /* str element: load (ptr, len) into (AX, BX). */ if (u && u->sub && type_isstr(u->sub)) { ins2(c, A_MOVQ, amem(D_AX, 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_AX, 0), areg(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; } { 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, BX=hi-lo, CX=hi-lo) so callers can route * to a slice slot, return, or arg with the same ABI. Cap * defaults to the new length — there's no syntax for a * larger cap yet. Element scaling on the ptr isn't wired * (matches the let-init path), so non-u8 slices need a * follow-up audit when fixtures exercise them. */ 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; 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)); ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); ins2(c, A_SUBQ, areg(D_CX), areg(D_BX)); 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 *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; int sz = 8; if (lu && lu->kind == TY_ARRAY) sz = (int)lu->size; else if (lu && lu->kind == TY_SLICE) sz = 24; else if (lu && lu->kind == TY_STR) sz = 16; else if (lu && lu->kind == TY_STRUCT) sz = (int)lu->size; else if (lu && lu->kind == TY_TUPLE) sz = (int)lu->size; else if (lu && 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. */ if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24 && n->rhs->kind == N_CALL && n->rhs->lhs && n->rhs->lhs->kind == N_IDENT && strcmp(n->rhs->lhs->str, "alloc") == 0 && n->rhs->list && n->rhs->list->kind == N_ARRLIT && n->rhs->list->list == NULL && n->rhs->list->next && n->rhs->list->next->next == NULL) { Node *count = n->rhs->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("alloc"))); 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 initialiser: cgexpr produces (AX=ptr, BX=len). */ if (n->rhs && type_isstr(lt) && sz == 16) { 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)); break; } /* 2-tuple initialiser from a function call: SysV returns * a 16-byte aggregate in (AX, DX). Store both into the * tuple slot. */ if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 16) { cgexpr(c, n->rhs, *locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8)); break; } /* 24B tuple initialiser for `(scalar, str)` / `(str, scalar)`. * Per the AX:DX:CX return convention: AX = scalar elem, * DX = str.ptr, CX = str.len. The slot is laid out positionally * (e0 at +0, e1 at +8 for scalars; str takes 16B starting at * its position), so we route each register to the slot dictated * by the element's type, not by AX/DX position. */ if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 24) { Tparam *p0 = lu->params; Tparam *p1 = p0 ? p0->next : NULL; Type *t0 = p0 ? p0->type : NULL; Type *t1 = p1 ? p1->type : NULL; Type *u0 = (t0 && t0->kind == TY_NAMED) ? t0->under : t0; Type *u1 = (t1 && t1->kind == TY_NAMED) ? t1->under : t1; int e0_str = u0 && u0->kind == TY_STR; int e1_str = u1 && u1->kind == TY_STR; if (e0_str ^ e1_str) { cgexpr(c, n->rhs, *locals); if (e0_str) { /* layout: str@+0 (16B), scalar@+16. */ ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 16)); } else { /* layout: scalar@+0 (8B), str@+8 (16B). */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); } 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; } /* slice expression initialiser: build a {ptr, len, cap} header * referring to the source. Element size assumed to be 1 * (u8) for now; real element-size scaling is a future TODO. */ if (n->rhs && n->rhs->kind == N_SLICE && lu && lu->kind == TY_SLICE) { Node *base = n->rhs->lhs; Node *lo = n->rhs->rhs; Node *hi = n->rhs->cond; Type *bt = base ? base->type : NULL; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; /* load base address */ if (base->kind == N_IDENT) { int boff = localfind(*locals, base->str); if (bu && bu->kind == TY_ARRAY) { ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX)); } else { /* slice/str/ptr: load .ptr */ ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX)); } } else { cgexpr(c, base, *locals); } ins1(c, A_PUSHQ, areg(D_AX)); /* save base addr */ /* lo (default 0) */ if (lo) cgexpr(c, lo, *locals); else cgexpr_int(c, 0); ins1(c, A_PUSHQ, areg(D_AX)); /* save lo */ /* hi (default base length) */ 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); 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)); /* BX = hi */ ins1(c, A_POPQ, areg(D_CX)); /* CX = lo */ ins1(c, A_POPQ, areg(D_AX)); /* AX = base */ ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); /* AX = base+lo */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); ins2(c, A_SUBQ, areg(D_CX), areg(D_BX)); /* BX = hi-lo */ ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); break; } /* Generic slice rhs (e.g. fn returning []u8, slice ident, * slice-typed param). cgexpr leaves (AX=ptr, BX=len, CX= * cap); store all three into the local slot. Runs after * the alloc and N_SLICE specialisations above so they keep * their direct-store 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). */ 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); int op = A_MOVQ; if (!is_str_el) { if (esz == 1) op = A_MOVB; else if (esz == 4) op = A_MOVL; /* esz == 2 (i16/u16) falls through to MOVQ — * over-writes by 6B; the next element store * rewrites the high half. For the last element * this trails 6 bytes into the next stack slot. * Add MOVW to w6a if real i16 arrays land. */ } 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 { 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 { 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)) { ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); ins2(c, A_MOVQ, areg(D_AX), areg(D_DX)); /* str fills DX,CX. Zero R8 if dst * slot covers slot+24. */ 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 *rt = cg_ret_type; if (rt->kind == TY_NAMED) rt = rt->under; /* 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 *rt = cg_ret_type; if (rt->kind == TY_NAMED) rt = rt->under; 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; } } 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)) { cgexpr(c, n->lhs, *locals); /* AX=ptr, BX=len */ ins2(c, A_MOVQ, areg(D_BX), areg(D_DX)); 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) { /* 2-tuple ABI: * (scalar, scalar) — AX = e0, DX = e1. (16B, fits SysV.) * (scalar, str) — AX = scalar elem, * DX = str.ptr, CX = str.len. (24B custom.) * (str, scalar) — same regs, type-keyed not position-keyed. * * The 24B convention mirrors the existing tagged-union return * (AX:DX:CX); receive sites destructure off the same regs. */ Node *e0 = n->lhs->list; Node *e1 = e0 ? e0->next : NULL; if (e1 && e1->next == NULL) { int e0_is_str = node_isstr(e0); int e1_is_str = node_isstr(e1); if (e0_is_str ^ e1_is_str) { Node *strn = e0_is_str ? e0 : e1; Node *scaln = e0_is_str ? e1 : e0; cgexpr(c, scaln, *locals); /* AX = scalar */ ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, strn, *locals); /* AX=ptr, BX=len */ ins2(c, A_MOVQ, areg(D_BX), areg(D_CX)); ins2(c, A_MOVQ, areg(D_AX), areg(D_DX)); ins1(c, A_POPQ, areg(D_AX)); } else { cgexpr(c, e1, *locals); ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, e0, *locals); ins1(c, A_POPQ, areg(D_DX)); } } else { /* >2-tuple not yet implemented; fall back to first elem */ if (e0) cgexpr(c, e0, *locals); else cgexpr_int(c, 0); } } 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"); if (nloops < LOOP_MAX) { loop_cont[nloops] = loop; 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); 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"); 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] = loop; loop_brk[nloops] = end; nloops++; } cgstmt(c, n->body, locals, frame); if (nloops > 0) nloops--; if (n->rhs) 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: { /* eval rhs; consume the per-type return-ABI registers. * (scalar, scalar) — AX → l0, DX → l1. * (scalar, str) — AX → scalar slot, (DX, CX) → str slot * as (.ptr, .len). Position-agnostic. * Local sizing comes from each l->type so the str slot gets * the full 16B; without this, only DX would land and the * len half (CX) would have nowhere to go. */ cgexpr(c, n->rhs, *locals); Node *l0 = n->list; Node *l1 = l0 ? l0->next : NULL; Type *t0 = l0 ? l0->type : NULL; Type *t1 = l1 ? l1->type : NULL; Type *u0 = (t0 && t0->kind == TY_NAMED) ? t0->under : t0; Type *u1 = (t1 && t1->kind == TY_NAMED) ? t1->under : t1; int s0_is_str = u0 && u0->kind == TY_STR; int s1_is_str = u1 && u1->kind == TY_STR; if (l0 && l1 && (s0_is_str ^ s1_is_str)) { int sz0 = s0_is_str ? 16 : 8; int sz1 = s1_is_str ? 16 : 8; int off0 = localoff(c, locals, l0->str, sz0, frame); int off1 = localoff(c, locals, l1->str, sz1, frame); if (s0_is_str) { /* l0 is str: ptr=DX, len=CX. l1 is scalar: l1 = AX. */ ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off0 + 0)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off0 + 8)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off1)); } else { /* l0 is scalar; l1 is str. */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off0)); ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off1 + 0)); ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off1 + 8)); } break; } ins1(c, A_PUSHQ, areg(D_DX)); /* save 2nd while we store 1st */ if (l0) { int off = localoff(c, locals, l0->str, 8, frame); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); } ins1(c, A_POPQ, areg(D_DX)); if (l1) { int off = localoff(c, locals, l1->str, 8, frame); ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off)); } break; } case N_MASSIGN: { cgexpr(c, n->rhs, *locals); ins1(c, A_PUSHQ, areg(D_DX)); Node *l0 = n->list; Node *l1 = l0 ? l0->next : NULL; if (l0 && l0->kind == N_IDENT) { int off = localfind(*locals, l0->str); if (off != 0) ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); } ins1(c, A_POPQ, areg(D_DX)); if (l1 && l1->kind == N_IDENT) { int off = localfind(*locals, l1->str); if (off != 0) ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off)); } 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_tagbase = 0; cg_tagbase_sz = 0; cg_tagscr = 0; cg_tagscr_sz = 0; cg_sret_arg_off = 0; cg_sret_dest_off = 0; 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); /* 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; int eightbytes = slice ? 3 : (is_str ? 2 : (is_struct ? struct_eb : (is_tagged ? tagged_eb : 1))); int regs_left = isf ? (8 - fargi) : (6 - argi); if (regs_left >= eightbytes) { int sz = slice ? 24 : (is_str ? 16 : (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. */ int sz = slice ? 24 : (is_str ? 16 : (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. */ 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)) { /* sz is 4 (f32) or 8 (f64). FLOATLIT init or zero. */ int isf32 = type_isf32(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; if (r->kind != N_FLOATLIT) continue; 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, "DATAW %s(SB),\"", mod_mangle(c, d->str)); for (int i = 0; i < sz; i++) emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff)); fputs("\"\n", out); continue; } if (sz == 8 && !let_isarray(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(c, d->str), 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. */ if (sz == 16 && r != NULL && r->kind == N_STRLIT && r->strlen > 0) { const char *lab = intern_strlit(c, r->str, r->strlen); const char *sym = mod_mangle(c, d->str); 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, ...];`. Walk * elements in declaration order; each must reduce to an * integer literal (casts are stripped). The trailing `...` * repeat marker fills remaining slots with the last value. * Falls through to zero-init if any element isn't a * constant we can evaluate at emit time. */ if (r != NULL && r->kind == N_ARRLIT && let_isarray(d->type)) { Type *u = type_unwrap(d->type); int esz = (u && u->sub) ? (int)u->sub->size : 1; int alen = (u) ? (int)u->alen : 0; 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 = r->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; } /* Same helper as the scalar arm above — * fold_int_literal covers N_UN over a leaf * so signed-negative array elements (`-1i8`) * encode as their two's-complement bytes * once truncated to esz below. */ if (!fold_int_literal(ev, &last)) { ok = 0; break; } vals[idx++] = last; } if (ok) { if (repeat) { while (idx < alen) vals[idx++] = last; } else { while (idx < alen) vals[idx++] = 0; } fprintf(out, "DATAW %s(SB),\"", mod_mangle(c, d->str)); 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)); } } fputs("\"\n", out); 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); if (is_struct) continue; if (is_array) continue; if (r->kind != N_NIL && !empty_str) continue; } emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), 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)) continue; /* not a fold-able literal constant */ fprintf(out, "DATA %s(SB),\"", mod_mangle(c, d->str)); 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); } (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) != 16) 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; }