Both stages materialise a float literal as a 64-bit double in X0 (MOVQ bits -> MOVSD), ignoring the node type. For an f32-typed literal the downstream MOVSS reads the low 4 bytes of that double — garbage (0.0f for clean values, which is why 0.0 survived the bug and 951's f32 rows, which only assert NaN ordering, never caught it). Append CVTSD2SS X0,X0 at both literal sites (N_FLOATLIT + the float-typed N_INTLIT arm) when the node is f32-typed, so the value reaches X0 as a true single. Mirror in cgenexpr.ww (rule-10) and regen the w6c/wwdump combined.ww embeds. Covers literals carrying an explicit f32 type (the `f32` suffix and the no-decimal `8f32` N_INTLIT arm). An un-suffixed literal in an f32 context (`let x: f32 = 1.0`) stays ty_untyped_float through the checker, so its node is never f32-typed and this branch can't fire — that needs fold-2 (checker untyped-float -> f32 lowering, both checkers). 964_f32lit_run: cstage run + cs==ww byte-id probe over concrete f32 values (suffixed), the hole 951 leaves open.
8229 lines
280 KiB
C
8229 lines
280 KiB
C
/*
|
|
* cgen.c — typed AST → Prog list, expressed as Plan 9-flavoured
|
|
* amd64 assembly text. This is the simplest thing that works:
|
|
*
|
|
* - Every function gets a stack frame sized for spilled locals + a
|
|
* 16-byte alignment pad.
|
|
* - Expressions are evaluated stack-machine style: result in AX,
|
|
* intermediate stuff pushed on the hardware stack via PUSHQ AX.
|
|
* - The first six integer args go in DI, SI, DX, CX, R8, R9
|
|
* (SysV amd64 ABI). We don't yet handle struct-by-value or
|
|
* floats; floats and slices are deferred.
|
|
*
|
|
* Calling our own functions: emit CALL <name>(SB), let w6a/w6l resolve.
|
|
* Calling C externs: same — extern symbols are just unresolved CALLs.
|
|
*/
|
|
#include "gc.h"
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
static const int sysv_argregs[] = { D_DI, D_SI, D_DX, D_CX, D_R8, D_R9 };
|
|
static const int sysv_fargregs[] = { D_X0, D_X1, D_X2, D_X3, D_X4, D_X5, D_X6, D_X7 };
|
|
|
|
/* per-fn cursor, reset before each cgfn: counts how many 8-byte
|
|
* stack-arg slots above BP have been claimed. */
|
|
int cg_stack_arg_cursor;
|
|
|
|
/* return type of the current function, set by cgfn before walking
|
|
* the body. Drives tagged-union return construction and the `?` /
|
|
* `!` propagation paths. */
|
|
static Type *cg_ret_type;
|
|
/* Pointer to the current function's frame size accumulator. cgexpr
|
|
* needs this to allocate scratch slots (e.g. match bindings) without
|
|
* threading it through every signature. */
|
|
static int *cg_frame;
|
|
/* Per-fn @retscr offset (single-slot SSoT, task #14). Returns are
|
|
* terminal: at most one return path fires per call, so all retscr
|
|
* uses share one slot. Mirrors wwstage's `@retscr` convention
|
|
* (cgen.ww localadd '@'-prefix dedup; #38 ratified single-slot
|
|
* semantics for synthetic scratches). 0 means "not yet allocated";
|
|
* negative offsets returned by local_alloc are the live value. */
|
|
static int cg_retscr;
|
|
/* Per-fn @-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;
|
|
}
|
|
|
|
/* type_chase_named — walk the TY_NAMED.under chain to the deepest non-
|
|
* named type. Chain-of-aliases (#22): `type b = a; type a = struct;`
|
|
* stacks two TY_NAMED layers — a single peel leaves `t` pointing at
|
|
* the inner alias (still TY_NAMED), so kind-gated arms (TY_STRUCT,
|
|
* TY_SLICE, TY_TAGGED, TY_PTR) miss and the codegen silently falls
|
|
* through to a scalar shape. Mirror of wwstage's structlookupchain. */
|
|
static Type *
|
|
type_chase_named(Type *t)
|
|
{
|
|
while (t && t->kind == TY_NAMED) t = t->under;
|
|
return t;
|
|
}
|
|
|
|
/* cg_sret_retsize — 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)
|
|
{
|
|
rt = type_chase_named(rt);
|
|
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);
|
|
}
|
|
|
|
/* #83: positional tuple register-return ABI. Tuple elements ride
|
|
* consecutive eightbytes over tuple_rseq[]; a slice/str rides its 3-word
|
|
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8, ty_str->size SSoT), a
|
|
* scalar rides 1. SEND (N_RETURN) and RECEIVE (N_MLET/N_MASSIGN) walk the
|
|
* SAME widths so element->register agrees — mirrors harec's
|
|
* create_unpack_bindings element walk (ref/harec/src/check.c:1354-1416). */
|
|
static const int tuple_rseq[] = { D_AX, D_DX, D_CX, D_R8 };
|
|
|
|
static int
|
|
tuple_ebytes(int wide)
|
|
{
|
|
return wide ? (int)(ty_str->size / 8) : 1;
|
|
}
|
|
|
|
static int
|
|
type_isf32(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_F32;
|
|
}
|
|
|
|
static int
|
|
node_isf32(Node *n)
|
|
{
|
|
return n && type_isf32(n->type);
|
|
}
|
|
|
|
/* fld_isfloat — true iff f's underlying type is f32, f64, or
|
|
* untyped_float. The cgen passes float values in X0 (via MOVSD/MOVSS),
|
|
* integer/ptr values in AX (via MOVQ). Without this check, a field
|
|
* store/load on an f64 slot runs through AX and the bits never reach
|
|
* the SSE side — see the vfloat / L.curfval traps documented in
|
|
* examples/lisp/CLAUDE.md.
|
|
*
|
|
* TY_UNTYPED_FLOAT defaults to f64 (no TY_UNTYPED_F32 exists). Every
|
|
* field/element/pointee caller passes a declared type that is never
|
|
* UNTYPED — adding the case is a no-op for them. The variant-widen
|
|
* call site (cg_widen_tagged_store) is the only one passing an
|
|
* expression type, where `let _: (i64|f64) = -2.5;` arrives with
|
|
* src->type = ty_untyped_float (cunop returns the operand type for
|
|
* TK_MINUS, untyped_float for an untyped float literal). The earlier
|
|
* narrow predicate dropped the payload via the AX scalar fallback —
|
|
* matches cg_isfloat's acceptance set now.
|
|
*
|
|
* Sets *isf32 to 1 for f32, 0 for f64 / untyped_float. */
|
|
static int
|
|
fld_isfloat(Type *t, int *isf32)
|
|
{
|
|
if (isf32) *isf32 = 0;
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_F64) return 1;
|
|
if (t->kind == TY_UNTYPED_FLOAT) return 1;
|
|
if (t->kind == TY_F32) { if (isf32) *isf32 = 1; return 1; }
|
|
return 0;
|
|
}
|
|
|
|
/* 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 <module>.<name> at emission
|
|
* time so two modules can each define the same fn leaf — including
|
|
* exported ones (lib/os and lib/io both ship `read`/`write`/`close`)
|
|
* — without colliding at link time. Non-fn decls (let/def/type) keep
|
|
* the older "non-exported only" rule: their export-side namespace is
|
|
* the user-facing data ABI and mangling them changes the surface. */
|
|
typedef struct Mod Mod;
|
|
struct Mod {
|
|
const char *name;
|
|
const char *module;
|
|
Mod *next;
|
|
};
|
|
static Mod *mod_map;
|
|
|
|
/* Top-level `let` map. Populated alongside mod_map; consulted by the
|
|
* N_IDENT store path and the &-of path to route reads/writes through
|
|
* a RIP-relative reference rather than dropping them as the (pre-
|
|
* writable-.data) compiler did. emit_lets emits a DATAW for each. */
|
|
typedef struct LetVar LetVar;
|
|
struct LetVar {
|
|
const char *name;
|
|
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_SIZE:
|
|
case TY_PTR:
|
|
return 8;
|
|
case TY_F32:
|
|
return 4; /* MOVSS loads/stores 4B via LEAQ+indir. */
|
|
case TY_F64:
|
|
return 8; /* MOVSD loads/stores 8B via LEAQ+indir. */
|
|
case TY_STR:
|
|
case TY_SLICE:
|
|
return (int)u->size; /* #43: ty_str / ty_slice SSoT. */
|
|
case TY_STRUCT:
|
|
return (int)u->size; /* zero-init only; field reads/
|
|
* scalar-field writes only. */
|
|
case TY_ARRAY:
|
|
return (int)u->size; /* zero-init only; element
|
|
* loads/stores via cgindex. Mirror
|
|
* of selfhost letemitsize's
|
|
* N_TARRAY branch. */
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Is the unwrapped type a str? Used by the load/store paths so the
|
|
* (AX, BX) pair convention is preserved for str globals, mirroring
|
|
* what we already do for str locals. */
|
|
static int
|
|
let_isstr(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_STR;
|
|
}
|
|
|
|
/* Is the unwrapped type a slice? Slice globals flow as the (AX, BX,
|
|
* CX) triple — same as the local ABI. */
|
|
static int
|
|
let_isslice(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_SLICE;
|
|
}
|
|
|
|
/* Is the unwrapped type a struct? Struct globals only support field
|
|
* access (read + plain `=` write for scalar fields). Whole-struct
|
|
* by-value flow through expressions isn't wired. */
|
|
static int
|
|
let_isstruct(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_STRUCT;
|
|
}
|
|
|
|
/* Is the unwrapped type a fixed-length array? Array globals are
|
|
* zero-init DATAW slots; cgindex addresses them as LEAQ name(SB)
|
|
* and lets the element load/store run as usual. */
|
|
static int
|
|
let_isarray(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_ARRAY;
|
|
}
|
|
|
|
/* Is the unwrapped type a float (f32 or f64)? Float globals flow
|
|
* through X0 — load/store goes LEAQ name(SB),CX → MOVSS/MOVSD via the
|
|
* indirect, since the asm has no D_EXTERN form for SSE moves yet. */
|
|
static int
|
|
let_isfloat(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && (u->kind == TY_F32 || u->kind == TY_F64);
|
|
}
|
|
|
|
/* Returns the unwrapped Type — handy when we need to walk struct
|
|
* fields. NULL if t is NULL or unresolved. */
|
|
static Type *
|
|
type_unwrap(Type *t)
|
|
{
|
|
if (t == NULL) return NULL;
|
|
return (t->kind == TY_NAMED) ? t->under : t;
|
|
}
|
|
|
|
/* Element-effective type for indexing. For `*[N]T` we drill through
|
|
* the pointer to the underlying array so esz/esub reflect T, not the
|
|
* whole-array pointee. For everything else returns t unchanged. */
|
|
static Type *
|
|
idx_eff(Type *t)
|
|
{
|
|
if (t == NULL) return NULL;
|
|
Type *u = type_unwrap(t);
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *p = type_unwrap(u->sub);
|
|
if (p && p->kind == TY_ARRAY) return p;
|
|
}
|
|
return u;
|
|
}
|
|
|
|
static int
|
|
decl_has_ffisym(Node *d)
|
|
{
|
|
for (Node *a = d->attr; a; a = a->next) {
|
|
if (a->kind != N_ATTR) continue;
|
|
if (strcmp(a->str, "symbol") == 0) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export` for fns.
|
|
* Both stages must match exactly — ww2/ww3/ww4 byte-identity depends on it. */
|
|
static void
|
|
mod_collect(Cg *c, Node *file)
|
|
{
|
|
mod_map = NULL;
|
|
if (file == NULL) return;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
int isfn = (d->kind == N_FNDECL);
|
|
int track = isfn || (d->kind == N_TYPEDECL)
|
|
|| (d->kind == N_DEF) || (d->kind == N_LET);
|
|
if (!track) continue;
|
|
/* Non-fn decls (let/def/type) still skip exported entries —
|
|
* their export-side namespace is the user-facing data ABI
|
|
* and mangling them changes the surface. Fns mangle
|
|
* unconditionally so cross-module same-leaf exports
|
|
* (os.read vs io.read) coexist at link time. */
|
|
if (!isfn && d->export) continue;
|
|
if (d->module == NULL || d->module[0] == '\0') continue;
|
|
if (decl_has_ffisym(d)) continue;
|
|
/* `main` is the linker entry-point convention. Even when not
|
|
* marked `export`, it must keep its bare name so w6l can
|
|
* resolve `_start`'s `CALL main(SB)`. */
|
|
if (d->str && strcmp(d->str, "main") == 0) continue;
|
|
Mod *m = amalloc(c->a, sizeof *m);
|
|
m->name = d->str;
|
|
m->module = d->module;
|
|
m->next = mod_map;
|
|
mod_map = m;
|
|
}
|
|
}
|
|
|
|
/* Returns the originating module for a name, or NULL if the name
|
|
* isn't a registered private decl. By-name only — works for non-fn
|
|
* refs (let/def/type) where the mod_collect skip rule keeps each leaf
|
|
* unique across the program. Fn refs go through mod_lookup_for_fn
|
|
* since multiple modules can now export the same fn leaf. */
|
|
static const char *
|
|
mod_lookup(const char *name)
|
|
{
|
|
for (Mod *m = mod_map; m; m = m->next)
|
|
if (strcmp(m->name, name) == 0) return m->module;
|
|
return NULL;
|
|
}
|
|
|
|
/* Hint-aware variant for fn names. Walks mod_map looking for a
|
|
* (name, hint) pair; returns NULL if there's no leaf-name match at
|
|
* all, the hinted module if a match exists, or the first leaf match
|
|
* when the caller had no hint. The hint comes from AST shape:
|
|
* - N_DOT call `m.fn(...)`: hint = the SK_USE module ident's str.
|
|
* - bare N_IDENT call `fn(...)`: hint = c->cur_mod (current fn's
|
|
* module — bare names resolve same-module by ww's rules).
|
|
* Falling back to the first leaf match preserves the legacy single-
|
|
* owner shape for callers that don't (yet) thread a hint. */
|
|
static const char *
|
|
mod_lookup_for_fn(const char *name, const char *hint)
|
|
{
|
|
const char *first = NULL;
|
|
for (Mod *m = mod_map; m; m = m->next) {
|
|
if (strcmp(m->name, name) != 0) continue;
|
|
if (hint != NULL && m->module != NULL
|
|
&& strcmp(m->module, hint) == 0)
|
|
return m->module;
|
|
if (first == NULL) first = m->module;
|
|
}
|
|
return first;
|
|
}
|
|
|
|
/* 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 `<module>.<ident>` into a fresh arena buffer. */
|
|
static const char *
|
|
mod_join(Cg *c, const char *mod, const char *ident)
|
|
{
|
|
size_t mn = strlen(mod), in = strlen(ident);
|
|
char *buf = amalloc(c->a, mn + 1 + in + 1);
|
|
memcpy(buf, mod, mn);
|
|
buf[mn] = '.';
|
|
memcpy(buf + mn + 1, ident, in);
|
|
buf[mn + 1 + in] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
/* Mangle an AST identifier into its asm linker symbol:
|
|
* - @symbol("...") binding wins (return mapped name).
|
|
* - module-private decl → <module>.<name>.
|
|
* - else → name unchanged.
|
|
* Used at every CALL/MOVQ/LEAQ site that targets an AST name. Plain
|
|
* `asym(s)` still emits `s` verbatim — use it for strlit labels and
|
|
* hard-coded runtime symbols like "rt_streq". */
|
|
static const char *
|
|
mod_mangle(Cg *c, const char *ident)
|
|
{
|
|
const char *resolved = ffi_resolve(ident);
|
|
if (resolved != ident) return resolved;
|
|
const char *mod = mod_lookup(ident);
|
|
if (mod == NULL) return ident;
|
|
return mod_join(c, mod, ident);
|
|
}
|
|
|
|
/* Fn-flavoured mangle: same shape as mod_mangle but consults
|
|
* mod_lookup_for_fn so the right module wins when multiple modules
|
|
* register the same fn leaf. `hint` is the explicit module from a
|
|
* N_DOT call site (or c->cur_mod for bare-ident calls); pass NULL
|
|
* to get the legacy first-match-wins behaviour. */
|
|
static const char *
|
|
mod_mangle_fn(Cg *c, const char *ident, const char *hint)
|
|
{
|
|
const char *resolved = ffi_resolve(ident);
|
|
if (resolved != ident) return resolved;
|
|
const char *mod = mod_lookup_for_fn(ident, hint);
|
|
if (mod == NULL) return ident;
|
|
return mod_join(c, mod, ident);
|
|
}
|
|
|
|
/* Forward decl — masym below depends on asym defined further down. */
|
|
static Adr asym(const char *s);
|
|
|
|
static Adr
|
|
masym(Cg *c, const char *ident)
|
|
{
|
|
return asym(mod_mangle(c, ident));
|
|
}
|
|
|
|
/* Fn-name address builder. Use at every CALL/LEAQ site whose target
|
|
* is a top-level fn — passes the hint so cross-module same-leaf
|
|
* exports resolve to the right module. */
|
|
static Adr
|
|
mafn(Cg *c, const char *ident, const char *hint)
|
|
{
|
|
return asym(mod_mangle_fn(c, ident, hint));
|
|
}
|
|
|
|
void
|
|
cg_init(Cg *c, Arena *a)
|
|
{
|
|
memset(c, 0, sizeof *c);
|
|
c->a = a;
|
|
}
|
|
|
|
Prog *
|
|
newprog(Cg *c, int op)
|
|
{
|
|
Prog *p = amalloc(c->a, sizeof *p);
|
|
p->as = op;
|
|
return p;
|
|
}
|
|
|
|
void
|
|
emit(Cg *c, Prog *p)
|
|
{
|
|
if (c->head == NULL) c->head = p;
|
|
else c->tail->link = p;
|
|
c->tail = p;
|
|
}
|
|
|
|
static Adr
|
|
areg(int r)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = r;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
aimm(long long v)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_CONST;
|
|
a.offset = v;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
amem(int r, long long off)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_INDIR;
|
|
a.reg = r;
|
|
a.offset = off;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
asym(const char *s)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_EXTERN;
|
|
a.sym = s;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
abranch(const char *s)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_BRANCH;
|
|
a.sym = s;
|
|
return a;
|
|
}
|
|
|
|
static char *
|
|
mklabel(Cg *c, const char *prefix)
|
|
{
|
|
/* Module-qualified to avoid cross-module same-leaf collisions
|
|
* (task #13). w6a accepts '.' in label-cont (lex.c:18). */
|
|
return aprintf(c->a, "%s%s%s_%s_%d",
|
|
c->cur_mod ? c->cur_mod : "",
|
|
c->cur_mod ? "." : "",
|
|
c->fnname ? c->fnname : "_", prefix, c->labelseq++);
|
|
}
|
|
|
|
static void
|
|
ins2(Cg *c, int op, Adr from, Adr to)
|
|
{
|
|
Prog *p = newprog(c, op);
|
|
p->from = from;
|
|
p->to = to;
|
|
emit(c, p);
|
|
}
|
|
|
|
static void
|
|
ins1(Cg *c, int op, Adr to)
|
|
{
|
|
Prog *p = newprog(c, op);
|
|
p->to = to;
|
|
emit(c, p);
|
|
}
|
|
|
|
static void
|
|
ins0(Cg *c, int op)
|
|
{
|
|
emit(c, newprog(c, op));
|
|
}
|
|
|
|
static void
|
|
label(Cg *c, const char *s)
|
|
{
|
|
Prog *p = newprog(c, A_NOP);
|
|
p->label = s;
|
|
emit(c, p);
|
|
}
|
|
|
|
/* cgslicehdr — load the 24B slice/str header at `base`+0 into the
|
|
* (AX=ptr, BX=len, CX=cap) triple. `base` holds the element address;
|
|
* the load that targets `base` destroys it, so that word is emitted
|
|
* LAST. Order otherwise mirrors the slice-FIELD arm (len, cap, ptr).
|
|
* Shared by the N_INDEX str-element arms (caller does the kind-gate)
|
|
* and, later, the typeassert str-variant leaf (#9). */
|
|
static void
|
|
cgslicehdr(Cg *c, int base)
|
|
{
|
|
if (base != D_BX) ins2(c, A_MOVQ, amem(base, 8), areg(D_BX));
|
|
if (base != D_CX) ins2(c, A_MOVQ, amem(base, 16), areg(D_CX));
|
|
if (base != D_AX) ins2(c, A_MOVQ, amem(base, 0), areg(D_AX));
|
|
if (base == D_BX) ins2(c, A_MOVQ, amem(base, 8), areg(D_BX));
|
|
else if (base == D_CX) ins2(c, A_MOVQ, amem(base, 16), areg(D_CX));
|
|
else if (base == D_AX) ins2(c, A_MOVQ, amem(base, 0), areg(D_AX));
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* per-fn local table: name → stack offset (positive = below FP) */
|
|
|
|
typedef struct Local Local;
|
|
struct Local {
|
|
const char *name;
|
|
int off; /* relative to BP; negative for locals */
|
|
Local *next;
|
|
};
|
|
|
|
/* localoff — push a fresh stack slot for this binding and return its
|
|
* BP offset. Never dedups by name (post-#27): two `let a: T` in disjoint
|
|
* scopes within one fn must each get their own slot, sized to their own
|
|
* declared T. Pre-fix the dedup loop returned the first-allocated slot
|
|
* regardless of the new declaration's size, so an outer `let a: [128]u8`
|
|
* after an inner `let a: i64` would collapse onto the 8B slot and
|
|
* `a[127]` would land at +119(BP), past the saved RIP, into the
|
|
* caller's frame. localfind walks from the head, so the most recent
|
|
* binding still wins lookups inside its scope. */
|
|
static int
|
|
localoff(Cg *c, Local **head, const char *name, int size, int *frame)
|
|
{
|
|
int al = 8;
|
|
*frame = (*frame + size + al - 1) & ~(al - 1);
|
|
int off = -*frame;
|
|
Local *l = amalloc(c->a, sizeof *l);
|
|
l->name = name;
|
|
l->off = off;
|
|
l->next = *head;
|
|
*head = l;
|
|
return off;
|
|
}
|
|
|
|
/* local_alloc — synonym for localoff. Pre-#27 localoff deduped by name
|
|
* and local_alloc was the always-fresh escape hatch (match-arm bindings,
|
|
* synthetic scratch slots). Post-#27 localoff is also always-fresh, so
|
|
* the two are functionally identical; both names are kept so the call
|
|
* sites read intentfully (let-decl vs scratch). */
|
|
static int
|
|
local_alloc(Cg *c, Local **head, const char *name, int size, int *frame)
|
|
{
|
|
int al = 8;
|
|
*frame = (*frame + size + al - 1) & ~(al - 1);
|
|
int off = -*frame;
|
|
Local *l = amalloc(c->a, sizeof *l);
|
|
l->name = name;
|
|
l->off = off;
|
|
l->next = *head;
|
|
*head = l;
|
|
return off;
|
|
}
|
|
|
|
static int
|
|
localfind(Local *head, const char *name)
|
|
{
|
|
for (Local *l = head; l; l = l->next)
|
|
if (strcmp(l->name, name) == 0) return l->off;
|
|
return 0; /* 0 = not found (caller must verify) */
|
|
}
|
|
|
|
/* cg_base_cap — load the capacity of a sub-slice's UNDERLYING storage
|
|
* into `dst` for the #20 cap = base_cap - lo formula (drew: harec
|
|
* eval.c:1017 slice cap-=start / eval.c:1024 array cap=length-start;
|
|
* ensure.ha:4-8 distinct capacity field). array [N]T -> N (literal);
|
|
* slice/str -> the .capacity word carried in the header at +16 (the
|
|
* +16 load mirrors the hi-default +8 length dispatch, but emitted
|
|
* unconditionally). Returns 0 when base_cap isn't cleanly available so
|
|
* the caller keeps the prior cap=len: a non-ident base (cgexpr already
|
|
* discarded its header cap; recomputing would re-evaluate a possibly
|
|
* side-effecting base -- #74, which also owns the pre-existing
|
|
* defaulted-hi len gap there), or a GLOBAL str base (wwstage cgslice
|
|
* has no global-str load, #73 -- matching it keeps the stages
|
|
* byte-identical rather than introducing a fresh divergence). */
|
|
static int
|
|
cg_base_cap(Cg *c, Node *base, Type *bu, Local *locals, int dst)
|
|
{
|
|
if (!base || base->kind != N_IDENT)
|
|
return 0;
|
|
if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_MOVQ, aimm((long long)bu->alen), areg(dst));
|
|
return 1;
|
|
}
|
|
if (bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) && let_islet(base->str);
|
|
if (isglobal && bu->kind == TY_STR)
|
|
return 0;
|
|
if (isglobal) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(dst));
|
|
ins2(c, A_MOVQ, amem(dst, 16), areg(dst));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 16), areg(dst));
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* expressions: result lands in AX. Returns 1 on success. */
|
|
|
|
static void cgexpr(Cg*, Node*, Local*);
|
|
static void cgstmt(Cg*, Node*, Local**, int*);
|
|
static void cg_widen_tagged_push(Cg*, Local**, Type*, Node*, int);
|
|
static void cg_widen_tagged_store(Cg*, Local**, Type*, Node*, int, int, int);
|
|
static void cg_widen_tag_remap(Cg*, Type*, Type*, int);
|
|
/* cg_structlit_fill modes — see helper docstring. */
|
|
enum {
|
|
DST_BP = 0,
|
|
DST_PTR_LOCAL = 1,
|
|
DST_GLOBAL = 2,
|
|
};
|
|
static void cg_structlit_fill(Cg*, Local**, Type*, Node*, int, int, const char*, int);
|
|
static void cg_structlit_fill_bp(Cg*, Local**, Type*, Node*, int);
|
|
|
|
static void
|
|
cgexpr_int(Cg *c, long long v)
|
|
{
|
|
ins2(c, A_MOVQ, aimm(v), areg(D_AX));
|
|
}
|
|
|
|
/* Materialise a float constant in X0: MOVQ the IEEE bits into AX, PUSH,
|
|
* MOVSD off the stack into X0. Shared by N_FLOATLIT and the f64/f32-typed
|
|
* N_INTLIT arm (#103 FACE X): a no-decimal `0f64`/`8f64` is an N_INTLIT
|
|
* carrying float TYPE, so it must reach X0 like a true float literal does
|
|
* — the integer-immediate path left the value stranded in AX, so an SSE
|
|
* compare/mul read a stale X0. */
|
|
static void
|
|
cgexpr_float(Cg *c, double val)
|
|
{
|
|
union { double d; u64 u; } x;
|
|
x.d = val;
|
|
ins2(c, A_MOVQ, aimm((long long)x.u), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVSD, amem(D_SP, 0), areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
}
|
|
|
|
/* cg_widen_tag_remap — when widening from one tagged union to another,
|
|
* rewrite the source's variant tag at BP+slot_off+0 to use the dst
|
|
* union's variant indices. No-op when src and dst index orders coincide.
|
|
*
|
|
* Mirrors Hare's tagged-subset assignment: a value of type (A|B) flows
|
|
* into (A|B|C) by re-tagging the discriminator to the position the
|
|
* variant occupies in the wider union. Both must already match by
|
|
* cg_variant_match — the checker enforces that.
|
|
*
|
|
* Emits a CMPQ-chain switch over the source tag because w6a has no
|
|
* CMOVQ encoding. The chain is linear in nvariants; in practice tagged
|
|
* unions are small. */
|
|
static void
|
|
cg_widen_tag_remap(Cg *c, Type *du, Type *su, int slot_off)
|
|
{
|
|
if (du == NULL || du->kind != TY_TAGGED) return;
|
|
if (su == NULL || su->kind != TY_TAGGED) return;
|
|
int identity = 1, idx = 0;
|
|
for (Tparam *p = su->params; p; p = p->next, idx++) {
|
|
int di = cg_tag_for_variant(du, p->type);
|
|
if (di < 0) di = 0;
|
|
if (di != idx) { identity = 0; break; }
|
|
}
|
|
if (identity) return;
|
|
const char *done = mklabel(c, "remap_done");
|
|
ins2(c, A_MOVQ, amem(D_BP, slot_off + 0), areg(D_AX));
|
|
idx = 0;
|
|
for (Tparam *p = su->params; p; p = p->next, idx++) {
|
|
const char *next = mklabel(c, "remap_next");
|
|
int di = cg_tag_for_variant(du, p->type);
|
|
if (di < 0) di = 0;
|
|
ins2(c, A_CMPQ, aimm(idx), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(next));
|
|
ins2(c, A_MOVQ, aimm(di), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0));
|
|
ins1(c, A_JMP, abranch(done));
|
|
label(c, next);
|
|
}
|
|
label(c, done);
|
|
}
|
|
|
|
/* cg_widen_tagged_store — write the tagged-union slot bytes for `src`
|
|
* into base_reg+slot_off, sized to `sz` (8 for nullable fold, else
|
|
* 16/24+). Used by call-site widening (via cg_widen_tagged_push) and
|
|
* by the let/assign/return/struct-field-init paths.
|
|
*
|
|
* base_reg picks the addressing root for every write:
|
|
* - D_BP: function-frame slot. The original layout — callers pass
|
|
* a BP-relative slot_off and the function writes directly.
|
|
* - else (e.g. D_BX for a *struct field, D_CX for a top-level
|
|
* struct field): pointer-rooted dst. cgexpr inside this function
|
|
* trashes every GPR, so we can't carry base_reg across — instead
|
|
* we route every write through a fresh BP-rooted scratch slot,
|
|
* reload base_reg from a temp spill at the end, and word-copy
|
|
* scratch → (base_reg, slot_off). Caller is responsible for
|
|
* loading base_reg with the dst address before the call; the
|
|
* function preserves it across cgexpr via the spill.
|
|
*
|
|
* Branches by source shape (tagged_arg_size > 0 source counts as a
|
|
* tagged subset — possibly with different variant indices):
|
|
* - nullable: dst is folded (*T|void); store pointer at +0.
|
|
* - tagged ident: byte-copy slot words then remap tag at +0.
|
|
* - tagged expression: cgexpr leaves AX=tag, DX=val0, [CX=val1] —
|
|
* spill into slot then remap.
|
|
* - struct ident: zero-fill, byte-copy struct words to +8.
|
|
* - struct literal: zero-fill, store each field at slot+8+field_off.
|
|
* - str: cgexpr leaves AX=ptr, BX=len.
|
|
* - scalar: cgexpr leaves AX; store at +8 with zero pad. */
|
|
static void
|
|
cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
|
|
int base_reg, int slot_off, int sz)
|
|
{
|
|
/* For pointer-rooted dst, materialise into a BP-rooted scratch
|
|
* slot — body writes via `amem(D_BP, write_off + k)` — then copy
|
|
* out. Spill base_reg first so cgexpr can clobber freely. */
|
|
int via_outer = (base_reg != D_BP);
|
|
int base_spill = 0;
|
|
int write_off = slot_off;
|
|
if (via_outer) {
|
|
if (cg_tagbase != 0) {
|
|
base_spill = cg_tagbase;
|
|
} else {
|
|
base_spill = local_alloc(c, locals_p, "@tagbase", 8,
|
|
cg_frame);
|
|
cg_tagbase = base_spill;
|
|
cg_tagbase_sz = 8;
|
|
}
|
|
ins2(c, A_MOVQ, areg(base_reg), amem(D_BP, base_spill));
|
|
if (cg_tagscr != 0) {
|
|
if (sz > cg_tagscr_sz)
|
|
fatal("cg_widen_tagged_store: @tagscr "
|
|
"cached sz %d, need %d (pinned offset "
|
|
"can't grow in place; rule 7 — #15/#26c)",
|
|
cg_tagscr_sz, sz);
|
|
write_off = cg_tagscr;
|
|
} else {
|
|
write_off = local_alloc(c, locals_p, "@tagscr", sz,
|
|
cg_frame);
|
|
cg_tagscr = write_off;
|
|
cg_tagscr_sz = sz;
|
|
}
|
|
/* Pre-zero so str/scalar branches (which leave high words
|
|
* untouched when sz exceeds the variant's footprint) still
|
|
* deliver a clean slot to the copy-out. */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst;
|
|
if (du == NULL || du->kind != TY_TAGGED) return;
|
|
if (du->nullable) {
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* `expr: TaggedAlias` where the cast's destination IS the union
|
|
* itself is a widening, not a re-interpret. cgexpr on the cast
|
|
* leaves the inner expression's register shape (str: AX=ptr,
|
|
* BX=len), not the tagged AX/DX/CX triple — so route through the
|
|
* concrete-variant branches below by peeling the cast. Casts to
|
|
* a concrete variant (`7: i32`) keep their type for proper tag
|
|
* lookup and fall through to the matching branch. */
|
|
if (src && src->kind == N_CAST && src->lhs) {
|
|
Type *castt = src->type;
|
|
Type *castu = (castt && castt->kind == TY_NAMED)
|
|
? castt->under : castt;
|
|
Type *innert = src->lhs->type;
|
|
Type *innu = (innert && innert->kind == TY_NAMED)
|
|
? innert->under : innert;
|
|
int cast_is_widen = (castu == du) ||
|
|
(castu && castu->kind == TY_TAGGED && type_eq(castt, dst));
|
|
int inner_is_tagged = innu && innu->kind == TY_TAGGED;
|
|
if (cast_is_widen && !inner_is_tagged) {
|
|
src = src->lhs;
|
|
}
|
|
}
|
|
Type *st = src ? src->type : NULL;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
/* Tagged → tagged subset: copy slot words then tag-remap. */
|
|
if (su && su->kind == TY_TAGGED) {
|
|
int ssz = (int)su->size;
|
|
if (src->kind == N_IDENT) {
|
|
int soff = localfind(*locals_p, src->str);
|
|
for (int k = 0; k < ssz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
} else {
|
|
/* Tagged source returned via the tagged-return ABI
|
|
* (AX=tag, DX=word0, CX=word1, R8=word2). The unused
|
|
* ABI words are zeroed by the producer (#18 cgreturn
|
|
* variant-widen) so the unconditional store here is
|
|
* safe even when the source variant has fewer payload
|
|
* words than the dst slot. */
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 0));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, write_off + 8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, write_off + 16));
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, write_off + 24));
|
|
}
|
|
if (ssz < sz) {
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = ssz; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
cg_widen_tag_remap(c, du, su, write_off);
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* Struct payload: zero the whole slot, then write fields/words
|
|
* at slot+8+ — keeping the tag word at slot+0 from the zero-fill,
|
|
* then patch it with the variant tag. */
|
|
if (su && su->kind == TY_STRUCT) {
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
int tag = cg_tag_for_variant(du, st);
|
|
if (src->kind == N_IDENT) {
|
|
int soff = localfind(*locals_p, src->str);
|
|
int ssz = (int)su->size;
|
|
int k = 0;
|
|
while (k + 8 <= ssz) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + k));
|
|
k += 8;
|
|
}
|
|
if (k < ssz) {
|
|
/* Tail word: load with the right width to
|
|
* avoid stepping past the source slot. The
|
|
* zero-fill above means trailing slop is
|
|
* already clean. */
|
|
int tail = ssz - k;
|
|
int lop = (tail == 4) ? A_MOVL :
|
|
(tail == 1 ? A_MOVB : A_MOVQ);
|
|
ins2(c, lop,
|
|
amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, lop, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + k));
|
|
}
|
|
} else if (src->kind == N_STRUCTLIT) {
|
|
for (Node *f = src->list; f; f = f->next) {
|
|
u64 foff = 0;
|
|
int fsz = 8;
|
|
Type *ftype = NULL;
|
|
for (Tfield *fl = su->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, f->str) == 0) {
|
|
foff = fl->offset;
|
|
fsz = (int)(fl->type ? fl->type->size : 8);
|
|
ftype = fl->type;
|
|
break;
|
|
}
|
|
}
|
|
cgexpr(c, f->lhs, *locals_p);
|
|
int sl_isf32 = 0;
|
|
if (fld_isfloat(ftype, &sl_isf32)) {
|
|
int mov = sl_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP, write_off + 8 + (int)foff));
|
|
continue;
|
|
}
|
|
Type *fu = (ftype && ftype->kind == TY_NAMED)
|
|
? ftype->under : ftype;
|
|
if (fu && fu->kind == TY_STR) {
|
|
/* str IS []u8: 3-word field (ptr,len,cap)
|
|
* from cgexpr's AX/BX/CX (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, write_off + 8 + (int)foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, write_off + 8 + (int)foff + 16));
|
|
continue;
|
|
}
|
|
int op = A_MOVQ;
|
|
if (fsz == 1) op = A_MOVB;
|
|
else if (fsz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + (int)foff));
|
|
}
|
|
}
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* str IS []u8 — same 32B payload as a slice: cgexpr leaves
|
|
* (AX=ptr, BX=len, CX=cap); slot layout tag@+0, ptr@+8, len@+16,
|
|
* cap@+24, destination slot >= 32B. str folds onto the slice arm
|
|
* (#1/Phase 3 collapse). */
|
|
if (type_isslice(st) || (su && su->kind == TY_SLICE) ||
|
|
type_isstr(st) || (su && su->kind == TY_STR)) {
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 16));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, write_off + 24));
|
|
int tag = cg_tag_for_variant(du, st);
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* Float arm: cgexpr on an f64/f32 source leaves the bit pattern in
|
|
* X0 only — the AX-store below would silently write whatever was
|
|
* loaded into AX before the SSE conversion. Literal `1.0` works by
|
|
* coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX
|
|
* before MOVSD'ing into X0); every runtime f64 shape (cast, call,
|
|
* unary, ident, struct-field load) needs the explicit MOVSD path.
|
|
* Same kind-specific dispatch as the str/slice branches above and
|
|
* the structlit field-flow at the top of this function. */
|
|
int wid_isf32 = 0;
|
|
if (fld_isfloat(st, &wid_isf32)) {
|
|
int mov = wid_isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, mov, areg(D_X0), amem(D_BP, write_off + 8));
|
|
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)) {
|
|
/* str IS []u8: slot 32 [+0]=tag, [+8]=ptr, [+16]=len,
|
|
* [+24]=cap — same shape as the slice arm below. Push
|
|
* cap, len, ptr, tag (high→low so pop drains tag first)
|
|
* (#1/Phase 3). */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
|
|
return;
|
|
}
|
|
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
|
|
/* slot 32: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap. */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
|
|
return;
|
|
}
|
|
/* Scalar / pointer variant. Pad with zero high words when
|
|
* the slot has room for a wider variant. */
|
|
int nwords = sz / 8;
|
|
for (int k = nwords - 1; k >= 2; k--) {
|
|
ins2(c, A_XORQ, areg(D_DX), areg(D_DX));
|
|
ins1(c, A_PUSHQ, areg(D_DX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */
|
|
return;
|
|
}
|
|
int scr;
|
|
if (cg_tagscr != 0) {
|
|
if (sz > cg_tagscr_sz)
|
|
fatal("cg_widen_tagged_push: @tagscr cached sz %d, "
|
|
"need %d (pinned offset can't grow in place; "
|
|
"rule 7 — #15/#26c)",
|
|
cg_tagscr_sz, sz);
|
|
scr = cg_tagscr;
|
|
} else {
|
|
scr = local_alloc(c, locals_p, "@tagscr", sz, cg_frame);
|
|
cg_tagscr = scr;
|
|
cg_tagscr_sz = sz;
|
|
}
|
|
/* Zero the scratch slot first so any pad word the store path
|
|
* leaves untouched (struct payload shorter than the slot's value
|
|
* area) reads as 0 on the callee. The store path then writes the
|
|
* variant bytes over the zeros. */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k));
|
|
cg_widen_tagged_store(c, locals_p, dst, src, D_BP, scr, sz);
|
|
int nwords = sz / 8;
|
|
for (int k = nwords - 1; k >= 0; k--) {
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + k * 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
}
|
|
|
|
/* cg_structlit_fill — fill a struct-typed slot from an N_STRUCTLIT
|
|
* value into one of three destination flavors. Used by N_LET, N_ASSIGN
|
|
* N_IDENT-lhs, N_RETURN N_STRUCTLIT (BP-rel), and N_ASSIGN N_DOT-lhs
|
|
* (BP-rel / via *struct local / via struct global) at single-dot and
|
|
* chained-dot sites.
|
|
*
|
|
* Destination modes:
|
|
* DST_BP — base = BP, no reload. Stores at disp+i(BP).
|
|
* srcoff/name unused.
|
|
* DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before the
|
|
* ELLIPSIS zero-fill loop and before EVERY field
|
|
* store (cgexpr clobbers BX between fields).
|
|
* Stores at disp+i(BX). name unused.
|
|
* DST_GLOBAL — base = BX, reloaded via `LEAQ name(SB), BX` with
|
|
* the same reload cadence as DST_PTR_LOCAL.
|
|
* srcoff unused.
|
|
*
|
|
* Param semantics (locked in here so the recursion contract is clear):
|
|
* - `disp` is the per-recursion accumulator — grows by `foff` as
|
|
* we descend into a nested struct-typed structlit field.
|
|
* - `srcoff` (DST_PTR_LOCAL) and `name` (DST_GLOBAL) are *constant*
|
|
* across the whole call tree — they identify the root dst, which
|
|
* doesn't change with depth. Recursion passes them through.
|
|
*
|
|
* Why a helper? The inline field-walk at each call site previously
|
|
* did `cgexpr(f->lhs); store AX (sized)`. For struct-typed fields
|
|
* whose value is itself a nested N_STRUCTLIT, cgexpr has no whole-
|
|
* struct-in-register convention — it lands AX = first qword and the
|
|
* trailing bytes silently stay zero (or stack garbage). #17 fixed
|
|
* the BP-rel sites; #18 extends the same recursion to the four
|
|
* N_ASSIGN N_DOT-lhs structlit walks (single-dot via_ptr/global/
|
|
* local + chained depth>=2).
|
|
*
|
|
* The non-BP modes emit a redundant BX reload at the start of each
|
|
* recursive nested zero-fill / each recursive scalar store — this is
|
|
* correctness-by-construction (BX is always freshly loaded right
|
|
* before use), and the redundancy only fires on the nested-STRUCTLIT
|
|
* shapes that didn't compile before. Byte-identity for the no-nested
|
|
* case (the only shape selfhost source uses today) is preserved
|
|
* because the existing inline code's reload-before-each-store pattern
|
|
* matches the helper's per-store reload exactly.
|
|
*
|
|
* The scalar store dispatch stays at the explicit {1->MOVB, 4->MOVL,
|
|
* else MOVQ} shape (not fieldstoreop, which emits MOVW for fsz==2) to
|
|
* stay byte-identical with cstage pending task #13. */
|
|
static void
|
|
cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit,
|
|
int mode, int srcoff, const char *name, int disp)
|
|
{
|
|
int sz = (int)lu->size;
|
|
int base_reg = (mode == DST_BP) ? D_BP : D_BX;
|
|
if (lit->op == TK_ELLIPSIS) {
|
|
/* `..., ...` autofill — zero the entire slot first so
|
|
* unmentioned fields read as 0. Sized stores: 8/4/1. For
|
|
* non-BP modes, reload BX once before the loop (cgexpr-free
|
|
* region between iterations, so one reload is enough). */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX));
|
|
else if (mode == DST_GLOBAL)
|
|
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
|
|
int zi = 0;
|
|
while (zi + 8 <= sz) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(base_reg, disp + zi));
|
|
zi += 8;
|
|
}
|
|
while (zi + 4 <= sz) {
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(base_reg, disp + zi));
|
|
zi += 4;
|
|
}
|
|
while (zi < sz) {
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(base_reg, disp + zi));
|
|
zi += 1;
|
|
}
|
|
}
|
|
for (Node *f = lit->list; f; f = f->next) {
|
|
u64 foff = 0;
|
|
int fsz = 8;
|
|
Type *ft = NULL;
|
|
for (Tfield *fl = lu->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, f->str) == 0) {
|
|
foff = fl->offset;
|
|
fsz = (int)(fl->type ? fl->type->size : 8);
|
|
ft = fl->type;
|
|
break;
|
|
}
|
|
}
|
|
Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft;
|
|
if (fu && fu->kind == TY_TAGGED) {
|
|
/* Tagged store: reload BX first (if non-BP) so the
|
|
* widener sees a valid base reg. The widener itself
|
|
* preserves base_reg through its internal cgexpr. */
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff),
|
|
areg(D_BX));
|
|
else if (mode == DST_GLOBAL)
|
|
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
|
|
cg_widen_tagged_store(c, locals_p, fu, f->lhs,
|
|
base_reg, disp + (int)foff, (int)fu->size);
|
|
continue;
|
|
}
|
|
/* Nested struct-typed structlit value: recurse at the
|
|
* field's offset so all inner fields land. Pre-#17/#18 the
|
|
* cgexpr-then-store below would land AX = first qword and
|
|
* the rest silently stayed zero. */
|
|
if (fu && fu->kind == TY_STRUCT
|
|
&& f->lhs && f->lhs->kind == N_STRUCTLIT) {
|
|
cg_structlit_fill(c, locals_p, fu, f->lhs,
|
|
mode, srcoff, name, disp + (int)foff);
|
|
continue;
|
|
}
|
|
/* Nested struct-typed CALL value (#20). cgexpr leaves
|
|
* AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23] per
|
|
* #4's cgreturn ABI. Pre-#20 the cgexpr-then-AX-store
|
|
* fallthrough below silently dropped past the first
|
|
* qword for any fsz > 8 (only AX got stored).
|
|
*
|
|
* Sized stores: MOVQ for full 8B chunks plus a sized tail
|
|
* (MOVL/MOVW/MOVB) by `tail = fsz%8`. Mirrors #4's receive
|
|
* shape at the N_LET / N_ASSIGN call-rhs sites; the
|
|
* MOVW-for-tail==2 emission only fires on shapes that
|
|
* didn't compile before, so no #13 byte-identity concern.
|
|
*
|
|
* Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches #4's
|
|
* cgreturn ABI: >24B falls through (sret deferred);
|
|
* fsz%8 ∈ {3,5,6,7} would need shift-store and is also
|
|
* unsupported by #4 — falls through to the existing
|
|
* AX-only wrongness (consistent, tracked as follow-up).
|
|
*
|
|
* INVARIANT: between cgexpr(N_CALL) and the AX/DX/CX
|
|
* stores below, NO instruction may touch AX/DX/CX. The
|
|
* BX reload (MOVQ/LEAQ) is safe; any other emission
|
|
* added here will silently corrupt the return value. */
|
|
if (fu && fu->kind == TY_STRUCT
|
|
&& f->lhs && f->lhs->kind == N_CALL
|
|
&& fsz <= 24
|
|
&& (fsz % 8 == 0 || fsz % 8 == 1
|
|
|| fsz % 8 == 2 || fsz % 8 == 4)) {
|
|
cgexpr(c, f->lhs, *locals_p);
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff),
|
|
areg(D_BX));
|
|
else if (mode == DST_GLOBAL)
|
|
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = fsz / 8;
|
|
int tail = fsz % 8;
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(base_reg,
|
|
disp + (int)foff + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW : A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(base_reg,
|
|
disp + (int)foff + full * 8));
|
|
}
|
|
continue;
|
|
}
|
|
/* str IS []u8: 3-word field (ptr,len,cap). cgexpr leaves
|
|
* AX/BX/CX; for non-BP modes the dst base goes in DX to dodge
|
|
* BX=len / CX=cap (the generic store below reloads BX, which
|
|
* would clobber len) (#1/Phase 3). */
|
|
if (fu && fu->kind == TY_STR) {
|
|
cgexpr(c, f->lhs, *locals_p);
|
|
if (mode == DST_BP) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, disp + (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, disp + (int)foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, disp + (int)foff + 16));
|
|
} else {
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff),
|
|
areg(D_DX));
|
|
else
|
|
ins2(c, A_LEAQ, masym(c, name),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_DX, disp + (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_DX, disp + (int)foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_DX, disp + (int)foff + 16));
|
|
}
|
|
continue;
|
|
}
|
|
cgexpr(c, f->lhs, *locals_p);
|
|
/* For non-BP modes, cgexpr just clobbered BX; reload it
|
|
* before the store. */
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX));
|
|
else if (mode == DST_GLOBAL)
|
|
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
|
|
int sl_isf32 = 0;
|
|
if (fld_isfloat(ft, &sl_isf32)) {
|
|
int mov = sl_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(base_reg, disp + (int)foff));
|
|
continue;
|
|
}
|
|
int op = A_MOVQ;
|
|
if (fsz == 1) op = A_MOVB;
|
|
else if (fsz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX),
|
|
amem(base_reg, disp + (int)foff));
|
|
}
|
|
}
|
|
|
|
/* Thin wrapper preserving the BP-rel call shape used by N_LET,
|
|
* N_ASSIGN N_IDENT-lhs, and N_RETURN. Byte-identical to the pre-#18
|
|
* helper. */
|
|
static void
|
|
cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off)
|
|
{
|
|
cg_structlit_fill(c, locals_p, lu, lit, DST_BP, 0, NULL, bp_off);
|
|
}
|
|
|
|
static void
|
|
cgexpr(Cg *c, Node *n, Local *locals)
|
|
{
|
|
if (n == NULL) {
|
|
cgexpr_int(c, 0);
|
|
return;
|
|
}
|
|
switch (n->kind) {
|
|
case N_INTLIT:
|
|
case N_RUNELIT:
|
|
if (node_isfloat(n)) {
|
|
cgexpr_float(c, (double)(long long)n->uval);
|
|
/* #104: cgexpr_float materialises a DOUBLE in X0; an
|
|
* f32-typed literal must narrow with hardware single-
|
|
* rounding so the downstream MOVSS reads a true single. */
|
|
if (node_isf32(n))
|
|
ins2(c, A_CVTSD2SS, areg(D_X0), areg(D_X0));
|
|
break;
|
|
}
|
|
cgexpr_int(c, (long long)n->uval);
|
|
break;
|
|
case N_FLOATLIT:
|
|
cgexpr_float(c, n->fval);
|
|
/* #104: narrow the double in X0 to single for an f32 literal. */
|
|
if (node_isf32(n))
|
|
ins2(c, A_CVTSD2SS, areg(D_X0), areg(D_X0));
|
|
break;
|
|
case N_STRLIT: {
|
|
/* str IS []u8: the (ptr, len, cap) triple — ptr in AX, len in
|
|
* BX, cap in CX. A static literal has no spare storage, so
|
|
* cap = len (#1/Phase 3, task (b)). */
|
|
const char *lab = intern_strlit(c, n->str, n->strlen);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_BX));
|
|
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_CX));
|
|
break;
|
|
}
|
|
case N_TRUE: cgexpr_int(c, 1); break;
|
|
case N_FALSE:
|
|
case N_NIL:
|
|
case N_VOIDLIT: cgexpr_int(c, 0); break;
|
|
case N_IDENT: {
|
|
int off = localfind(locals, n->str);
|
|
if (off != 0) {
|
|
if (node_isfloat(n)) {
|
|
int op = op_for(n, A_MOVSD, A_MOVSS);
|
|
ins2(c, op, amem(D_BP, off), areg(D_X0));
|
|
} else if (node_isstr(n)) {
|
|
/* str IS []u8: flow as (AX=ptr, BX=len, CX=cap),
|
|
* mirroring the slice local load below (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
|
|
} else if (node_isslice(n)) {
|
|
/* slice values flow as (AX=ptr, BX=len, CX=cap)
|
|
* — mirror the global-slice load so a slice
|
|
* local can be reassigned, returned, or copied
|
|
* with the same triple convention. */
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
|
|
} else {
|
|
ins2(c, localloadop(n->type),
|
|
amem(D_BP, off), areg(D_AX));
|
|
}
|
|
} else {
|
|
/* Non-local: function symbols load by address (LEAQ),
|
|
* str-typed `def`s expand to (ptr, len) of the literal,
|
|
* other globals (def constants) load by value (MOVQ). */
|
|
Type *t = n->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (u && u->kind == TY_FN) {
|
|
/* Take the address of a function. Apply
|
|
* @symbol resolution so taking the address
|
|
* of a body-less FFI binding yields the C
|
|
* symbol, not the ww-side ident. Hare emits
|
|
* the same `$symname` for both call and
|
|
* address-of via QBE; here we mirror that.
|
|
* Bare ident → same-module by ww's resolver,
|
|
* so c->cur_mod is the right disambiguation
|
|
* hint. */
|
|
ins2(c, A_LEAQ,
|
|
mafn(c, n->str, c->cur_mod), areg(D_AX));
|
|
break;
|
|
}
|
|
{
|
|
/* Same-module-first walk over Sdef. Without
|
|
* the prefer pass two modules with same-leaf
|
|
* `def MSG: str = "..."` silently fold the
|
|
* wrong strlit into the caller's bare-ident
|
|
* load (sister callsite of cgdot's str-def
|
|
* field fold + wwstage deflookuprhs #4c). */
|
|
Sdef *s;
|
|
for (s = sdefs; s; s = s->next) {
|
|
if (strcmp(s->name, n->str) != 0)
|
|
continue;
|
|
if (sdef_mod_match(c, s)) break;
|
|
}
|
|
if (s == NULL) {
|
|
for (s = sdefs; s; s = s->next)
|
|
if (strcmp(s->name, n->str) == 0)
|
|
break;
|
|
}
|
|
if (s != NULL) {
|
|
const char *lab = intern_strlit(c,
|
|
s->bytes, s->len);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
aimm((long long)s->len),
|
|
areg(D_BX));
|
|
/* str IS []u8: cap = len for a static
|
|
* def literal (#1/Phase 3). */
|
|
ins2(c, A_MOVQ,
|
|
aimm((long long)s->len),
|
|
areg(D_CX));
|
|
goto ident_done;
|
|
}
|
|
}
|
|
if (let_islet(n->str)
|
|
&& (let_isstr(n->type) || let_isslice(n->type))) {
|
|
/* Top-level str/slice global: load each word
|
|
* via its address (the asm has no `name+8(SB)`
|
|
* operand form). str IS []u8 now — both carry a
|
|
* third 8B (cap); the address holder CX gets
|
|
* overwritten by the cap as the last step, after
|
|
* we no longer need it (#1/Phase 3). */
|
|
ins2(c, A_LEAQ, 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));
|
|
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;
|
|
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 */
|
|
/* f64/f32 result rides X0 (SSE), not AX — an integer
|
|
* MOVQ strands the value off the float ABI and the
|
|
* caller's MOVSD X0 reads stale bits (#96). Mirrors the
|
|
* float field/ident load idiom at 1462/1838. */
|
|
if (node_isfloat(n)) {
|
|
ins2(c, node_isf32(n) ? A_MOVSS : A_MOVSD,
|
|
amem(D_AX, 0), areg(D_X0));
|
|
} else {
|
|
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=ZF=CF=1 on unordered (a NaN
|
|
* operand). IEEE-754: any relop with a NaN operand is
|
|
* unordered — `!=` true, the other five false. PF must steer
|
|
* `!=`/`==`/`<`/`<=` (#97): JNE keys on ZF=0 so `nan != nan`
|
|
* came out false; JE/JB/JBE all fire on the unordered ZF/CF.
|
|
* `>`/`>=` (JA/JAE) need CF=0, which unordered never gives,
|
|
* so they are ALREADY NaN-correct and stay byte-identical to
|
|
* the pre-#97 single-template arm — no redundant PF guard. */
|
|
if (n->lhs && node_isfloat(n->lhs) &&
|
|
(n->op == TK_EQ || n->op == TK_NEQ
|
|
|| n->op == TK_LT || n->op == TK_LE
|
|
|| n->op == TK_GT || n->op == TK_GE)) {
|
|
int isf32 = node_isf32(n->lhs);
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
int ucomi = isf32 ? A_UCOMISS : A_UCOMISD;
|
|
cgexpr(c, n->rhs, locals); /* rhs → X0 */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
cgexpr(c, n->lhs, locals); /* lhs → X0 */
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, ucomi, areg(D_X1), areg(D_X0));
|
|
if (n->op == TK_NEQ) {
|
|
/* not-equal OR unordered -> true */
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, A_JNE, abranch(t));
|
|
ins1(c, A_JP, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
if (n->op == TK_EQ || n->op == TK_LT || n->op == TK_LE) {
|
|
/* unordered -> false; otherwise the ordered Jcc decides */
|
|
int op = (n->op == TK_EQ) ? A_JE
|
|
: (n->op == TK_LT) ? A_JB : A_JBE;
|
|
char *fl = mklabel(c, "cf");
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, A_JP, abranch(fl));
|
|
ins1(c, op, abranch(t));
|
|
label(c, fl);
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
/* `>`/`>=`: JA/JAE already reject unordered (CF=1), so
|
|
* keep the pre-#97 single-template shape verbatim. */
|
|
int op = (n->op == TK_GT) ? A_JA : A_JAE;
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, op, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
if (node_isfloat(n)) {
|
|
int isf32 = node_isf32(n);
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals); /* X0 */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
cgexpr(c, n->lhs, locals); /* X0 */
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
switch (n->op) {
|
|
case TK_PLUS:
|
|
ins2(c, isf32 ? A_ADDSS : A_ADDSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_MINUS:
|
|
ins2(c, isf32 ? A_SUBSS : A_SUBSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_STAR:
|
|
ins2(c, isf32 ? A_MULSS : A_MULSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_SLASH:
|
|
ins2(c, isf32 ? A_DIVSS : A_DIVSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
default: break;
|
|
}
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUS: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUS: ins2(c, A_SUBQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_STAR: ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_SLASH: {
|
|
/* Use DIV (unsigned) when either operand is an unsigned
|
|
* integer type — IDIV would sign-extend a u64 with high
|
|
* bit set into a negative i64 and produce wrong results
|
|
* (see strconv.u64tos with v = 1 << 63). Signed IDIV
|
|
* needs CQO to sign-extend RAX into RDX:RAX; zeroing
|
|
* DX would treat a negative dividend as a huge unsigned
|
|
* 128-bit value. */
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX));
|
|
break;
|
|
}
|
|
case TK_PERCENT: {
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case TK_AMP: ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_PIPE: ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_CARET: ins2(c, A_XORQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_LSHIFT: case TK_RSHIFT:
|
|
/* shift amount must be in CL */
|
|
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_chase_named (#22): a chain `type b = a; a = struct`
|
|
* left u at TY_NAMED a after a single peel, missing the
|
|
* TY_STRUCT field-walk gate below — the assignment
|
|
* silently dropped (the `break` at the bottom of the
|
|
* N_DOT-lhs arm). */
|
|
Type *u = type_chase_named(bt);
|
|
int via_ptr = 0;
|
|
if (u && u->kind == TY_PTR) {
|
|
via_ptr = 1;
|
|
u = type_chase_named(u->sub);
|
|
}
|
|
/* slice/str pseudo-field write (.ptr/.len/.cap) */
|
|
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)) {
|
|
const char *fld = n->lhs->str;
|
|
int delta = -1;
|
|
if (strcmp(fld, "ptr") == 0) delta = 0;
|
|
else if (strcmp(fld, "len") == 0) delta = 8;
|
|
else if (strcmp(fld, "cap") == 0) delta = 16;
|
|
if (delta < 0) goto after_dot_assign;
|
|
int boff = localfind(locals, base->str);
|
|
if (n->op != TK_ASSIGN) {
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + delta), areg(D_BX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
if (n->op != TK_ASSIGN) {
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUSEQ:
|
|
/* old in BX, rhs in AX; want AX = old-rhs.
|
|
* SUBQ src,dst is dst -= src in Plan 9. */
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, delta));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + delta));
|
|
}
|
|
break;
|
|
}
|
|
after_dot_assign:
|
|
if (u && u->kind == TY_STRUCT) {
|
|
/* find field metadata */
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = u->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, n->lhs->str) == 0)
|
|
{ f = fl; break; }
|
|
if (f == NULL) break;
|
|
/* Tagged-union field: synthesise tag and store
|
|
* value bytes. Compound ops on tagged fields are
|
|
* not meaningful, so only plain `=` is wired.
|
|
* Three base shapes:
|
|
* - via_ptr: base is *struct local; address
|
|
* pre-loaded into BX. Buggy with a str
|
|
* variant since cgexpr will overwrite BX,
|
|
* but matches the existing pre-global
|
|
* behaviour.
|
|
* - is_global: struct global. LEAQ after
|
|
* cgexpr drops the slot address into CX
|
|
* without touching AX/BX, so str variants
|
|
* work cleanly.
|
|
* - else: struct local, BP-relative. */
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
/* Tagged-union field — full slot rewrite via the
|
|
* shared widener so every rhs shape (whole-tagged
|
|
* ident or expr with tag-remap, concrete-variant
|
|
* widening of str/slice/struct/scalar/void) lands
|
|
* the right tag + payload bytes. The pre-#26
|
|
* branch synthesised a single tag from
|
|
* cg_tag_for_variant and stored only AX at +8, so
|
|
* whole-tagged rhs (vt == fu, no concrete tag)
|
|
* silently wrote tag 0 and dropped trailing words.
|
|
* cg_widen_tagged_store handles every shape by
|
|
* branching on the source's resolved type. */
|
|
if (fu && fu->kind == TY_TAGGED
|
|
&& n->op == TK_ASSIGN) {
|
|
int boff = localfind(locals, base->str);
|
|
int is_global = (boff == 0 && !via_ptr
|
|
&& let_islet(base->str));
|
|
int foff = (int)f->offset;
|
|
int fsz = (int)fu->size;
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BX, foff, fsz);
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BX, foff, fsz);
|
|
} else {
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BP,
|
|
boff + foff, fsz);
|
|
}
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int load_op = fldloadop(f->type, fsz);
|
|
int store_op = fldstoreop(f->type, fsz);
|
|
int boff = localfind(locals, base->str);
|
|
int is_global = (boff == 0 && !via_ptr
|
|
&& let_islet(base->str));
|
|
int foff = (int)f->offset;
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
/* str/slice field: str IS []u8, so both store the full
|
|
* 3-word {ptr,len,cap} that rhs cgexpr leaves in
|
|
* (AX,BX,CX) at field+0/+8/+16. Address scratch must
|
|
* dodge CX (holds cap), so via_ptr/is_global stage the
|
|
* struct base in DX (#1/Phase 3). Without this the
|
|
* generic store_op below writes only AX, silently
|
|
* dropping .len/.cap. Only plain `=` is wired; compound
|
|
* on a str/slice field is not meaningful. */
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& (str_fu->kind == TY_SLICE || str_fu->kind == TY_STR)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, boff + foff + 16));
|
|
}
|
|
break;
|
|
}
|
|
/* struct-typed field, three rhs shapes:
|
|
* - N_IDENT: word-copy from the rhs slot directly
|
|
* onto the destination field. cgexpr cannot
|
|
* materialise a whole struct value in registers
|
|
* for an arbitrary local, so we read field words
|
|
* straight from the source slot.
|
|
* - N_CALL (added with #5): cgexpr leaves the value
|
|
* in AX/DX/CX per #4's cgreturn ABI; sized stores
|
|
* write only the declared field size — MOVQ for
|
|
* full 8B chunks plus MOVL/MOVW/MOVB tail. See
|
|
* the N_LET receive site for the ASYMMETRY
|
|
* rationale. cgreturn touches only AX/DX/CX, so
|
|
* BX stays free for the dst-addr load after the
|
|
* call.
|
|
* - N_STRUCTLIT (added with #5): field-by-field
|
|
* store; for via_ptr/is_global the dst base addr
|
|
* is reloaded into BX before each store so cgexpr
|
|
* can clobber AX/BX between fields. */
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& (int)str_fu->size <= 24
|
|
&& n->rhs && n->rhs->kind == N_CALL
|
|
&& (str_fu->size % 8 == 0
|
|
|| str_fu->size % 8 == 1
|
|
|| str_fu->size % 8 == 2
|
|
|| str_fu->size % 8 == 4)) {
|
|
int ssz = (int)str_fu->size;
|
|
cgexpr(c, n->rhs, locals);
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = ssz / 8;
|
|
int tail = ssz % 8;
|
|
int base_reg, base_disp;
|
|
if (via_ptr || is_global) {
|
|
if (via_ptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
base_reg = D_BX;
|
|
base_disp = foff;
|
|
} else {
|
|
base_reg = D_BP;
|
|
base_disp = boff + foff;
|
|
}
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(base_reg,
|
|
base_disp + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW
|
|
: A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(base_reg,
|
|
base_disp + full * 8));
|
|
}
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
|
|
/* Delegate to the shared structlit fill
|
|
* helper. For via_ptr/is_global, helper
|
|
* reloads BX before zero-fill loop + each
|
|
* field store. For local BP-rel, helper
|
|
* stores direct off BP. AND nested struct-
|
|
* typed structlit values recurse instead
|
|
* of silently dropping trailing bytes
|
|
* (#18 fix). */
|
|
int mode = via_ptr ? DST_PTR_LOCAL
|
|
: is_global ? DST_GLOBAL : DST_BP;
|
|
int disp = (mode == DST_BP)
|
|
? (boff + foff) : foff;
|
|
cg_structlit_fill(c, &locals, str_fu,
|
|
n->rhs, mode, boff,
|
|
is_global ? base->str : NULL, disp);
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_IDENT
|
|
&& localfind(locals, n->rhs->str) != 0) {
|
|
int soff = localfind(locals, n->rhs->str);
|
|
int ssz = (int)str_fu->size;
|
|
if (via_ptr)
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
else if (is_global)
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
int k = 0;
|
|
while (k + 8 <= ssz) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX));
|
|
if (via_ptr || is_global)
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + k));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + k));
|
|
k += 8;
|
|
}
|
|
if (k < ssz) {
|
|
int tail = ssz - k;
|
|
int lop = (tail == 4) ? A_MOVL
|
|
: (tail == 1 ? A_MOVB : A_MOVQ);
|
|
ins2(c, lop, amem(D_BP, soff + k), areg(D_AX));
|
|
if (via_ptr || is_global)
|
|
ins2(c, lop, areg(D_AX), amem(D_BX, foff + k));
|
|
else
|
|
ins2(c, lop, areg(D_AX), amem(D_BP, boff + foff + k));
|
|
}
|
|
break;
|
|
}
|
|
/* compound: load current value into BX */
|
|
if (n->op != TK_ASSIGN) {
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, foff), areg(D_BX));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, foff), areg(D_BX));
|
|
} else {
|
|
ins2(c, load_op, amem(D_BP, boff + foff), areg(D_BX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
}
|
|
cgexpr(c, n->rhs, locals); /* AX = rhs */
|
|
if (n->op != TK_ASSIGN) {
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUSEQ:
|
|
/* old in BX, rhs in AX; want AX=old-rhs */
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
default: break; /* others rare */
|
|
}
|
|
}
|
|
/* f64/f32 field, plain `=`: cgexpr left the value in
|
|
* X0, not AX. Route the store via MOVSD/MOVSS.
|
|
* Compound ops on float fields aren't wired here —
|
|
* see CLAUDE.md #8 in examples/lisp; same in the
|
|
* structlit-init path below. */
|
|
int b_isf32 = 0;
|
|
if (n->op == TK_ASSIGN
|
|
&& fld_isfloat(f->type, &b_isf32)) {
|
|
int mov = b_isf32 ? A_MOVSS : A_MOVSD;
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, foff));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str), areg(D_BX));
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, foff));
|
|
} else {
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP, boff + foff));
|
|
}
|
|
break;
|
|
}
|
|
/* now store AX into target */
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, foff));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, foff));
|
|
} else {
|
|
ins2(c, store_op, areg(D_AX), amem(D_BP, boff + foff));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric
|
|
* write-side of the cgdot N_INDEX-lhs branch. Compute &arr[i]
|
|
* inline (LEAQ for `[N]Struct`, MOVQ-load for `[N]*Struct` /
|
|
* `[]Struct` / `*Struct`), deref once when the element is
|
|
* `*Struct`, then store rhs at `field.offset(addr)`. The
|
|
* chained-pointer-field branch below catches `[N]*Struct`
|
|
* writes via its `!= N_IDENT` guard, but `[N]Struct` value-arrays
|
|
* fall through and silently drop the store. Placed before the
|
|
* `!= N_IDENT` branch so both shapes share one path. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
|
|
&& n->lhs->lhs->kind == N_INDEX) {
|
|
Node *idxbase = n->lhs->lhs->lhs;
|
|
Node *idx = n->lhs->lhs->rhs;
|
|
if (idxbase && idxbase->kind == N_IDENT && idx) {
|
|
Type *elemt = n->lhs->lhs->type;
|
|
Type *elemu = (elemt && elemt->kind == TY_NAMED)
|
|
? elemt->under : elemt;
|
|
Type *struct_t = NULL;
|
|
int viaptr = 0;
|
|
if (elemu && elemu->kind == TY_PTR) {
|
|
Type *inner = elemu->sub;
|
|
if (inner && inner->kind == TY_NAMED)
|
|
inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
struct_t = inner;
|
|
viaptr = 1;
|
|
}
|
|
} else if (elemu && elemu->kind == TY_STRUCT) {
|
|
struct_t = elemu;
|
|
}
|
|
if (struct_t) {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = struct_t->fields; fl;
|
|
fl = fl->next)
|
|
if (strcmp(fl->name,
|
|
n->lhs->str) == 0)
|
|
{ f = fl; break; }
|
|
Type *bt = idxbase->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
int is_arr = bu && bu->kind == TY_ARRAY;
|
|
int is_sl = bu && bu->kind == TY_SLICE;
|
|
int is_ptr = bu && bu->kind == TY_PTR;
|
|
int off = localfind(locals, idxbase->str);
|
|
if (f != NULL && (is_arr || is_sl || is_ptr)
|
|
&& off != 0) {
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int store_op = fldstoreop(ft, fsz);
|
|
int foff = (int)f->offset;
|
|
int esz = (int)elemt->size;
|
|
int h_isf32 = 0;
|
|
if (n->op == TK_ASSIGN
|
|
&& fld_isfloat(ft, &h_isf32)) {
|
|
int mov = h_isf32
|
|
? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_SUBQ, aimm(8),
|
|
areg(D_SP));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_SP, 0));
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ,
|
|
areg(D_AX),
|
|
areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0),
|
|
areg(D_BX));
|
|
ins2(c, mov,
|
|
amem(D_SP, 0),
|
|
areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8),
|
|
areg(D_SP));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN
|
|
&& fu && (fu->kind == TY_STR
|
|
|| fu->kind == TY_SLICE)) {
|
|
/* str/slice: rhs leaves
|
|
* AX=ptr, BX=len, CX=cap
|
|
* (#1/Phase 3). Spill all
|
|
* three across the index/
|
|
* address computation
|
|
* (IMULQ's CX scratch
|
|
* clobbers cap), stage
|
|
* &arr[i] in DX off the str
|
|
* AX/BX/CX convention
|
|
* (mirrors s.f=v), then store
|
|
* the full triple at
|
|
* foff+0/+8/+16. */
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ,
|
|
areg(D_CX));
|
|
ins1(c, A_PUSHQ,
|
|
areg(D_BX));
|
|
ins1(c, A_PUSHQ,
|
|
areg(D_AX));
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_DX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_DX));
|
|
ins2(c, A_ADDQ,
|
|
areg(D_AX),
|
|
areg(D_DX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_DX, 0),
|
|
areg(D_DX));
|
|
ins1(c, A_POPQ,
|
|
areg(D_AX));
|
|
ins1(c, A_POPQ,
|
|
areg(D_BX));
|
|
ins1(c, A_POPQ,
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
areg(D_AX),
|
|
amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ,
|
|
areg(D_BX),
|
|
amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ,
|
|
areg(D_CX),
|
|
amem(D_DX, foff + 16));
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ,
|
|
areg(D_AX));
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ,
|
|
areg(D_AX),
|
|
areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0),
|
|
areg(D_BX));
|
|
ins1(c, A_POPQ,
|
|
areg(D_AX));
|
|
ins2(c, store_op,
|
|
areg(D_AX),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
/* compound: rhs→push; compute
|
|
* struct addr→BX (deref if *T);
|
|
* push addr; load old field→AX;
|
|
* pop addr→BX, rhs→CX; combine;
|
|
* store. Float/str compound
|
|
* not wired. */
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_AX),
|
|
areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0),
|
|
areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
int load_op = fldloadop(ft, fsz);
|
|
ins2(c, load_op,
|
|
amem(D_BX, foff),
|
|
areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ:
|
|
ins2(c, A_ADDQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_MINUSEQ:
|
|
ins2(c, A_SUBQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_STAREQ:
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_AMPEQ:
|
|
ins2(c, A_ANDQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_PIPEEQ:
|
|
ins2(c, A_ORQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_CARETEQ:
|
|
ins2(c, A_XORQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
default: break;
|
|
}
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* Chained `<expr>.field = v` where <expr> evaluates to a *struct.
|
|
* cgexpr on the inner expression already returns the pointer;
|
|
* we then store at (ptr + field.offset). Without this, only the
|
|
* single-level N_IDENT base above is wired and shapes like
|
|
* `r.sym.flag = 1` (where r.sym: *T) silently emit no store —
|
|
* the read still works because the chained-N_DOT read path is
|
|
* wired below. (This was trap 1 of the cgen miscompilations.) */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
|
|
&& n->lhs->lhs->kind != N_IDENT) {
|
|
Type *bt = n->lhs->lhs->type;
|
|
/* type_chase_named (#22); same rationale as the cgexpr-
|
|
* side pointer-to-struct field branch. */
|
|
Type *bu = type_chase_named(bt);
|
|
if (bu && bu->kind == TY_PTR && bu->sub) {
|
|
Type *inner = type_chase_named(bu->sub);
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = inner->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, n->lhs->str) == 0)
|
|
{ f = fl; break; }
|
|
if (f != NULL) {
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int store_op = fldstoreop(ft, fsz);
|
|
int foff = (int)f->offset;
|
|
if (n->op == TK_ASSIGN) {
|
|
int c_isf32 = 0;
|
|
if (fld_isfloat(ft, &c_isf32)) {
|
|
/* f64/f32 chained-store: cgexpr rhs
|
|
* left the value in X0. Spill to stack
|
|
* so cgexpr on the inner pointer can
|
|
* use AX, then reload into X0 and
|
|
* MOVSD/MOVSS into the slot. */
|
|
int mov = c_isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_SP, 0));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_BX));
|
|
ins2(c, mov, amem(D_SP, 0),
|
|
areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
if (fu && (fu->kind == TY_STR
|
|
|| fu->kind == TY_SLICE)) {
|
|
/* str/slice: rhs leaves AX=ptr,
|
|
* BX=len, CX=cap (#1/Phase 3). Spill
|
|
* all three across the base-expr eval
|
|
* (it may clobber any reg), stage the
|
|
* *struct ptr in DX off the str
|
|
* AX/BX/CX convention (mirrors s.f=v),
|
|
* then store the full triple at
|
|
* foff+0/+8/+16. */
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_CX));
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_DX, foff + 16));
|
|
} else {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_BX, foff));
|
|
}
|
|
break;
|
|
}
|
|
/* compound op: AX=rhs → push; eval ptr → push;
|
|
* load old field → AX; pop ptr→BX, rhs→CX;
|
|
* combine; store. 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 `<chain>.field = v` where <chain> spans value-struct
|
|
* dots ending at a root ident — `o.i.a = 10`, `v.a.b.c = …`.
|
|
* Also handles a slice/str pseudo-field leaf (`b.buf.len = 5`):
|
|
* spine walks down to the slice/str header, then the +0/+8/+16
|
|
* delta selects ptr/len/cap. Sibling of the chained-pointer-
|
|
* field branch above; without this the LHS is silently dropped
|
|
* (the existing 1-deep branch only fires for `ident.field = …`).
|
|
* Only plain `=` is wired — compound on a chained value-struct
|
|
* field is rare and stays unhandled. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
|
|
&& n->lhs->lhs->kind == N_DOT && n->op == TK_ASSIGN) {
|
|
struct { Type *pu; const char *name; } steps[16];
|
|
int nsteps = 0;
|
|
int ptr_root = 0;
|
|
Node *cur = n->lhs;
|
|
int abort = 0;
|
|
while (cur && cur->kind == N_DOT && cur->lhs) {
|
|
Type *pt = cur->lhs->type;
|
|
Type *pu = (pt && pt->kind == TY_NAMED)
|
|
? pt->under : pt;
|
|
if (!pu) { abort = 1; break; }
|
|
if (cur == n->lhs && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR)) {
|
|
/* leaf pseudo-field on slice/str header */
|
|
} else if (pu->kind == TY_STRUCT) {
|
|
/* value-struct hop */
|
|
} else if (pu->kind == TY_PTR && pu->sub
|
|
&& cur->lhs->kind == N_IDENT) {
|
|
/* `*T` root: dereference at emit time;
|
|
* walk through pointee struct fields.
|
|
* Last-hop only (root is a bare ident). */
|
|
Type *sub = (pu->sub->kind == TY_NAMED)
|
|
? pu->sub->under : pu->sub;
|
|
if (sub && sub->kind == TY_STRUCT) {
|
|
pu = sub;
|
|
ptr_root = 1;
|
|
} else {
|
|
abort = 1;
|
|
break;
|
|
}
|
|
} else {
|
|
abort = 1;
|
|
break;
|
|
}
|
|
if (nsteps >= 16) { abort = 1; break; }
|
|
steps[nsteps].pu = pu;
|
|
steps[nsteps].name = cur->str;
|
|
nsteps++;
|
|
cur = cur->lhs;
|
|
}
|
|
if (!abort && cur && cur->kind == N_IDENT
|
|
&& nsteps > 0) {
|
|
int total_off = 0;
|
|
Type *leaf_type = NULL;
|
|
int slice_delta = -1;
|
|
int ok = 1;
|
|
for (int i = nsteps - 1; i >= 0; i--) {
|
|
Type *pu = steps[i].pu;
|
|
if (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR) {
|
|
if (strcmp(steps[i].name, "ptr") == 0)
|
|
slice_delta = 0;
|
|
else if (strcmp(steps[i].name, "len") == 0)
|
|
slice_delta = 8;
|
|
else if (strcmp(steps[i].name, "cap") == 0)
|
|
slice_delta = 16;
|
|
else { ok = 0; break; }
|
|
} else {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = pu->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, steps[i].name) == 0)
|
|
{ f = fl; break; }
|
|
if (!f) { ok = 0; break; }
|
|
total_off += (int)f->offset;
|
|
leaf_type = f->type;
|
|
}
|
|
}
|
|
if (ok) {
|
|
int root_off = localfind(locals, cur->str);
|
|
int base_disp = root_off;
|
|
int is_global = 0;
|
|
int root_resolved = (root_off != 0);
|
|
if (!root_resolved && let_islet(cur->str)) {
|
|
root_resolved = 1;
|
|
is_global = 1;
|
|
}
|
|
if (root_resolved) {
|
|
/* `*T` root and global both store via CX as
|
|
* the base register; only the loader differs
|
|
* (LEAQ name(SB) vs MOVQ off(BP)). Compute it
|
|
* AFTER cgexpr(rhs) so AX/BX/X0 stay intact. */
|
|
int via_cx = is_global || ptr_root;
|
|
if (slice_delta >= 0) {
|
|
/* slice/str pseudo-field store. .ptr writes
|
|
* 8 bytes; .len / .cap write 8 bytes each
|
|
* (matches the existing N_IDENT pseudo-
|
|
* field branch). */
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_CX, total_off + slice_delta));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off + slice_delta));
|
|
}
|
|
break;
|
|
}
|
|
Type *fu = (leaf_type
|
|
&& leaf_type->kind == TY_NAMED)
|
|
? leaf_type->under : leaf_type;
|
|
int fsz = (int)(leaf_type
|
|
? leaf_type->size : 8);
|
|
int store_op = fldstoreop(leaf_type, fsz);
|
|
if (fu && (fu->kind == TY_STR
|
|
|| fu->kind == TY_SLICE)) {
|
|
/* str/slice: store ptr/len/cap. cgexpr
|
|
* leaves CX=cap, so the via_cx base goes in
|
|
* DX (not CX) to avoid clobbering it — same
|
|
* as the single-dot str field store
|
|
* (#1/Phase 3). */
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_DX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_DX, total_off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_DX, total_off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_DX, total_off + 16));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, base_disp + total_off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, base_disp + total_off + 16));
|
|
}
|
|
break;
|
|
}
|
|
/* TY_STRUCT terminal in the chained-DOT walker:
|
|
* three rhs shapes — mirror of the single-dot
|
|
* branch.
|
|
* - N_IDENT: word-copy from rhs local slot.
|
|
* - N_CALL (added with #5): cgexpr → AX/DX/CX
|
|
* per #4's cgreturn ABI; sized stores per
|
|
* declared field size. cgreturn touches only
|
|
* AX/DX/CX so via_cx loads the dst addr into
|
|
* BX (not CX) after the call to keep CX as
|
|
* the third value word.
|
|
* - N_STRUCTLIT (added with #5): field-by-field
|
|
* store; via_cx reloads BX before each store
|
|
* so cgexpr can clobber AX/BX between fields.
|
|
*/
|
|
if (fu && fu->kind == TY_STRUCT
|
|
&& fsz <= 24
|
|
&& n->rhs && n->rhs->kind == N_CALL
|
|
&& (fsz % 8 == 0 || fsz % 8 == 1
|
|
|| fsz % 8 == 2 || fsz % 8 == 4)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = fsz / 8;
|
|
int tail = fsz % 8;
|
|
int base_reg, base_off;
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_BX));
|
|
base_reg = D_BX;
|
|
base_off = total_off;
|
|
} else {
|
|
base_reg = D_BP;
|
|
base_off = base_disp + total_off;
|
|
}
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(base_reg,
|
|
base_off + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW
|
|
: A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(base_reg,
|
|
base_off + full * 8));
|
|
}
|
|
break;
|
|
}
|
|
if (fu && fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
|
|
/* Delegate to the shared structlit fill
|
|
* helper. For via_cx (ptr_root | is_global),
|
|
* helper reloads BX before zero-fill loop +
|
|
* each field store. For local through chain,
|
|
* helper stores direct off BP. AND nested
|
|
* struct-typed structlit values recurse
|
|
* instead of silently dropping trailing
|
|
* bytes (#18 fix). */
|
|
int dst_mode = ptr_root ? DST_PTR_LOCAL
|
|
: is_global ? DST_GLOBAL : DST_BP;
|
|
int dst_disp = (dst_mode == DST_BP)
|
|
? (base_disp + total_off) : total_off;
|
|
cg_structlit_fill(c, &locals, fu,
|
|
n->rhs, dst_mode, base_disp,
|
|
is_global ? cur->str : NULL,
|
|
dst_disp);
|
|
break;
|
|
}
|
|
if (fu && fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_IDENT
|
|
&& localfind(locals, n->rhs->str) != 0) {
|
|
int soff = localfind(locals, n->rhs->str);
|
|
int ssz = fsz;
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_CX));
|
|
}
|
|
int k = 0;
|
|
while (k + 8 <= ssz) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
if (via_cx)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_CX, total_off + k));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off + k));
|
|
k += 8;
|
|
}
|
|
if (k < ssz) {
|
|
int tail = ssz - k;
|
|
int lop = (tail == 4) ? A_MOVL
|
|
: (tail == 1 ? A_MOVB : A_MOVQ);
|
|
ins2(c, lop,
|
|
amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
if (via_cx)
|
|
ins2(c, lop, areg(D_AX),
|
|
amem(D_CX, total_off + k));
|
|
else
|
|
ins2(c, lop, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off + k));
|
|
}
|
|
break;
|
|
}
|
|
int sf32 = 0;
|
|
if (fld_isfloat(leaf_type, &sf32)) {
|
|
int mov = sf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_CX));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_CX, total_off));
|
|
} else {
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP, base_disp + total_off));
|
|
}
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_CX));
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_CX, total_off));
|
|
} else {
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* float assignment to a local or top-level global. Globals
|
|
* route through LEAQ+indirect (no D_EXTERN SSE in w6a).
|
|
* Compound (`acc += d` etc.) loads slot into X1, combines
|
|
* into X1 (Plan 9 syntax: OP src, dst), stores X1 back —
|
|
* w6a's ADDSD/SUBSD/MULSD/DIVSD are register-register only,
|
|
* so we can't use a direct mem-form like the integer ADDQ. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) {
|
|
cgexpr(c, n->rhs, locals); /* X0 */
|
|
int mvop = op_for(n, A_MOVSD, A_MOVSS);
|
|
int addop = op_for(n, A_ADDSD, A_ADDSS);
|
|
int subop = op_for(n, A_SUBSD, A_SUBSS);
|
|
int mulop = op_for(n, A_MULSD, A_MULSS);
|
|
int divop = op_for(n, A_DIVSD, A_DIVSS);
|
|
int off = localfind(locals, n->lhs->str);
|
|
int isglobal = (off == 0) && let_islet(n->lhs->str);
|
|
if (off == 0 && !isglobal) break;
|
|
if (n->op == TK_ASSIGN) {
|
|
if (off != 0) {
|
|
ins2(c, mvop, areg(D_X0), amem(D_BP, off));
|
|
} else {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, mvop, areg(D_X0), amem(D_CX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* Compound: X1 = load; X1 OP= X0; store X1. */
|
|
int fop = -1;
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: fop = addop; break;
|
|
case TK_MINUSEQ: fop = subop; break;
|
|
case TK_STAREQ: fop = mulop; break;
|
|
case TK_SLASHEQ: fop = divop; break;
|
|
default: break;
|
|
}
|
|
if (off != 0) {
|
|
if (fop < 0) {
|
|
/* Unsupported compound (e.g., %= on float):
|
|
* fall back to plain store of rhs. */
|
|
ins2(c, mvop, areg(D_X0),
|
|
amem(D_BP, off));
|
|
break;
|
|
}
|
|
ins2(c, mvop, amem(D_BP, off), areg(D_X1));
|
|
ins2(c, fop, areg(D_X0), areg(D_X1));
|
|
ins2(c, mvop, areg(D_X1), amem(D_BP, off));
|
|
} else {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
if (fop < 0) {
|
|
ins2(c, mvop, areg(D_X0),
|
|
amem(D_CX, 0));
|
|
break;
|
|
}
|
|
ins2(c, mvop, amem(D_CX, 0), areg(D_X1));
|
|
ins2(c, fop, areg(D_X0), areg(D_X1));
|
|
ins2(c, mvop, areg(D_X1), amem(D_CX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* arr[i] = v store. Base may be a simple ident (array/slice/
|
|
* ptr local) or a more complex expression like s.ptr where
|
|
* s: *[]u8. We compute the base address, scale the index by
|
|
* elem size, and store with the right size. */
|
|
if (n->lhs->kind == N_INDEX && n->lhs->lhs) {
|
|
Node *base = n->lhs->lhs;
|
|
Type *bt = base->type;
|
|
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
int is_arr = u && u->kind == TY_ARRAY;
|
|
int is_sl = u && u->kind == TY_SLICE;
|
|
int is_ptr = u && u->kind == TY_PTR;
|
|
/* For `*[N]T` drill through to the array so esz reflects
|
|
* T, not sizeof(array). Base load still uses u (MOVQ
|
|
* because is_ptr stays true). */
|
|
Type *eff = idx_eff(bt);
|
|
int esz = (eff && eff->sub) ? (int)eff->sub->size : 1;
|
|
int elem_is_str = eff && eff->sub && type_isstr(eff->sub);
|
|
int elem_is_slice = eff && eff->sub && type_isslice(eff->sub);
|
|
Type *esub = eff ? eff->sub : NULL;
|
|
Type *esubu = (esub && esub->kind == TY_NAMED)
|
|
? esub->under : esub;
|
|
int elem_tagged = esubu && esubu->kind == TY_TAGGED;
|
|
/* Tagged-union element: route widening through a
|
|
* scratch slot, then copy slot bytes to &arr[i].
|
|
* Materialising into the scratch first lets us reuse
|
|
* the full cg_widen_tagged_store machinery — scalar /
|
|
* str / struct / subset payloads, tag remap, nullable
|
|
* fold — without duplicating it. The scratch lives in
|
|
* the function frame; no cleanup needed. */
|
|
if ((is_arr || is_sl || is_ptr) && elem_tagged) {
|
|
int ssz = esz;
|
|
int scr;
|
|
if (cg_tagscr != 0) {
|
|
if (ssz > cg_tagscr_sz)
|
|
fatal("N_INDEX tagged: "
|
|
"@tagscr cached sz %d, "
|
|
"need %d (pinned offset "
|
|
"can't grow in place; "
|
|
"rule 7 — #15/#26c)",
|
|
cg_tagscr_sz, ssz);
|
|
scr = cg_tagscr;
|
|
} else {
|
|
scr = local_alloc(c, &locals,
|
|
"@tagscr", ssz, cg_frame);
|
|
cg_tagscr = scr;
|
|
cg_tagscr_sz = ssz;
|
|
}
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < ssz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
cg_widen_tagged_store(c, &locals, esubu,
|
|
n->rhs, D_BP, scr, ssz);
|
|
/* Compute &arr[i] → BX. */
|
|
cgexpr(c, n->lhs->rhs, locals);
|
|
if (ssz > 1) {
|
|
ins2(c, A_MOVQ, aimm(ssz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
if (base->kind == N_IDENT && is_arr) {
|
|
int boff = localfind(locals, base->str);
|
|
ins2(c, A_LEAQ, amem(D_BP, boff),
|
|
areg(D_BX));
|
|
} else if (base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, boff),
|
|
areg(D_BX));
|
|
} else {
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, base, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
/* Copy scratch slot → dest. */
|
|
for (int k = 0; k < ssz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BX, k));
|
|
}
|
|
break;
|
|
}
|
|
if (is_arr || is_sl || is_ptr) {
|
|
cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len,CX=cap if str) */
|
|
/* str/slice: stash cap+len so all three store
|
|
* (#1/Phase 3). */
|
|
if (elem_is_str || elem_is_slice) {
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
}
|
|
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 || elem_is_slice) {
|
|
/* str/slice: store ptr/len/cap (#1/Phase 3, #7). */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16));
|
|
break;
|
|
}
|
|
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=ptr (BX=len, CX=cap if str/slice) */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (vt && (vt->kind == TY_STR || vt->kind == TY_SLICE)) {
|
|
/* str IS []u8 and a slice is the same 3-word
|
|
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8):
|
|
* stash len + cap across the pointer eval, which
|
|
* clobbers BX/CX (#1/Phase 3; slice arm #79). */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
}
|
|
cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
if (vt && (vt->kind == TY_STR || vt->kind == TY_SLICE)) {
|
|
ins1(c, A_POPQ, areg(D_CX)); /* cap */
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16));
|
|
ins1(c, A_POPQ, areg(D_CX)); /* len */
|
|
ins1(c, A_POPQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
|
|
} else {
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
int sz = vt ? (int)vt->size : 8;
|
|
int store_op = fldstoreop(vt, sz);
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* `*p OP= v` — compound assign through a pointer deref. The
|
|
* plain-assign branch above only fires for TK_ASSIGN; without
|
|
* this, compound ops fall through the switch and emit nothing
|
|
* (silent no-op). Evaluate rhs → save, evaluate ptr → BX, load
|
|
* *BX (sized + extended), combine with rhs in CX, sized store
|
|
* back. Scalar deref targets only — float and aggregate deref
|
|
* compounds (rare) still fall through. */
|
|
if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR
|
|
&& n->op != TK_ASSIGN) {
|
|
Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL;
|
|
Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
|
|
Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL;
|
|
if (vt && vt->kind == TY_NAMED) vt = vt->under;
|
|
int sz = vt ? (int)vt->size : 8;
|
|
int load_op = fldloadop(vt, sz);
|
|
int store_op = fldstoreop(vt, sz);
|
|
int handled = (sz == 1 || sz == 2 || sz == 4 || sz == 8);
|
|
if (handled) {
|
|
cgexpr(c, n->rhs, locals); /* AX = rhs */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals); /* AX = ptr */
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_LSHIFTEQ: ins2(c, A_SHLQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_RSHIFTEQ: 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;
|
|
}
|
|
}
|
|
/* `name = expr;` reassignment of a str/slice/struct local or
|
|
* top-level let. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
|
|
&& n->lhs->type) {
|
|
Type *lt = n->lhs->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
/* str/slice local/let: str IS []u8, so both store the full
|
|
* 3-word {ptr,len,cap} from (AX,BX,CX) at off+0/+8/+16
|
|
* (local) or via &name(SB) → DI scratch (global — CX holds
|
|
* the cap, and the asm has no `name+8(SB)` operand form, so
|
|
* a different address register is needed) (#1/Phase 3). */
|
|
if (lu && (lu->kind == TY_SLICE || lu->kind == TY_STR)) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off != 0) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
if (let_islet(n->lhs->str)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_DI));
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, 8));
|
|
ins2(c, A_MOVQ, areg(D_DI), amem(D_CX, 16));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
/* sret receive (#23): `s = f();` where s is a struct
|
|
* local >24B. s's slot IS the caller-prealloc dest;
|
|
* the callee writes through hidden RDI. Mirrors the
|
|
* cglet branch above. */
|
|
if (lu && lu->kind == TY_STRUCT && (int)lu->size > 24
|
|
&& n->rhs && n->rhs->kind == N_CALL
|
|
&& n->op == TK_ASSIGN) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off != 0) {
|
|
cg_sret_dest_off = off;
|
|
cgexpr(c, n->rhs, locals);
|
|
cg_sret_dest_off = 0;
|
|
break;
|
|
}
|
|
}
|
|
/* 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
|
|
* <abort body>
|
|
* skip: */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
n->lhs->type == ty_err &&
|
|
strcmp(n->lhs->str, "assert") == 0 && n->list) {
|
|
cgexpr(c, n->list, locals);
|
|
char *skip = mklabel(c, "as");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(skip));
|
|
Node *msg = n->list->next;
|
|
if (msg) {
|
|
cgexpr(c, msg, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
|
|
} else {
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
|
|
}
|
|
ins1(c, A_CALL, asym("rt_abort"));
|
|
label(c, skip);
|
|
break;
|
|
}
|
|
/* Hare-style builtins: len(x) and append(s, v). */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
strcmp(n->lhs->str, "len") == 0 && n->list) {
|
|
Node *a = n->list;
|
|
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->type == ty_err &&
|
|
n->lhs->str && strcmp(n->lhs->str, "alloc") == 0 &&
|
|
n->list) {
|
|
/* alloc(value): heap-init a fresh *T with the value's
|
|
* bytes. Size comes from the value's static type.
|
|
* `n->lhs->type == ty_err` gate (mirrors assert above)
|
|
* — check.c only stamps ty_err when no user-scoped
|
|
* `alloc` shadows the builtin (task #23).
|
|
*
|
|
* Result is the graduated `(*T | nomem)` tagged-pointer
|
|
* ABI (AX=tag, DX=ptr) — task #30. rt_malloc returns 0
|
|
* on OOM (rt/alloc.s); we branch on AX, building tag=1
|
|
* (nomem, DX=0) on null and tag=0 (success, DX=ptr)
|
|
* after the value-init stores complete. Callers wrap
|
|
* with `!` / `?` to consume the union. */
|
|
Node *v = n->list;
|
|
Type *t = v->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
Type *def = type_default(t);
|
|
int sz = def ? (int)def->size : 8;
|
|
if (sz == 0) sz = 8;
|
|
char *alloc_ok = mklabel(c, "alloc_ok");
|
|
char *alloc_done = mklabel(c, "alloc_done");
|
|
ins2(c, A_MOVQ, aimm(sz), areg(D_DI));
|
|
ins1(c, A_CALL, asym(ffi_resolve("malloc")));
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(alloc_ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
ins1(c, A_JMP, abranch(alloc_done));
|
|
label(c, alloc_ok);
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* save ptr */
|
|
if (v->kind == N_STRUCTLIT && u && u->kind == TY_STRUCT) {
|
|
for (Node *f = v->list; f; f = f->next) {
|
|
u64 foff = 0;
|
|
int fsz = 8;
|
|
Type *ftype = NULL;
|
|
for (Tfield *fl = u->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, f->str) == 0) {
|
|
foff = fl->offset;
|
|
fsz = (int)(fl->type ? fl->type->size : 8);
|
|
ftype = fl->type;
|
|
break;
|
|
}
|
|
}
|
|
cgexpr(c, f->lhs, locals); /* AX or (AX,BX) or X0 */
|
|
int f_isf32 = 0;
|
|
if (fld_isfloat(ftype, &f_isf32)) {
|
|
int mov = f_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BX, (int)foff));
|
|
continue;
|
|
}
|
|
/* str IS []u8: cgexpr leaves (AX=ptr, BX=len,
|
|
* CX=cap). Route the heap base through DX so all
|
|
* three survive — CX now holds cap, BX holds len
|
|
* (#1/Phase 3). */
|
|
Type *fu = (ftype && ftype->kind == TY_NAMED)
|
|
? ftype->under : ftype;
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_DX, (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_DX, (int)foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_DX, (int)foff + 16));
|
|
continue;
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
|
|
int op = A_MOVQ;
|
|
if (fsz == 1) op = A_MOVB;
|
|
else if (fsz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX), amem(D_BX, (int)foff));
|
|
}
|
|
} else {
|
|
cgexpr(c, v, locals); /* AX = value */
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
|
|
int op = A_MOVQ;
|
|
if (sz == 1) op = A_MOVB;
|
|
else if (sz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX), amem(D_BX, 0));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_DX)); /* DX = success ptr */
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
label(c, alloc_done);
|
|
break;
|
|
}
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
strcmp(n->lhs->str, "append") == 0 && n->list &&
|
|
n->list->next) {
|
|
/* append(s, v) lowering — Hare's rt::ensure model.
|
|
* ; AX = value
|
|
* ; PUSHQ AX ; save
|
|
* ; ADDQ $1, sn_off+8(BP) ; s.len += 1
|
|
* ; LEAQ sn_off(BP), DI ; arg1 = &s
|
|
* ; MOVQ esz, SI ; arg2 = membsz
|
|
* ; CALL rt_ensure(SB) ; may realloc s.ptr
|
|
* ; MOVQ sn_off+8(BP), CX ; CX = new len
|
|
* ; SUBQ $1, CX ; slot index
|
|
* ; [IMULQ esz, CX] ; byte offset (esz>1)
|
|
* ; MOVQ sn_off(BP), BX ; reread s.ptr
|
|
* ; ADDQ CX, BX ; BX = target
|
|
* ; POPQ AX ; v
|
|
* ; MOV* AX, (BX) ; store (MOVB / MOVQ)
|
|
*
|
|
* Spread form `append(s, items...)` runs this same body
|
|
* in a counted loop over items. */
|
|
Node *sn = n->list;
|
|
Type *st = sn->type;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int esz = (su && su->sub) ? (int)su->sub->size : 1;
|
|
Type *esub = su ? su->sub : NULL;
|
|
int sn_off = (sn->kind == N_IDENT)
|
|
? localfind(locals, sn->str) : 0;
|
|
int store_op = fldstoreop(esub, esz);
|
|
for (Node *vn = sn->next; vn; vn = vn->next) {
|
|
if (vn->kind == N_SPREAD &&
|
|
vn->lhs && vn->lhs->kind == N_IDENT) {
|
|
int it_off = localfind(locals, vn->lhs->str);
|
|
int load_op = fldloadop(esub, esz);
|
|
/* push counter (i) on stack */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, A_MOVQ, aimm(0), amem(D_SP, 0));
|
|
char *ll = mklabel(c, "spr_l");
|
|
char *le = mklabel(c, "spr_e");
|
|
label(c, ll);
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_BP, it_off + 8), areg(D_DX));
|
|
ins2(c, A_CMPQ, areg(D_DX), areg(D_CX));
|
|
ins1(c, A_JGE, abranch(le));
|
|
/* AX = items.ptr[i] */
|
|
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
|
|
/* ensure + store one element */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
|
|
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
|
|
ins1(c, A_CALL, masym(c, "rt_ensure"));
|
|
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
|
|
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
/* loop tail */
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_SP, 0));
|
|
ins1(c, A_JMP, abranch(ll));
|
|
label(c, le);
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
continue;
|
|
}
|
|
cgexpr(c, vn, locals); /* val → AX */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
|
|
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
|
|
ins1(c, A_CALL, masym(c, "rt_ensure"));
|
|
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
|
|
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* up to 6 integer + 8 float args via SysV registers.
|
|
* str args occupy two integer eightbytes (ptr, len). The
|
|
* arg-buffer cap accommodates Hare-style variadic gather
|
|
* (`fmt.println(a, b, c, ...)`) where N args of element
|
|
* type T fold into a single []T slice slot below. */
|
|
int argcount = 0;
|
|
Node *args[64] = {0};
|
|
for (Node *a = n->list; a; a = a->next)
|
|
if (argcount < 64) args[argcount++] = a;
|
|
/* Resolve callee fn-type so we can match each arg against
|
|
* its declared parameter type — needed to detect implicit
|
|
* widening of a concrete variant into a tagged-union slot. */
|
|
Type *callee_t = n->lhs ? n->lhs->type : NULL;
|
|
Type *cu = (callee_t && callee_t->kind == TY_NAMED) ?
|
|
callee_t->under : callee_t;
|
|
Tparam *callee_params = (cu && cu->kind == TY_FN) ?
|
|
cu->params : NULL;
|
|
/* Hare-style variadic last param: gather N tail args into a
|
|
* stack-resident []T or forward an `xs...` spread, then
|
|
* splice in a single slice arg so the downstream widen/push/
|
|
* pop machinery sees one 24B slice slot for the variadic.
|
|
*
|
|
* Forward shape: `f(... , xs...)` becomes `f(... , xs)`.
|
|
* Gather shape: `f(... , e0, e1, eN)` materialises e0..eN
|
|
* into a frame-resident `[N]T` (widening each element when T
|
|
* is a tagged union), writes a 24B slice descriptor
|
|
* {ptr=&data, len=N, cap=N}, and replaces the tail args with
|
|
* an N_IDENT pointing at the descriptor. Empty form
|
|
* (`f(...)` with no variadic args) writes {0, 0, 0}. */
|
|
{
|
|
int nfixed = 0;
|
|
Tparam *var_p = NULL;
|
|
for (Tparam *p = callee_params; p; p = p->next) {
|
|
if (p->variadic) { var_p = p; break; }
|
|
nfixed++;
|
|
}
|
|
if (var_p != NULL) {
|
|
int nvar = argcount - nfixed;
|
|
if (nvar < 0) nvar = 0;
|
|
int forwarding = (nvar == 1 && args[nfixed] &&
|
|
args[nfixed]->kind == N_SPREAD);
|
|
if (forwarding) {
|
|
args[nfixed] = args[nfixed]->lhs;
|
|
argcount = nfixed + 1;
|
|
} else {
|
|
Type *vst = var_p->type;
|
|
Type *vsu = (vst && vst->kind == TY_NAMED)
|
|
? vst->under : vst;
|
|
Type *velem = (vsu && vsu->kind == TY_SLICE)
|
|
? vsu->sub : NULL;
|
|
int esz = (velem && velem->size)
|
|
? (int)velem->size : 8;
|
|
/* Allocate dname BEFORE sname so the
|
|
* descriptor lives below the element
|
|
* buffer, matching wwstage's emit-time
|
|
* order (rule 10). */
|
|
int doff = 0;
|
|
if (nvar > 0) {
|
|
const char *dname = mklabel(c, "vararg_d");
|
|
doff = localoff(c, &locals,
|
|
dname, nvar * esz, cg_frame);
|
|
}
|
|
const char *slname = mklabel(c, "vararg_sl");
|
|
/* #60: route slice-descriptor width through
|
|
* vsu->size so a future slice-header bump
|
|
* propagates (mirrors wwstage cgcall vararg
|
|
* gather using tyslicesize()). */
|
|
int sloff = localoff(c, &locals,
|
|
slname, (int)vsu->size, cg_frame);
|
|
if (nvar > 0) {
|
|
int v_is_tagged = velem &&
|
|
tagged_arg_size(velem) > 0;
|
|
int v_is_str = type_isstr(velem);
|
|
int v_is_slice = type_isslice(velem);
|
|
for (int j = 0; j < nvar; j++) {
|
|
Node *a = args[nfixed + j];
|
|
int slot = doff + j * esz;
|
|
if (v_is_tagged) {
|
|
cg_widen_tagged_store(c,
|
|
&locals, velem,
|
|
a, D_BP, slot, esz);
|
|
continue;
|
|
}
|
|
cgexpr(c, a, locals);
|
|
/* str / slice element: cgexpr
|
|
* returns the full descriptor in
|
|
* AX/(BX)/(CX); a bare MOVQ AX
|
|
* stores .ptr only and the
|
|
* trailing fields read stack
|
|
* garbage at the callee. */
|
|
if (v_is_str) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, slot));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, slot + 8));
|
|
continue;
|
|
}
|
|
if (v_is_slice) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, slot));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, slot + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, slot + 16));
|
|
continue;
|
|
}
|
|
int op = A_MOVQ;
|
|
if (esz == 1) op = A_MOVB;
|
|
else if (esz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, slot));
|
|
}
|
|
}
|
|
if (nvar > 0)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, doff),
|
|
areg(D_AX));
|
|
else
|
|
ins2(c, A_XORQ, areg(D_AX),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sloff + 0));
|
|
ins2(c, A_MOVQ, aimm(nvar),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sloff + 8));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sloff + 16));
|
|
Node *sn = newnode(c->a, N_IDENT, n->pos);
|
|
sn->str = slname;
|
|
sn->strlen = 0;
|
|
sn->type = vst;
|
|
args[nfixed] = sn;
|
|
argcount = nfixed + 1;
|
|
}
|
|
}
|
|
}
|
|
/* widen[i]: param is tagged and arg needs re-layout.
|
|
* - arg is a concrete variant (str/struct/scalar) — wrap
|
|
* in the param's slot shape.
|
|
* - arg is itself a tagged union of a subset/different
|
|
* variant set — copy the slot words and remap the tag.
|
|
* Identical types pass through unchanged. */
|
|
int widen[64] = {0};
|
|
int widen_sz[64] = {0};
|
|
Type *widen_param[64] = {0};
|
|
{
|
|
Tparam *p = callee_params;
|
|
for (int i = 0; i < argcount; i++) {
|
|
if (p == NULL) break;
|
|
Type *at = args[i] ? args[i]->type : NULL;
|
|
int psz = tagged_arg_size(p->type);
|
|
if (psz > 0) {
|
|
Type *pu = (p->type && p->type->kind == TY_NAMED)
|
|
? p->type->under : p->type;
|
|
Type *au = (at && at->kind == TY_NAMED)
|
|
? at->under : at;
|
|
int same = (pu == au) || type_eq(p->type, at);
|
|
if (!same) {
|
|
widen[i] = 1;
|
|
widen_sz[i] = psz;
|
|
widen_param[i] = p->type;
|
|
}
|
|
}
|
|
p = p->next;
|
|
}
|
|
}
|
|
/* eval right-to-left, push to stack. Each N_IDENT fast-path
|
|
* is guarded by !widen[i] so the tagged-union widening (which
|
|
* needs to synthesise tag + payload + pad) takes precedence
|
|
* over the verbatim slice/struct/tagged-ident loads below. */
|
|
for (int i = argcount - 1; i >= 0; i--) {
|
|
if (!widen[i] && node_isslice(args[i]) && args[i]->kind == N_IDENT) {
|
|
int off = localfind(locals, args[i]->str);
|
|
/* push cap, len, ptr (top) so pops give ptr,len,cap */
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
continue;
|
|
}
|
|
if (!widen[i] && args[i]->kind == N_SLICE) {
|
|
Node *base = args[i]->lhs;
|
|
Node *lo = args[i]->rhs;
|
|
Node *hi = args[i]->cond;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ?
|
|
bt->under : bt;
|
|
/* N_IDENT-gated: non-ident bases stay esz=1
|
|
* (unscaled), byte-id with wwstage which has no
|
|
* tnode there (rule 10) -- #76 residual, non-
|
|
* ident cluster #74. */
|
|
int esz = (base && base->kind == N_IDENT
|
|
&& bu && bu->sub)
|
|
? (int)bu->sub->size : 1;
|
|
/* base addr → push */
|
|
if (base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal && bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr(c, base, locals);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
/* hi (default base length) → push */
|
|
if (hi) cgexpr(c, hi, locals);
|
|
else if (bu && bu->kind == TY_ARRAY)
|
|
cgexpr_int(c, (long long)bu->alen);
|
|
else if (base->kind == N_IDENT && bu &&
|
|
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_CX, 8), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
/* lo (default 0) → AX */
|
|
if (lo) cgexpr(c, lo, locals);
|
|
else cgexpr_int(c, 0);
|
|
ins1(c, A_POPQ, areg(D_BX)); /* hi */
|
|
ins1(c, A_POPQ, areg(D_CX)); /* base */
|
|
/* len = hi - lo (DX) */
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_DX));
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_DX));
|
|
/* ptr = base + lo*esz (#76; ensure.ha:30
|
|
* membsz-unit). BX=lo*esz; AX=lo PRESERVED
|
|
* for cap. BX (dead hi) reloaded by cap below. */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_BX), areg(D_CX));
|
|
} else {
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
/* push cap, len, ptr (top). cap = base_cap - lo
|
|
* (#20); AX=lo, BX free. */
|
|
if (cg_base_cap(c, base, bu, locals, D_BX)) {
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* cap */
|
|
} else {
|
|
ins1(c, A_PUSHQ, areg(D_DX)); /* cap = len */
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_DX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* ptr */
|
|
continue;
|
|
}
|
|
if (!widen[i] && node_isstructarg(args[i]) && args[i]->kind == N_IDENT) {
|
|
/* load qword(s) directly from the struct's slot */
|
|
int off = localfind(locals, args[i]->str);
|
|
int sz = struct_arg_size(args[i]->type);
|
|
if (sz > 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
continue;
|
|
}
|
|
if (!widen[i] && node_istaggedarg(args[i]) && args[i]->kind == N_IDENT) {
|
|
/* Tagged-union: push each 8B word from the slot.
|
|
* High word goes first so the popper drains them
|
|
* in low→high order into the arg-register class. */
|
|
int off = localfind(locals, args[i]->str);
|
|
int sz = tagged_arg_size(args[i]->type);
|
|
int nwords = sz / 8;
|
|
for (int k = nwords - 1; k >= 0; k--) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + k*8),
|
|
areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
continue;
|
|
}
|
|
if (widen[i]) {
|
|
/* Concrete → tagged-union widening at the call
|
|
* site. Mirrors the let/assign/return widening:
|
|
* lay out the value in the parameter's slot
|
|
* shape, then push high→low so pop drains tag
|
|
* first.
|
|
*
|
|
* Branches by source shape:
|
|
* - nullable (sz==8): pointer IS the disc.
|
|
* - str: tag@+0, ptr@+8, len@+16.
|
|
* - struct ident: copy struct words then
|
|
* prepend tag, zero-pad to slot size.
|
|
* - struct literal: materialise via a stack
|
|
* scratch slot — store each field at its
|
|
* struct-relative offset (with the +8 tag
|
|
* shift), zero-fill, then push from slot.
|
|
* - tagged source: load src slot words, remap
|
|
* the tag word via cg_widen_tag_remap, pad
|
|
* to wider dst slot, push.
|
|
* - scalar: tag@+0, value@+8, optional pad. */
|
|
cg_widen_tagged_push(c, &locals, widen_param[i],
|
|
args[i], widen_sz[i]);
|
|
continue;
|
|
}
|
|
cgexpr(c, args[i], locals);
|
|
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])) {
|
|
/* str IS []u8: cgexpr left (AX=ptr, BX=len,
|
|
* CX=cap). Push the triple, same as slice
|
|
* (#1/Phase 3). */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
|
|
} else if (node_isslice(args[i])) {
|
|
/* Slice-typed arg without a fast path above
|
|
* (e.g. `s: []u8` cast): cgexpr left
|
|
* (AX=ptr, BX=len, CX=cap). Push the triple. */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
|
|
} else if (node_istaggedarg(args[i])) {
|
|
/* Tagged-return ABI: AX=tag, DX=val0,
|
|
* CX=val1, R8=val2. Push high-to-low so pop
|
|
* drains tag first (into arg-reg[0]), then
|
|
* values into arg-reg[1..]. Nullable (sz=8):
|
|
* AX holds the pointer, no value-word
|
|
* registers — push just AX. */
|
|
int sz = tagged_arg_size(args[i]->type);
|
|
if (sz > 24)
|
|
ins1(c, A_PUSHQ, areg(D_R8));
|
|
if (sz > 16)
|
|
ins1(c, A_PUSHQ, areg(D_CX));
|
|
if (sz > 8)
|
|
ins1(c, A_PUSHQ, areg(D_DX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
} else {
|
|
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])) {
|
|
/* str IS []u8: 3-word arg, same as slice (#1/Phase 3). */
|
|
for (int k = 0; k < 3; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if (node_isslice(args[i])) {
|
|
for (int k = 0; k < 3; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if (node_isstructarg(args[i])) {
|
|
int 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));
|
|
/* str IS []u8: callee returns AX=ptr, BX=len, CX=cap —
|
|
* same as a slice, no receive-side shuffle (#1/Phase 3). */
|
|
break;
|
|
}
|
|
case N_MATCH: {
|
|
/* match on a tagged-union scrutinee. Read tag and value from
|
|
* the slot. Dispatch by the resolved variant index of each
|
|
* case's type pattern — case order is independent of variant
|
|
* declaration order. A case with no pattern (`case =>`) is a
|
|
* default arm; its body always runs.
|
|
*
|
|
* Slot layout: [+0]=tag, [+8]=value0, [+16]=value1. The third
|
|
* word is only meaningful for variants whose payload is >8B
|
|
* (e.g. str). Bindings sized 16B (str) copy two words.
|
|
*
|
|
* Nullable folded `(*T | void)`: slot is one 8B word holding
|
|
* the pointer; null IS the void variant. Discriminator =
|
|
* value, not a separate tag. */
|
|
Node *s = n->lhs;
|
|
Type *st = s ? s->type : NULL;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int is_nullable = type_isnullable(st);
|
|
int slot_size = (su && su->kind == TY_TAGGED) ? (int)su->size : 16;
|
|
int sl_off;
|
|
if (s->kind == N_IDENT) {
|
|
sl_off = localfind(locals, s->str);
|
|
} else if (s->kind == N_DOT && s->lhs && s->lhs->kind == N_IDENT
|
|
&& s->lhs->type) {
|
|
/* `match (p.field)` — point sl_off at the field's slot
|
|
* inside the parent struct. The slot layout (tag at +0,
|
|
* value words at +8/+16) is contiguous within the struct,
|
|
* so no spill is needed. */
|
|
Type *bt = s->lhs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
Tfield *f = NULL;
|
|
if (bu && bu->kind == TY_STRUCT) {
|
|
for (Tfield *fl = bu->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, s->str) == 0) {
|
|
f = fl; break;
|
|
}
|
|
}
|
|
}
|
|
if (f) {
|
|
int boff = localfind(locals, s->lhs->str);
|
|
sl_off = boff + (int)f->offset;
|
|
} else {
|
|
/* fall back to spill — `match (h.e)` where
|
|
* h is *struct. cgexpr → cgdot now leaves the
|
|
* AX=tag, DX=val0, CX=val1[, R8=val2] shape
|
|
* (task #28), so spill all words the variant
|
|
* may carry. Pre-#28 only AX landed and the
|
|
* dispatch fired on a stale slot. */
|
|
sl_off = localoff(c, &locals, "@match_spill",
|
|
slot_size, cg_frame);
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sl_off + 0));
|
|
if (!is_nullable) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
if (slot_size > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, sl_off + 24));
|
|
}
|
|
}
|
|
} else {
|
|
/* Spill non-ident scrutinees (e.g. `match (foo()?)`) into
|
|
* a scratch slot so we can index out the tag/value. The
|
|
* call ABI for tagged returns is AX=tag, DX=value0,
|
|
* CX=value1, R8=value2 — copy each word into the slot.
|
|
* Nullable returns are single-word: AX is the pointer;
|
|
* spill only that. */
|
|
sl_off = localoff(c, &locals, "@match_spill", slot_size,
|
|
cg_frame);
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
|
if (!is_nullable) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
if (slot_size > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, sl_off + 24));
|
|
}
|
|
}
|
|
char *end = mklabel(c, "match_end");
|
|
/* Push the end label as the yield target for arm bodies. */
|
|
if (nyields < YIELD_MAX) {
|
|
yield_target[nyields++] = end;
|
|
}
|
|
for (Node *cs = n->list; cs; cs = cs->next) {
|
|
char *next = mklabel(c, "match_next");
|
|
/* Per-arm scope: save the locals head, restore it
|
|
* after the body runs. Mirrors check.c's saved/restore
|
|
* around cstmt — the case bind (and any lets inside
|
|
* the arm) shouldn't leak past the arm, where a
|
|
* matching outer name would otherwise resolve to the
|
|
* shadow instead of the original. */
|
|
Local *arm_locals_saved = locals;
|
|
if (cs->type != NULL) {
|
|
int tag = cg_tag_for_variant(su, cs->type);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
if (is_nullable) {
|
|
/* discriminator = pointer-vs-null.
|
|
* *T variant: skip if ptr == 0.
|
|
* void variant: skip if ptr != 0. */
|
|
int ptr_tag = nullable_ptr_tag(su);
|
|
int want_ptr = (tag == ptr_tag);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
if (want_ptr)
|
|
ins1(c, A_JE, abranch(next));
|
|
else
|
|
ins1(c, A_JNE, abranch(next));
|
|
} else if (cs->list != NULL) {
|
|
/* Multi-pattern `case T1 | T2 | ... =>`:
|
|
* if the tag matches any of the alts,
|
|
* jump to body; otherwise to the next
|
|
* case. */
|
|
char *body = mklabel(c, "match_body");
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag),
|
|
areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
for (Node *alt = cs->list; alt;
|
|
alt = alt->next) {
|
|
int atag = cg_tag_for_variant(
|
|
su, alt->type);
|
|
ins2(c, A_CMPQ,
|
|
aimm(atag < 0 ? 0 : atag),
|
|
areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
}
|
|
ins1(c, A_JMP, abranch(next));
|
|
label(c, body);
|
|
} else {
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag),
|
|
areg(D_AX));
|
|
ins1(c, A_JNE, abranch(next));
|
|
}
|
|
}
|
|
if (cs->str && cs->str[0] && cs->type) {
|
|
Type *bt = cs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
if (is_nullable) {
|
|
/* Bind *T or void to a local. The
|
|
* value IS the slot's pointer word; no
|
|
* payload to copy. void binding is
|
|
* unusable (size 0), so only emit for
|
|
* the *T variant. local_alloc (not
|
|
* localoff): the bind must NEVER reuse
|
|
* an outer same-named slot. */
|
|
if (bu && bu->kind == TY_PTR) {
|
|
int voff = local_alloc(c, &locals,
|
|
cs->str, 8, cg_frame);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, sl_off + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, voff));
|
|
}
|
|
} else {
|
|
/* #43: route through Type.size SSoT rather
|
|
* than re-asserting 16/24 for str/slice. */
|
|
int bsz = 8;
|
|
if (bu) bsz = (int)bu->size;
|
|
if (bsz <= 0) bsz = 8;
|
|
/* local_alloc to dodge name-collision
|
|
* dedup — a 16B str bind shadowing an
|
|
* 8B outer would otherwise overflow
|
|
* into the saved BP. */
|
|
int voff = local_alloc(c, &locals, cs->str,
|
|
bsz, cg_frame);
|
|
int nwords = (bsz + 7) / 8;
|
|
for (int w = 0; w < nwords; w++) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, sl_off + 8 + 8*w),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, voff + 8*w));
|
|
}
|
|
}
|
|
}
|
|
cgstmt(c, cs->body, &locals, cg_frame);
|
|
locals = arm_locals_saved;
|
|
ins1(c, A_JMP, abranch(end));
|
|
label(c, next);
|
|
}
|
|
label(c, end);
|
|
if (nyields > 0) nyields--;
|
|
break;
|
|
}
|
|
case N_TRYPROP: {
|
|
/* Evaluate tagged value: AX=tag, DX=value0[, CX=value1].
|
|
* If the tag matches an error variant, propagate as the
|
|
* current function's return (with a tag remap to the
|
|
* enclosing fn's variant order). On success, unwrap to the
|
|
* success-variant ABI: ≤8B values in AX; str values in
|
|
* (AX=ptr, BX=len).
|
|
*
|
|
* Nullable: AX is the pointer; *T variant is the success
|
|
* (any non-null), void variant is the error (null). The
|
|
* enclosing fn's null encoding is the same — RET with AX=0
|
|
* if propagating; otherwise leave AX as-is on success. */
|
|
cgexpr(c, n->lhs, locals);
|
|
Type *u = n->lhs ? n->lhs->type : NULL;
|
|
if (u && u->kind == TY_NAMED) u = u->under;
|
|
Type *r = cg_ret_type;
|
|
if (r && r->kind == TY_NAMED) r = r->under;
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
char *cont = mklabel(c, "tryprop_ok");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(cont));
|
|
/* null = error: propagate. AX already 0; matches
|
|
* the enclosing nullable encoding if it has one. */
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
label(c, cont);
|
|
break;
|
|
}
|
|
int s_tag = cg_tagged_success_tag(u);
|
|
Type *succ_t = NULL;
|
|
if (u && u->kind == TY_TAGGED) {
|
|
int i = 0;
|
|
for (Tparam *p = u->params; p; p = p->next, i++)
|
|
if (i == s_tag) { succ_t = p->type; break; }
|
|
}
|
|
int success_is_str = type_isstr(succ_t);
|
|
char *cont = mklabel(c, "tryprop_ok");
|
|
ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX));
|
|
ins1(c, A_JE, abranch(cont));
|
|
if (u && r && r->kind == TY_TAGGED && u->params) {
|
|
/* Same-shape unions remap every variant to itself, so
|
|
* the loop emits no JMPs. Skip propret entirely then —
|
|
* wwstage doesn't emit a dead label either (CLAUDE.md
|
|
* rule 10, task #18). */
|
|
char *propret = NULL;
|
|
int i = 0;
|
|
for (Tparam *p = u->params; p; p = p->next, i++) {
|
|
if (!cg_variant_is_error(u, i)) continue;
|
|
int j = cg_tag_for_variant(r, p->type);
|
|
if (j < 0) j = 0;
|
|
if (j == i) continue;
|
|
char *skip = mklabel(c, "tryprop_skip");
|
|
ins2(c, A_CMPQ, aimm(i), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(skip));
|
|
ins2(c, A_MOVQ, aimm(j), areg(D_AX));
|
|
if (propret == NULL)
|
|
propret = mklabel(c, "tryprop_ret");
|
|
ins1(c, A_JMP, abranch(propret));
|
|
label(c, skip);
|
|
}
|
|
if (propret != NULL)
|
|
label(c, propret);
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
label(c, cont);
|
|
if (success_is_str) {
|
|
/* str IS []u8: success value arrives in the tagged
|
|
* ABI as DX=ptr, CX=len, R8=cap (slot 32B). Move len
|
|
* out before cap overwrites CX (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_R8), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case N_TRYUNW: {
|
|
/* On error variant: exit(1) directly via the syscall.
|
|
* Nullable: null = error; non-null = success (AX is the
|
|
* pointer, ready to use). */
|
|
cgexpr(c, n->lhs, locals);
|
|
Type *u = n->lhs ? n->lhs->type : NULL;
|
|
if (u && u->kind == TY_NAMED) u = u->under;
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
char *cont = mklabel(c, "tryunw_ok");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(cont));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, cont);
|
|
break;
|
|
}
|
|
int s_tag = cg_tagged_success_tag(u);
|
|
Type *succ_t = NULL;
|
|
if (u && u->kind == TY_TAGGED) {
|
|
int i = 0;
|
|
for (Tparam *p = u->params; p; p = p->next, i++)
|
|
if (i == s_tag) { succ_t = p->type; break; }
|
|
}
|
|
int success_is_str = type_isstr(succ_t);
|
|
char *cont = mklabel(c, "tryunw_ok");
|
|
ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX));
|
|
ins1(c, A_JE, abranch(cont));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, cont);
|
|
if (success_is_str) {
|
|
/* str IS []u8: success arrives DX=ptr, CX=len, R8=cap
|
|
* (slot 32B). Move len out before cap clobbers CX
|
|
* (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_R8), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case N_TYPETEST: {
|
|
/* `e is T` — Compare scrutinee tag against T's variant index.
|
|
* Result is bool (0/1) in AX. Nullable: discriminator is
|
|
* pointer-vs-null, not a tag. */
|
|
cgexpr(c, n->lhs, locals);
|
|
Type *u = n->lhs ? n->lhs->type : NULL;
|
|
if (u && u->kind == TY_NAMED) u = u->under;
|
|
Type *vt = n->rhs ? n->rhs->type : NULL;
|
|
char *ne = mklabel(c, "is_ne");
|
|
char *done = mklabel(c, "is_done");
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
int ptr_tag = nullable_ptr_tag(u);
|
|
int want_ptr = (tag == ptr_tag);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
if (want_ptr)
|
|
ins1(c, A_JE, abranch(ne));
|
|
else
|
|
ins1(c, A_JNE, abranch(ne));
|
|
} else {
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(ne));
|
|
}
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(done));
|
|
label(c, ne);
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
label(c, done);
|
|
break;
|
|
}
|
|
case N_TYPEASSERT: {
|
|
/* `e as T` — abort if tag != T's variant index; otherwise
|
|
* unwrap value to T's ABI: scalar/ptr variants land in AX;
|
|
* 16B str variants in AX:BX.
|
|
*
|
|
* We need both tag *and* value words. For an N_IDENT local
|
|
* the value lives at slot+8/+16 — cgexpr's single-MOVQ path
|
|
* does not load it. Mirror match's pattern: resolve a slot
|
|
* offset (existing local or a fresh @asrt_spill) and index
|
|
* out tag/value from memory.
|
|
*
|
|
* Nullable: the slot's word IS the pointer. *T variant
|
|
* asserts non-null; void variant asserts null. The value
|
|
* left in AX after the check is the pointer itself. */
|
|
Node *s = n->lhs;
|
|
Type *st = s ? s->type : NULL;
|
|
Type *u = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
Type *vt = n->type;
|
|
/* Enum ↔ integer: reinterpret-only. The value already lives
|
|
* in AX after evaluating the LHS; no tag/unwrap needed. */
|
|
{
|
|
Type *vu = (vt && vt->kind == TY_NAMED) ? vt->under : vt;
|
|
if ((u && u->kind == TY_ENUM) ||
|
|
(vu && vu->kind == TY_ENUM)) {
|
|
cgexpr(c, s, locals);
|
|
break;
|
|
}
|
|
}
|
|
int slot_size = (u && u->kind == TY_TAGGED) ? (int)u->size : 16;
|
|
int sl_off = 0;
|
|
if (s && s->kind == N_IDENT && s->str) {
|
|
sl_off = localfind(locals, s->str);
|
|
}
|
|
if (sl_off == 0) {
|
|
sl_off = localoff(c, &locals, "@asrt_spill",
|
|
slot_size, cg_frame);
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
|
if (!(u && u->kind == TY_TAGGED && u->nullable)) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
}
|
|
}
|
|
char *ok = mklabel(c, "asrt_ok");
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
int ptr_tag = nullable_ptr_tag(u);
|
|
int want_ptr = (tag == ptr_tag);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
if (want_ptr)
|
|
ins1(c, A_JNE, abranch(ok));
|
|
else
|
|
ins1(c, A_JE, abranch(ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, ok);
|
|
/* AX already holds the pointer (or 0 for void
|
|
* variant, where the result type has size 0 and
|
|
* no consumer reads it). */
|
|
break;
|
|
}
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
|
|
ins1(c, A_JE, abranch(ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, ok);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 8), areg(D_AX));
|
|
if (type_isstr(vt))
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 16), areg(D_BX));
|
|
break;
|
|
}
|
|
case N_CAST: {
|
|
int from_f = node_isfloat(n->lhs);
|
|
int to_f = cg_isfloat(n->type);
|
|
int from_f32 = node_isf32(n->lhs);
|
|
int to_f32 = type_isf32(n->type);
|
|
cgexpr(c, n->lhs, locals); /* AX or X0 depending */
|
|
if (from_f && !to_f) {
|
|
int op = from_f32 ? A_CVTTSS2SI : A_CVTTSD2SI;
|
|
ins2(c, op, areg(D_X0), areg(D_AX));
|
|
} else if (!from_f && to_f) {
|
|
int op = to_f32 ? A_CVTSI2SS : A_CVTSI2SD;
|
|
ins2(c, op, areg(D_AX), areg(D_X0));
|
|
} else if (from_f && to_f && from_f32 != to_f32) {
|
|
int op = to_f32 ? A_CVTSD2SS : A_CVTSS2SD;
|
|
ins2(c, op, areg(D_X0), areg(D_X0));
|
|
}
|
|
/* str → []u8 (or any []T): cgexpr left (AX=ptr, BX=len).
|
|
* Slice register convention is (AX=ptr, BX=len, CX=cap);
|
|
* synthesise cap = len so downstream arg-push / let-init
|
|
* paths see the canonical triple. Without this, the cap
|
|
* register stays whatever cgexpr happened to leave there
|
|
* and the receiver reads a stale value. */
|
|
{
|
|
Type *tt = n->type;
|
|
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
|
|
Type *ft = n->lhs ? n->lhs->type : NULL;
|
|
Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft;
|
|
if (tu && tu->kind == TY_SLICE
|
|
&& fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
}
|
|
}
|
|
/* Narrowing integer cast: clamp AX to the target width so
|
|
* downstream 64-bit ops see a value within the declared
|
|
* range. Hare semantics: `expr: T` truncates to T's bit
|
|
* width (mod 2^n). Without this, `(big_u64): u32` left the
|
|
* upper 32 bits intact and CMPQ/DIVQ misread the value.
|
|
*
|
|
* Unsigned targets use MOVL/ANDQ to clear the high bits.
|
|
* Signed-narrow targets (i8/i16/i32) sign-extend via
|
|
* MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates;
|
|
* this is what lets `(0xFF80i64): i8` compare equal to
|
|
* -128i64 after a widening read-back. Symmetric on signed
|
|
* vs unsigned: both branches gate on `type_isint(tu) &&
|
|
* size<8`, then dispatch on type_isunsigned(tu). The
|
|
* recursion through TY_ENUM in type_isunsigned (task #5)
|
|
* is what lets an enum-aliased narrow (`type myflag = i8`)
|
|
* pick up the right MOVS*Q. Wwstage's cgcast keys off the
|
|
* resolved type-name through the same shape. TY_RUNE is
|
|
* unsigned (Unicode scalar) and lands on the MOVL path. */
|
|
if (!from_f && !to_f && n->type) {
|
|
Type *tt = n->type;
|
|
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
|
|
/* Identity-width identity-sign cast is a no-op at the
|
|
* machine-int level: src and dst share both width and
|
|
* signedness, so the natural slot/load already carries
|
|
* the right canonical 64-bit shape and the narrow-clamp
|
|
* is dead. Replaces b5632b1's single-site `!dst_is_enum`
|
|
* gate (task #25) which mirrored wwstage's N_TENUM
|
|
* lacuna; the lacuna is fixed there too, so this gate
|
|
* stays symmetric across both stages (#33). Source side
|
|
* uses `castsrcprim` (a structural walk matching
|
|
* wwstage's exprprimresolved exactly), NOT n->lhs->type
|
|
* — cstage's checker has richer type info than wwstage
|
|
* can derive without a checker, and the asymmetric
|
|
* coverage broke 995_self_rebuild's byte-id. The cost
|
|
* is that some casts (`.len: i32`, N_BIN result, call
|
|
* return, match-bound payload) still emit a redundant
|
|
* clamp on both stages; closing those gaps is a
|
|
* sibling task that extends wwstage's type inference.
|
|
* Incidentally fixes a silent miscompile #25's
|
|
* dst-kind-only skip left in place: u32→enum-u8 (and
|
|
* similar narrow-to-enum casts) was suppressing the
|
|
* clamp, so the upper bits of the source value leaked
|
|
* through register-chained downstream uses. Caveat:
|
|
* removing the defensive MOVL exposes any upstream
|
|
* cgen path that leaves garbage in upper RAX when
|
|
* producing a sub-word value — the contract is
|
|
* producers leave the value in canonical width-
|
|
* extended form. */
|
|
int src_w = 0, src_unsignd = 0;
|
|
castsrcprim(n->lhs, &src_w, &src_unsignd);
|
|
int dst_w = (tu && type_isint(tu)) ? (int)tu->size : 0;
|
|
int identity = dst_w > 0 && src_w == dst_w
|
|
&& src_unsignd == type_isunsigned(tu);
|
|
if (tu && type_isint(tu) && tu->size > 0
|
|
&& tu->size < 8 && !identity) {
|
|
if (type_isunsigned(tu)) {
|
|
if (tu->size == 4) {
|
|
ins2(c, A_MOVL,
|
|
areg(D_AX), areg(D_AX));
|
|
} else {
|
|
u64 mask = ((u64)1 << (tu->size * 8)) - 1;
|
|
ins2(c, A_ANDQ,
|
|
aimm((i64)mask),
|
|
areg(D_AX));
|
|
}
|
|
} else {
|
|
int op = A_MOVSXD;
|
|
if (tu->size == 1) op = A_MOVSBQ;
|
|
else if (tu->size == 2) op = A_MOVSWQ;
|
|
ins2(c, op, areg(D_AX), areg(D_AX));
|
|
}
|
|
}
|
|
/* TY_BOOL is size 1 too; clamp to a single byte so
|
|
* `(u32_val): bool` produces 0 or a low-byte value
|
|
* instead of leaking the upper bits. type_isint(bool)
|
|
* is false, so the symmetric narrow above misses it
|
|
* — this dedicated branch covers the bool case. */
|
|
if (tu && tu->kind == TY_BOOL) {
|
|
ins2(c, A_ANDQ, aimm(0xFF), areg(D_AX));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case N_DOT: {
|
|
/* slice/str pseudo-fields: .ptr (offset 0), .len (8), .cap (16).
|
|
* Arrays don't carry a header; .len uses the static size and
|
|
* .ptr is the address of the first element. */
|
|
/* `(*p).f` read retarget: parser produces n->lhs = N_UN(STAR,
|
|
* IDENT(p)) with type T (post-deref struct). Pull the inner
|
|
* IDENT in as dot_lhs so bt resolves to *T and the pointer-
|
|
* auto-deref branch below fires (mirror of the N_ASSIGN
|
|
* N_DOT lhs retarget). v1 scope: N_IDENT inner only;
|
|
* (*expr).f follow-up task pending. Branches that gate on
|
|
* `n->lhs->kind == N_DOT/N_INDEX/...` keep checking the raw
|
|
* n->lhs since (*p) isn't either of those shapes. */
|
|
Node *dot_lhs = n->lhs;
|
|
if (dot_lhs && dot_lhs->kind == N_UN && dot_lhs->op == TK_STAR
|
|
&& dot_lhs->lhs && dot_lhs->lhs->kind == N_IDENT)
|
|
dot_lhs = dot_lhs->lhs;
|
|
Type *bt = dot_lhs ? dot_lhs->type : NULL;
|
|
/* type_chase_named (#22): `type b = a; type a = struct;` stacks
|
|
* two TY_NAMED layers; single peel left `u` still TY_NAMED,
|
|
* missing the TY_STRUCT field-walk gate below and collapsing
|
|
* `s.field` to a base-only MOVQ read (offset 0 instead of
|
|
* the field's declared offset). */
|
|
Type *u = type_chase_named(bt);
|
|
/* Module-qualified value reference: `mod.name`. The checker
|
|
* leaves SK_USE idents untyped (NULL/ty_err); detect that and
|
|
* look up the leaf in the flat (driver-concatenated) sym/def
|
|
* maps the same way a bare N_IDENT would. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT
|
|
&& (bt == NULL || bt == ty_err)) {
|
|
Type *t = n->type;
|
|
Type *tu = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (tu && tu->kind == TY_FN) {
|
|
/* `mod.fn` address-of via N_DOT — pass the
|
|
* module bareword as the disambiguation hint. */
|
|
ins2(c, A_LEAQ,
|
|
mafn(c, n->str, n->lhs->str), areg(D_AX));
|
|
break;
|
|
}
|
|
{
|
|
/* Same-module-first walk using n->lhs->str as
|
|
* the explicit module hint (sister of wwstage
|
|
* deflookuprhsmod). The TY_FN branch above
|
|
* already uses n->lhs->str via mafn for the
|
|
* cross-module qualifier disambiguation; this
|
|
* walk mirrors that polarity so `alpha.MSG`
|
|
* from a third module beats a head-of-sdefs
|
|
* beta.MSG collision (#11, sister of #4c). */
|
|
Sdef *s;
|
|
for (s = sdefs; s; s = s->next) {
|
|
if (strcmp(s->name, n->str) != 0)
|
|
continue;
|
|
if (sdef_mod_match_hint(s, n->lhs->str))
|
|
break;
|
|
}
|
|
if (s == NULL) {
|
|
for (s = sdefs; s; s = s->next)
|
|
if (strcmp(s->name, n->str) == 0)
|
|
break;
|
|
}
|
|
if (s != NULL) {
|
|
const char *lab = intern_strlit(c,
|
|
s->bytes, s->len);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
aimm((long long)s->len),
|
|
areg(D_BX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
/* Same gating as the bare-ident catch-all: lets route
|
|
* through localloadop (their slot can be the target of
|
|
* a narrow deref-store via `&letname: *iN`); defs and
|
|
* unresolved symbols stay on MOVQ so wwstage's defent-
|
|
* registry-without-tnode shape agrees byte-for-byte. */
|
|
int mqop = let_islet(n->str)
|
|
? localloadop(n->type) : A_MOVQ;
|
|
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);
|
|
/* f64/f32 tuple field must ride X0 via MOVSD/MOVSS;
|
|
* the integer fldloadop left it in AX (#103 FACE Z).
|
|
* Mirrors the struct-field float load at cgen.c:1462,
|
|
* 1838 (the #96 pattern). */
|
|
int tup_isf32 = 0;
|
|
if (fld_isfloat(tp->type, &tup_isf32)) {
|
|
int mov = tup_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov, amem(D_BP, off + foff),
|
|
areg(D_X0));
|
|
break;
|
|
}
|
|
/* str IS []u8 — load (ptr, len, cap) into
|
|
* (AX, BX, CX), the canonical slice-header ABI,
|
|
* so chains like `t.1.len` propagate through the
|
|
* slice-rhs convention (#1/Phase 3 collapse).
|
|
* UNLIKE the field arms there is no slice-element
|
|
* sibling here, so the triple is hand-authored;
|
|
* base is BP (frame, not a target reg) so the
|
|
* canonical ptr/len/cap order has no clobber risk. */
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + foff + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + foff + 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + foff + 16), areg(D_CX));
|
|
break;
|
|
}
|
|
ins2(c, op, amem(D_BP, off + foff), areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
/* real struct field: load at struct_base + field_off.
|
|
* Base is either a local frame slot (off(BP)) or a top-
|
|
* level let global (&name(SB) into CX); we resolve which
|
|
* once and then share the field-walk code. */
|
|
if (u && u->kind == TY_STRUCT && n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
int is_global = 0;
|
|
int base_reg = D_BP;
|
|
int base_disp = off;
|
|
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 IS []u8 — same 3-word {ptr,len,cap} as a slice
|
|
* field: load (ptr, len, cap) into (AX, BX, CX) so the
|
|
* value flows through the slice-rhs convention. str
|
|
* folds onto the slice arm (#1/Phase 3 collapse).
|
|
* base_reg may be CX for globals; load .cap LAST so
|
|
* the base survives the earlier reads. */
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if ((str_fu && str_fu->kind == TY_SLICE) ||
|
|
type_isstr(f->type)) {
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 16),
|
|
areg(D_CX));
|
|
break;
|
|
}
|
|
/* f64/f32 field: route through X0 (MOVSD/MOVSS).
|
|
* Loading via MOVQ AX would put the bits in the
|
|
* integer reg, and any downstream consumer that
|
|
* reads X0 (arg pass, return, arithmetic) would see
|
|
* stale data. */
|
|
int e_isf32 = 0;
|
|
if (fld_isfloat(f->type, &e_isf32)) {
|
|
int mov = e_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(base_reg, base_disp + (int)f->offset),
|
|
areg(D_X0));
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int op = fldloadop(f->type, fsz);
|
|
ins2(c, op,
|
|
amem(base_reg, base_disp + (int)f->offset),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
/* pointer-to-slice/str field: deref and read pseudo-field.
|
|
* Used by helpers like rt_appendu8(s: *[]u8, v: u8). dot_lhs
|
|
* gates the N_IDENT check so `(*p).len` (parser N_UN(STAR,
|
|
* IDENT)) emits the same load as `p.len` after the case-top
|
|
* retarget. */
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *inner = type_chase_named(u->sub);
|
|
if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR)
|
|
&& (lenfld || capfld || ptrfld)
|
|
&& dot_lhs && dot_lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, dot_lhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
|
|
int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
|
|
ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_AX));
|
|
break;
|
|
}
|
|
}
|
|
/* pointer-to-struct field: deref and load. Common pattern:
|
|
* fn move(p: *point) ... { p.x += dx; ... }
|
|
* dot_lhs gates this branch so both `p.f` (n->lhs is IDENT)
|
|
* and `(*p).f` (n->lhs is N_UN(STAR, IDENT), retargeted to
|
|
* inner IDENT at case-top) emit the same load sequence.
|
|
*
|
|
* type_chase_named (#22): `type b = a;` inside the pointer
|
|
* (`*b`) leaves a single peel still at TY_NAMED. Bites the
|
|
* strings.tokenize wrapper shape — caller signature
|
|
* `next_token(s: *strings.tokenizer)` where strings.tokenizer
|
|
* aliases bytes.tokenizer. */
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *inner = type_chase_named(u->sub);
|
|
if (inner && inner->kind == TY_STRUCT
|
|
&& dot_lhs && dot_lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, dot_lhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
|
|
for (Tfield *f = inner->fields; f; f = f->next) {
|
|
if (strcmp(f->name, n->str) != 0) continue;
|
|
/* tagged-union field through *struct: BX
|
|
* already holds the *struct pointer. Load
|
|
* the four payload regs from (BX, f->offset)
|
|
* — BX is not a target (AX/DX/CX/R8), so
|
|
* load order is harmless. Mirrors the direct-
|
|
* struct branch above so consumers see the
|
|
* same tagged-return register shape
|
|
* regardless of pointer rooting. Pre-#28 fell
|
|
* through to fldloadop and dropped the
|
|
* payload words. */
|
|
Type *ptag_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (ptag_fu && ptag_fu->kind == TY_TAGGED) {
|
|
int fo = (int)f->offset;
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 8),
|
|
areg(D_DX));
|
|
if (ptag_fu->size > 16)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 16),
|
|
areg(D_CX));
|
|
if (ptag_fu->size > 24)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 24),
|
|
areg(D_R8));
|
|
break;
|
|
}
|
|
/* str IS []u8 — same 3-word {ptr,len,cap} as a
|
|
* slice field through *struct: load (ptr, len,
|
|
* cap) into (AX, BX, CX). BX holds the *struct
|
|
* pointer, so load .len LAST — the earlier loads
|
|
* still index off the original base. str folds
|
|
* onto the slice arm (#1/Phase 3 collapse). */
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if ((str_fu && str_fu->kind == TY_SLICE) ||
|
|
type_isstr(f->type)) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 8),
|
|
areg(D_BX));
|
|
break;
|
|
}
|
|
/* f64/f32 field via *struct: load into X0.
|
|
* BX already holds the struct pointer from
|
|
* the MOVQ amem(D_BP,off) above. */
|
|
int f_isf32 = 0;
|
|
if (fld_isfloat(f->type, &f_isf32)) {
|
|
int mov = f_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_BX, (int)f->offset),
|
|
areg(D_X0));
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int op = fldloadop(f->type, fsz);
|
|
ins2(c, op,
|
|
amem(D_BX, (int)f->offset),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* Chained N_DOT through a *struct field. cgexpr lhs leaves
|
|
* AX = the inner *struct pointer; load the requested field
|
|
* with a single MOVQ. Without this, returning `o.p.val`
|
|
* silently leaves AX = o.p (the pointer) and the outer
|
|
* cast/use sees the pointer instead of the dereferenced
|
|
* field. (Surfaced building ww-w6l.) */
|
|
if (n->lhs->kind == N_DOT) {
|
|
Type *lt = n->lhs->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
if (lu && lu->kind == TY_PTR && lu->sub) {
|
|
Type *inner = lu->sub;
|
|
if (inner->kind == TY_NAMED) inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
for (Tfield *f = inner->fields; f; f = f->next) {
|
|
if (strcmp(f->name, n->str) != 0) continue;
|
|
cgexpr(c, n->lhs, locals); /* AX = inner ptr */
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
/* str IS []u8 — same 3-word {ptr,len,cap} as a
|
|
* slice field: load (ptr, len, cap) into
|
|
* (AX, BX, CX). AX is the *struct base, so
|
|
* load .ptr (which targets AX) LAST. str folds
|
|
* onto the slice arm (#1/Phase 3 collapse). */
|
|
if ((fu && fu->kind == TY_SLICE) ||
|
|
type_isstr(ft)) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 0),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
/* f64/f32 chained field: read into X0. */
|
|
int g_isf32 = 0;
|
|
if (fld_isfloat(ft, &g_isf32)) {
|
|
int mov = g_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_AX, (int)f->offset),
|
|
areg(D_X0));
|
|
goto dot_done;
|
|
}
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int op = fldloadop(ft, fsz);
|
|
ins2(c, op, amem(D_AX, (int)f->offset),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* `arr[i].field` — element-then-field through a `[N]*S` /
|
|
* `[N]S` (and slice/`*[N]S`) base. One branch covers both
|
|
* shapes: compute `&arr[i]` into BX, then either deref
|
|
* (`*Struct` element) or move-to-AX (value `Struct` element),
|
|
* so the leaf load is `(field.offset)(AX)` either way.
|
|
* Bypasses cgindex deliberately — cgindex's final MOVQ
|
|
* would truncate a value-struct element to 8 bytes. Mirrors
|
|
* selfhost/cmd/wcc/cgenexpr.ww's cgdot N_INDEX-lhs branch. */
|
|
if (n->lhs && n->lhs->kind == N_INDEX && n->lhs->lhs
|
|
&& n->lhs->lhs->kind == N_IDENT) {
|
|
Node *idxbase = n->lhs->lhs;
|
|
Type *elemt = n->lhs->type;
|
|
Type *elemu = (elemt && elemt->kind == TY_NAMED)
|
|
? elemt->under : elemt;
|
|
Type *struct_t = NULL;
|
|
int viaptr = 0;
|
|
if (elemu && elemu->kind == TY_PTR) {
|
|
Type *inner = elemu->sub;
|
|
if (inner && inner->kind == TY_NAMED)
|
|
inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
struct_t = inner;
|
|
viaptr = 1;
|
|
}
|
|
} else if (elemu && elemu->kind == TY_STRUCT) {
|
|
struct_t = elemu;
|
|
}
|
|
if (struct_t) {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = struct_t->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, n->str) == 0)
|
|
{ f = fl; break; }
|
|
Type *bt = idxbase->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
int is_arr = bu && bu->kind == TY_ARRAY;
|
|
int is_sl = bu && bu->kind == TY_SLICE;
|
|
int is_ptr = bu && bu->kind == TY_PTR;
|
|
int off = localfind(locals, idxbase->str);
|
|
if (f != NULL && (is_arr || is_sl || is_ptr)
|
|
&& off != 0) {
|
|
int esz = (int)elemt->size;
|
|
cgexpr(c, n->lhs->rhs, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off), areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0), areg(D_AX));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_AX));
|
|
int foff = (int)f->offset;
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
if (fu && (fu->kind == TY_STR
|
|
|| fu->kind == TY_SLICE)) {
|
|
/* str/slice: the 3-word {ptr,len,cap}
|
|
* slice header (#1). AX holds the
|
|
* element base, so load .ptr (which
|
|
* targets AX) LAST. Matches the
|
|
* caseB *struct slice arm and
|
|
* cgslicehdr(D_AX). */
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, foff + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, foff + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, foff + 0),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
int g_isf32 = 0;
|
|
if (fld_isfloat(ft, &g_isf32)) {
|
|
int mov = g_isf32
|
|
? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_AX, foff),
|
|
areg(D_X0));
|
|
goto dot_done;
|
|
}
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int op = fldloadop(ft, fsz);
|
|
ins2(c, op, amem(D_AX, foff),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
}
|
|
/* Nested module-qualified field where the chain didn't fold to
|
|
* a known shape (typical when w6c runs on a single file with
|
|
* `use mod;` but no driver concatenation — the body's enum /
|
|
* struct hasn't been seen). Emit `MOVQ <leaf>(SB), AX` so the
|
|
* linker surfaces a clean undefined-symbol error on the leaf
|
|
* — mirrors the bare-N_IDENT unresolved fallback used by
|
|
* single-segment N_DOTs. Keeps cstage / wwstage byte-aligned
|
|
* on the cgen-match isolation probes. */
|
|
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;
|
|
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/slice element: load the full (ptr, len, cap) header
|
|
* into (AX, BX, CX) — both are 24B since #1, so the cap
|
|
* word must survive. Kind-gate on type_isstr||type_isslice,
|
|
* never size==24: a >16B struct is 24B+ too but takes the
|
|
* struct-copy path, not this 3-word header load (#10).
|
|
* Base is BX. */
|
|
if (u->sub && (type_isstr(u->sub) || type_isslice(u->sub))) {
|
|
cgslicehdr(c, D_BX);
|
|
break;
|
|
}
|
|
/* tagged element: load slot words into (AX=tag,
|
|
* DX=val0, CX=val1, R8=val2) — matches the
|
|
* tagged-return ABI so let-init / match / call-arg
|
|
* paths consume it without spilling. Nullable folded
|
|
* element is one word in AX (caller treats it as a
|
|
* pointer). */
|
|
if (elem_tagged) {
|
|
int ssz = (int)esubu->size;
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, amem(D_BX, 24),
|
|
areg(D_R8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, amem(D_BX, 16),
|
|
areg(D_CX));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, amem(D_BX, 8),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
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/slice element via fallback base: load the full (ptr, len,
|
|
* cap) header into (AX, BX, CX). Kind-gate on type_isstr||
|
|
* type_isslice, never size==24 (see Site A). Base is AX. */
|
|
if (u && u->sub && (type_isstr(u->sub) || type_isslice(u->sub))) {
|
|
cgslicehdr(c, D_AX);
|
|
break;
|
|
}
|
|
/* tagged element via fallback base: AX holds the element
|
|
* address — copy to BX (the load into AX clobbers it), then
|
|
* load slot words. */
|
|
if (elem_tagged) {
|
|
int ssz = (int)esubu->size;
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
{
|
|
int load_op = fldloadop(esub, esz);
|
|
ins2(c, load_op, amem(D_AX, 0), areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
case N_SLICE: {
|
|
/* base[lo:hi] as a slice value. Leaves the triple in
|
|
* (AX=base+lo*esz, BX=hi-lo, CX=base_cap-lo) so callers can
|
|
* route to a slice slot, return, or arg with the same ABI.
|
|
* cap is the storage remaining to the base's end (#20,
|
|
* Go/Hare-identical), via cg_base_cap. ptr advances by BYTES
|
|
* (lo*esz, #76; ref/hare/rt/ensure.ha:30 membsz-unit); esz
|
|
* from the type table, mirroring the N_INDEX idiom. */
|
|
Node *base = n->lhs;
|
|
Node *lo = n->rhs;
|
|
Node *hi = n->cond;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
/* N_IDENT-gated: non-ident bases stay esz=1 (unscaled),
|
|
* byte-id with wwstage which has no tnode there to resolve
|
|
* esz (rule 10) -- #76 residual, non-ident cluster #74. */
|
|
int esz = (base && base->kind == N_IDENT && bu && bu->sub)
|
|
? (int)bu->sub->size : 1;
|
|
if (base && base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) && let_islet(base->str);
|
|
if (isglobal && bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ, masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
|
|
}
|
|
} else if (base) {
|
|
cgexpr(c, base, locals);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (lo) cgexpr(c, lo, locals);
|
|
else cgexpr_int(c, 0);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (hi) {
|
|
cgexpr(c, hi, locals);
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
cgexpr_int(c, (long long)bu->alen);
|
|
} else if (base && base->kind == N_IDENT && bu &&
|
|
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) && let_islet(base->str);
|
|
if (isglobal) {
|
|
ins2(c, A_LEAQ, masym(c, base->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8),
|
|
areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
/* ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit).
|
|
* DX=lo*esz; CX=lo PRESERVED for len + cap (#20). */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_DX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_DX));
|
|
ins2(c, A_ADDQ, areg(D_DX), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_BX));
|
|
/* cap = base_cap - lo (#20); CX=lo, BX=len here. */
|
|
if (cg_base_cap(c, base, bu, locals, D_DX)) {
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_CX));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
cgexpr_int(c, 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
|
{
|
|
if (n == NULL) return;
|
|
switch (n->kind) {
|
|
case N_BLOCK: {
|
|
/* Save/restore the locals head across the block (post-#27).
|
|
* Inner-scope `let` bindings prepend to *locals via localoff;
|
|
* without this restore, the prepended stubs leak into sibling
|
|
* and ancestor scopes, and localfind (head-first) returns the
|
|
* inner binding's offset for an identifier that semantically
|
|
* belongs to the outer scope. The frame is left grown — slot
|
|
* lifetimes don't overlap with later siblings observably (the
|
|
* popped stubs' offsets are no longer reachable by name), but
|
|
* we don't reclaim the frame bytes; that's the conservative
|
|
* choice C compilers make for simple lowering.
|
|
*
|
|
* cgfn iterates fn->body->list directly to bypass this
|
|
* save/restore at the function's outermost block — defers
|
|
* (and the implicit-return epilogue) need locals intact. */
|
|
Local *saved = *locals;
|
|
for (Node *s = n->list; s; s = s->next)
|
|
cgstmt(c, s, locals, frame);
|
|
*locals = saved;
|
|
break;
|
|
}
|
|
case N_EXPRSTMT:
|
|
cgexpr(c, n->lhs, *locals);
|
|
break;
|
|
case N_LET: {
|
|
Type *lt = n->type;
|
|
/* type_chase_named (#22): a chain `type a = struct{...};
|
|
* type b = a;` stacks two TY_NAMED layers. A single peel
|
|
* left `lu` pointing at the inner alias (still TY_NAMED),
|
|
* collapsed the struct/slice/tagged sizing arms to the 8B
|
|
* fallback, and the slot under-allocated the local. */
|
|
Type *lu = type_chase_named(lt);
|
|
/* #43: every composite kind already has its byte size cached in
|
|
* lu->size; route through it instead of re-asserting 16/24 for
|
|
* str/slice and re-reading for the others. */
|
|
int sz = 8;
|
|
if (lu && (lu->kind == TY_ARRAY || lu->kind == TY_SLICE
|
|
|| lu->kind == TY_STR || lu->kind == TY_STRUCT
|
|
|| lu->kind == TY_TUPLE || lu->kind == TY_TAGGED))
|
|
sz = (int)lu->size;
|
|
int off = localoff(c, locals, n->str, sz, frame);
|
|
int isf = cg_isfloat(lt);
|
|
int isf32 = type_isf32(lt);
|
|
/* alloc([], n) initialiser for a slice local: allocate
|
|
* n*esize bytes, build the {ptr, 0, n} header in the slot.
|
|
* Element size comes from the declared slice type.
|
|
*
|
|
* Task #30 graduated the builtin to `([]T | nomem)`. The let
|
|
* declares a bare `[]T`, so the canonical idiom wraps in `!`
|
|
* (abort on OOM) or `?` (propagate nomem to the enclosing
|
|
* fn's tagged return). Task #45 extends the shortcut to also
|
|
* match N_TRYPROP and emit the propret pattern. */
|
|
{
|
|
Node *call = NULL;
|
|
int via_tryunw = 0;
|
|
int via_tryprop = 0;
|
|
if (n->rhs && n->rhs->kind == N_TRYUNW && n->rhs->lhs
|
|
&& n->rhs->lhs->kind == N_CALL) {
|
|
call = n->rhs->lhs;
|
|
via_tryunw = 1;
|
|
} else if (n->rhs && n->rhs->kind == N_TRYPROP
|
|
&& n->rhs->lhs
|
|
&& n->rhs->lhs->kind == N_CALL) {
|
|
call = n->rhs->lhs;
|
|
via_tryprop = 1;
|
|
}
|
|
if (call && lu && lu->kind == TY_SLICE && sz == 24
|
|
&& call->lhs && call->lhs->kind == N_IDENT
|
|
&& strcmp(call->lhs->str, "alloc") == 0
|
|
&& call->list && call->list->kind == N_ARRLIT
|
|
&& call->list->list == NULL
|
|
&& call->list->next
|
|
&& call->list->next->next == NULL) {
|
|
Node *count = call->list->next;
|
|
int esz = (lu->sub) ? (int)lu->sub->size : 1;
|
|
cgexpr(c, count, *locals); /* AX = n */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* save count */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
|
|
ins2(c, A_IMULQ, areg(D_BX), areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
|
ins1(c, A_CALL, asym(ffi_resolve("malloc")));
|
|
if (via_tryunw) {
|
|
char *ok = mklabel(c, "tryunw_ok");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, ok);
|
|
} else if (via_tryprop) {
|
|
/* #45: null = nomem; propagate to the
|
|
* enclosing fn's tagged return. AX = tag
|
|
* of nomem variant in cg_ret_type; epilogue
|
|
* RETs to caller. */
|
|
char *ok = mklabel(c, "tryprop_ok");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(ok));
|
|
Type *r = cg_ret_type;
|
|
if (r && r->kind == TY_NAMED) r = r->under;
|
|
int nidx = cg_tag_for_variant(r, ty_nomem);
|
|
if (nidx < 0) nidx = 1;
|
|
ins2(c, A_MOVQ, aimm(nidx), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
label(c, ok);
|
|
}
|
|
ins1(c, A_POPQ, areg(D_BX)); /* count */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
}
|
|
/* str IS []u8: cgexpr produces (AX=ptr, BX=len, CX=cap);
|
|
* store all three, same as the slice initialiser below.
|
|
* #43 gate via ty_str->size already tracks the 24B bump
|
|
* (#1/Phase 3). */
|
|
if (n->rhs && type_isstr(lt) && sz == (int)ty_str->size) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
/* 2-tuple initialiser from a function call. An integer word
|
|
* rides its tuple_rseq[] reg (AX, DX); a single f64/f32 word
|
|
* rides X0, the SSE return reg — the RETURN leaves the float
|
|
* in X0 and pushes garbage through that word's integer slot,
|
|
* so a blanket MOVQ-from-integer spill stores garbage and the
|
|
* #103-FACE-Z field read (MOVSD-from-slot) reads it (#105).
|
|
* Spill each word from its real class. Multi-float tuples
|
|
* collide on X0 at the RETURN (#107), out of scope here. */
|
|
if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 16) {
|
|
Tparam *p0 = lu->params;
|
|
Tparam *p1 = p0 ? p0->next : NULL;
|
|
int f0_f32 = 0, f1_f32 = 0;
|
|
int e0_f = p0 && fld_isfloat(p0->type, &f0_f32);
|
|
int e1_f = p1 && fld_isfloat(p1->type, &f1_f32);
|
|
cgexpr(c, n->rhs, *locals);
|
|
if (e0_f)
|
|
ins2(c, f0_f32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_BP, off + 0));
|
|
else
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[0]),
|
|
amem(D_BP, off + 0));
|
|
if (e1_f)
|
|
ins2(c, f1_f32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_BP, off + 8));
|
|
else
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[1]),
|
|
amem(D_BP, off + 8));
|
|
break;
|
|
}
|
|
/* 32B tuple initialiser for `(scalar, str)` / `(str, scalar)`.
|
|
* Per the AX:DX:CX:R8 return convention: AX = scalar elem,
|
|
* DX = str.ptr, CX = str.len, R8 = str.cap. The slot is laid
|
|
* out positionally (str takes 24B at its position), so we route
|
|
* each register to the slot dictated by the element's type, not
|
|
* by AX/DX position. str IS []u8 (24B) → 32B tuple (#1/Phase 3,
|
|
* task #5). */
|
|
if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 32) {
|
|
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 (24B), scalar@+24. */
|
|
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_R8), amem(D_BP, off + 16));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 24));
|
|
} else {
|
|
/* layout: scalar@+0 (8B), str@+8 (24B). */
|
|
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));
|
|
ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, off + 24));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* Tagged-union initialiser. Delegates to cg_widen_tagged_store,
|
|
* which handles nullable fold, tagged→tagged (with tag remap
|
|
* when variant indices differ), struct payload (ident or
|
|
* literal — field-by-field at slot+8+field_off), str payload,
|
|
* and scalar payload (with zero-pad to the slot size). */
|
|
if (n->rhs && lu && lu->kind == TY_TAGGED) {
|
|
cg_widen_tagged_store(c, locals, lu, n->rhs, D_BP, off, sz);
|
|
break;
|
|
}
|
|
/* Every slice initialiser routes here — fn-return, slice
|
|
* ident, slice param, and sub-slice `buf[lo:hi]`. cgexpr
|
|
* leaves (AX=ptr, BX=len, CX=cap); store all three. The
|
|
* sub-slice case once had a vestigial inline builder that
|
|
* duplicated cgexpr's N_SLICE path and mishandled global
|
|
* bases; dropping it aligns cstage onto wwstage's shared
|
|
* store path (find-4). Runs after the alloc specialisation
|
|
* above so that keeps its direct {ptr,0,n} shape. */
|
|
if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
/* struct literal initialiser: field-by-field store via the
|
|
* shared cg_structlit_fill_bp helper. The literal carries
|
|
* op == TK_ELLIPSIS when the source ends in `..., ...` —
|
|
* helper zero-fills the slot first so unmentioned fields
|
|
* read as 0. Nested struct-typed structlit field values
|
|
* recurse into the helper at the correct offset instead of
|
|
* landing AX = first-qword via cgexpr (#17 silent zero). */
|
|
if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu
|
|
&& lu->kind == TY_STRUCT) {
|
|
cg_structlit_fill_bp(c, locals, lu, n->rhs, off);
|
|
break;
|
|
}
|
|
/* sret receive (#23): plain TY_STRUCT >24B. The let's own
|
|
* slot IS the caller-prealloc dest; the call writes
|
|
* through hidden RDI directly into our slot, no AX/DX/CX
|
|
* shuffle. Set cg_sret_dest_off so the nested cgexpr →
|
|
* N_CALL path emits `LEAQ off(BP), RDI` before CALL. */
|
|
if (n->rhs && n->rhs->kind == N_CALL && lu
|
|
&& lu->kind == TY_STRUCT && sz > 24) {
|
|
cg_sret_dest_off = off;
|
|
cgexpr(c, n->rhs, *locals);
|
|
cg_sret_dest_off = 0;
|
|
break;
|
|
}
|
|
/* Whole-struct receive for sizes <=24B (call-result rhs).
|
|
* Counterpart of #4's cgreturn ABI: cgexpr leaves
|
|
* AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23], zero-
|
|
* padded to 24B by the producer.
|
|
*
|
|
* ASYMMETRY (do NOT mirror the sender): producer emits three
|
|
* uniform MOVQs into a zero-padded 24B scratch slot; the
|
|
* receiver must write only `sz` bytes — MOVQ for full 8B
|
|
* chunks plus a sized tail (MOVL/MOVW/MOVB) by the *declared*
|
|
* struct size. Otherwise a trailing 1..7-byte chunk would
|
|
* overrun into the next local slot.
|
|
*
|
|
* Tail chunks in {3,5,6,7} (would need shift-and-store from
|
|
* the register) are unreachable under WW struct alignment
|
|
* rules (field aligns force size%align==0); the guard
|
|
* excludes them so they fall through to the existing scalar
|
|
* path rather than emit a stomping MOVQ tail. Sizes >24B also
|
|
* fall through (sret deferred, same constraint as #4). */
|
|
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)) {
|
|
/* str IS []u8: cgexpr leaves
|
|
* (AX=ptr, BX=len, CX=cap). Same
|
|
* shuffle as the slice arm above —
|
|
* DX=ptr, CX=len, R8=cap
|
|
* (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
areg(D_R8));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
/* scalar fills DX only. Zero
|
|
* CX / R8 if dst slot covers
|
|
* slot+16 / slot+24. */
|
|
if (rsz > 16)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_CX));
|
|
if (rsz > 24)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_R8));
|
|
}
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
areg(D_AX));
|
|
} else {
|
|
/* Struct variant or tagged-subset:
|
|
* materialise the widened value in a
|
|
* scratch slot, then load AX/DX/CX/R8
|
|
* from the slot. Struct literal: field
|
|
* stores; struct ident: word copy;
|
|
* tagged subset: copy + tag remap.
|
|
* 4th word in R8 covers slice payload
|
|
* variants (slot >= 32B).
|
|
*
|
|
* Single-slot @retscr (#14): returns are
|
|
* terminal, so all retscr uses in this fn
|
|
* share one slot. Pre-fix per-site fresh
|
|
* allocation over-grew the frame by sz
|
|
* bytes per extra return. */
|
|
int sz = (int)rt->size;
|
|
int scr;
|
|
if (cg_retscr != 0) {
|
|
scr = cg_retscr;
|
|
} else {
|
|
/* Fixed "@retscr" SSoT name —
|
|
* mirrors wwstage's localadd
|
|
* @-prefix dedup. Pre-fix
|
|
* mklabel(c, "retscr") consumed
|
|
* one labelseq counter slot per
|
|
* function with a tagged return,
|
|
* pushing every subsequent ct/ce/
|
|
* else/end label 1 ahead of
|
|
* wwstage. Site 1 sentinel
|
|
* masked by latent struct-widen
|
|
* offset divergence (#20/#21);
|
|
* fix is preventive symmetry per
|
|
* rule 10. */
|
|
scr = local_alloc(c, locals,
|
|
"@retscr", sz, cg_frame);
|
|
cg_retscr = scr;
|
|
}
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
cg_widen_tagged_store(c, locals, rt,
|
|
n->lhs, D_BP, scr, sz);
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
|
|
areg(D_AX));
|
|
if (sz > 8)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + 8),
|
|
areg(D_DX));
|
|
if (sz > 16)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + 16),
|
|
areg(D_CX));
|
|
if (sz > 24)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + 24),
|
|
areg(D_R8));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
/* sret return (#23): plain TY_STRUCT >24B. Callee writes
|
|
* the value through `*(@sretarg)` (the caller-prealloc
|
|
* dest passed in RDI at entry; saved to @sretarg in the
|
|
* prologue), then loads @sretarg into RAX and rets — the
|
|
* SysV sret discipline of "return the pointer". No
|
|
* AX/DX/CX shuffle, no scratch slot beyond @sretarg. */
|
|
if (n->lhs && cg_ret_type && cg_sret_arg_off != 0) {
|
|
/* type_chase_named (#22). A single peel left `rt` still
|
|
* TY_NAMED when the declared return type is `type b
|
|
* = a;` where a is itself a NAMED alias of a struct,
|
|
* so the TY_STRUCT gate below missed and the sret
|
|
* return arm fell through to the scalar-AX default —
|
|
* corrupting the caller's receive slot even though
|
|
* the prologue wired @sretarg. */
|
|
Type *rt = type_chase_named(cg_ret_type);
|
|
/* sret return-forwarding (task #9 follow-up to #23):
|
|
* `return f();` where outer + inner both return the
|
|
* same >24B struct shape. Outer's @sretarg already
|
|
* holds its caller's prealloc dest; pass it to inner
|
|
* in RDI (set by cgcall via cg_sret_forward), inner
|
|
* writes directly there, inner's RAX (dest pointer)
|
|
* is already outer's return value. The trailing
|
|
* MOVQ @sretarg(BP), AX is redundant after inner's
|
|
* RET but kept for byte-id symmetry with the
|
|
* N_IDENT / N_STRUCTLIT arms below. */
|
|
if (rt && rt->kind == TY_STRUCT
|
|
&& (int)rt->size > 24
|
|
&& n->lhs->kind == N_CALL) {
|
|
cg_sret_forward = 1;
|
|
cgexpr(c, n->lhs, *locals);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
if (rt && rt->kind == TY_STRUCT
|
|
&& (int)rt->size > 24
|
|
&& (n->lhs->kind == N_IDENT
|
|
|| n->lhs->kind == N_STRUCTLIT)) {
|
|
/* Natural size = max(foff + fsz) over declared
|
|
* fields; mirrors selfhost cgenutil.ww
|
|
* structnaturalsize / sretretsize. Pre-fix this
|
|
* used the slot-padded rt->size, so a trailing
|
|
* narrow field (e.g. bool@32 in a 33B struct
|
|
* padded to 40B) widened to an 8B MOVQ at the
|
|
* loop tail — diverged from wwstage's MOVB
|
|
* tail. Task #33, Class A. */
|
|
int sz = 0;
|
|
for (Tfield *fl = rt->fields; fl; fl = fl->next) {
|
|
int end = (int)fl->offset + (int)(fl->type
|
|
? fl->type->size : 8);
|
|
if (end > sz) sz = end;
|
|
}
|
|
if (n->lhs->kind == N_STRUCTLIT) {
|
|
/* Delegate to the shared *-relative
|
|
* fill helper. Same store sequence the
|
|
* ≤24B path emits, but the base reg is
|
|
* reloaded from @sretarg(BP) before each
|
|
* field store. Mirrors DST_PTR_LOCAL
|
|
* usage at N_ASSIGN N_DOT via_ptr. */
|
|
cg_structlit_fill(c, locals, rt,
|
|
n->lhs, DST_PTR_LOCAL,
|
|
cg_sret_arg_off, NULL, 0);
|
|
} else {
|
|
/* N_IDENT: word-copy from rhs slot to
|
|
* *(@sretarg). Whole 8B words via MOVQ;
|
|
* trailing partial words via MOVL/MOVB
|
|
* so the read stays inside the source
|
|
* slot's declared size. */
|
|
int rhsoff = localfind(*locals,
|
|
n->lhs->str);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_BX));
|
|
int k = 0;
|
|
while (k + 8 <= sz) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 8;
|
|
}
|
|
while (k + 4 <= sz) {
|
|
ins2(c, A_MOVL,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 4;
|
|
}
|
|
while (k < sz) {
|
|
ins2(c, A_MOVB,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 1;
|
|
}
|
|
}
|
|
/* sret return: RAX = dest pointer. */
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
/* Whole-struct return for sizes ≤24B. ABI: AX=bytes[0..7],
|
|
* DX=bytes[8..15], CX=bytes[16..23]. Sizes >24B route
|
|
* through the sret arm above. Materialise rhs into a
|
|
* zero-padded 24B scratch slot, then emit AX/DX/CX loads
|
|
* unconditionally so the instruction shape is constant
|
|
* regardless of declared struct size. The receive side
|
|
* masks via the dst slot's declared size. Two rhs shapes
|
|
* are wired: N_IDENT (word-copy from rhs local slot) and
|
|
* N_STRUCTLIT (field-by-field store at scratch+foff). Call-
|
|
* result chain return is deferred to #5's receive side. */
|
|
if (n->lhs && cg_ret_type) {
|
|
/* type_chase_named (#22); see the >24B sret arm above
|
|
* for the same rationale. The ≤24B register-return
|
|
* ABI uses the same TY_STRUCT gate. */
|
|
Type *rt = type_chase_named(cg_ret_type);
|
|
if (rt && rt->kind == TY_STRUCT && rt->size <= 24
|
|
&& (n->lhs->kind == N_IDENT
|
|
|| n->lhs->kind == N_STRUCTLIT)) {
|
|
int sz = (int)rt->size;
|
|
/* Single-slot @retscr (#14): see tagged arm
|
|
* above for rationale. Fixed "@retscr" name
|
|
* avoids bumping labelseq; mirrors wwstage's
|
|
* localadd @-prefix dedup. */
|
|
int scr;
|
|
if (cg_retscr != 0) {
|
|
scr = cg_retscr;
|
|
} else {
|
|
scr = local_alloc(c, locals, "@retscr",
|
|
24, cg_frame);
|
|
cg_retscr = scr;
|
|
}
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + 0));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + 8));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + 16));
|
|
if (n->lhs->kind == N_STRUCTLIT) {
|
|
/* Delegate to the shared BP-relative
|
|
* fill helper. Same store sequence the
|
|
* inline pre-#17 walk emitted, plus
|
|
* nested struct-typed structlit values
|
|
* recurse instead of dropping the
|
|
* trailing bytes. */
|
|
cg_structlit_fill_bp(c, locals, rt,
|
|
n->lhs, scr);
|
|
} else {
|
|
/* N_IDENT: word-copy rhs slot into
|
|
* scratch. Whole 8B words via MOVQ;
|
|
* trailing partial word via MOVL/MOVB
|
|
* so we read no further than the
|
|
* source slot's declared size. */
|
|
int rhsoff = localfind(*locals,
|
|
n->lhs->str);
|
|
int k = 0;
|
|
while (k + 8 <= sz) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 8;
|
|
}
|
|
while (k + 4 <= sz) {
|
|
ins2(c, A_MOVL,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 4;
|
|
}
|
|
while (k < sz) {
|
|
ins2(c, A_MOVB,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 1;
|
|
}
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 8),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
if (n->lhs && node_isstr(n->lhs)) {
|
|
/* str IS []u8: AX=ptr, BX=len, CX=cap from cgexpr —
|
|
* no AX:DX shuffle, same as a slice (#1/Phase 3). */
|
|
cgexpr(c, n->lhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
if (n->lhs && n->lhs->kind == N_TUPLE) {
|
|
/* #83: positional per-element register-return. Walk the
|
|
* tuple's elements (harec create_unpack_bindings,
|
|
* ref/harec/src/check.c:1354-1416); each rides consecutive
|
|
* eightbytes over tuple_rseq[]. A slice/str rides its 3-word
|
|
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8), cgexpr
|
|
* leaving it in (AX,BX,CX); a scalar rides 1 word in AX.
|
|
* Spill each element's word(s) L→R, then pop into the
|
|
* cursor's registers in reverse so positional slot i lands in
|
|
* tuple_rseq[i] — (scalar,str) keeps the historical AX +
|
|
* DX,CX,R8 layout, and the SAME cursor drives the receive
|
|
* sites. Over-capacity is a loud stop (return-ABI #10), never
|
|
* a silent drop. */
|
|
int cap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]);
|
|
int total = 0;
|
|
for (Node *e = n->lhs->list; e; e = e->next)
|
|
total += tuple_ebytes(node_isstr(e) || node_isslice(e));
|
|
if (total > cap)
|
|
fatal("tuple return exceeds register-return ABI "
|
|
"capacity (%d eightbytes); see return-ABI #10",
|
|
cap);
|
|
for (Node *e = n->lhs->list; e; e = e->next) {
|
|
int wide = node_isstr(e) || node_isslice(e);
|
|
cgexpr(c, e, *locals); /* scalar=AX; slice/str=AX,BX,CX */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* scalar / .ptr */
|
|
if (wide) {
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* .len */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* .cap */
|
|
}
|
|
}
|
|
for (int i = total - 1; i >= 0; i--)
|
|
ins1(c, A_POPQ, areg(tuple_rseq[i]));
|
|
} 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: {
|
|
/* #83: positional per-element destructure store. The rhs left
|
|
* each tuple element in the register-return cursor (see N_RETURN
|
|
* / harec create_unpack_bindings, ref/harec/src/check.c:1354-1416);
|
|
* walk the bindings over the SAME cursor and store each at its
|
|
* own width — a slice/str's 3-word {ptr,len,cap} header
|
|
* (ref/hare/rt/ensure.ha:4-8) into a header-sized slot (sized
|
|
* from u->size so #1 propagates), a scalar's 1 word into an 8B
|
|
* slot. Over-capacity is a loud stop, not a silent drop. */
|
|
cgexpr(c, n->rhs, *locals);
|
|
int cap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]);
|
|
int total = 0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *t = l->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR);
|
|
total += tuple_ebytes(wide);
|
|
}
|
|
if (total > cap)
|
|
fatal("tuple destructure exceeds register-return ABI "
|
|
"capacity (%d eightbytes); see return-ABI #10", cap);
|
|
int cur = 0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *t = l->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR);
|
|
int sz = wide ? (int)u->size : 8;
|
|
int off = localoff(c, locals, l->str, sz, frame);
|
|
if (wide) {
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[cur + 0]),
|
|
amem(D_BP, off + 0)); /* .ptr */
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[cur + 1]),
|
|
amem(D_BP, off + 8)); /* .len */
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[cur + 2]),
|
|
amem(D_BP, off + 16)); /* .cap */
|
|
} else {
|
|
/* #105: an f64/f32 element rides X0, not its
|
|
* integer cursor reg — MOVSD/MOVSS it, else
|
|
* the slot gets garbage and the FACE-Z field
|
|
* read sees it. X0 survives the reg->mem
|
|
* stores. Single-float scope; #107 is multi. */
|
|
int e_f32 = 0;
|
|
if (fld_isfloat(t, &e_f32))
|
|
ins2(c, e_f32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_BP, off));
|
|
else
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[cur]),
|
|
amem(D_BP, off));
|
|
}
|
|
cur += tuple_ebytes(wide);
|
|
}
|
|
break;
|
|
}
|
|
case N_MASSIGN: {
|
|
/* #83: positional per-element destructure REASSIGN. Same cursor
|
|
* as N_MLET (and N_RETURN; harec create_unpack_bindings,
|
|
* ref/harec/src/check.c:1354-1416), but the slots already exist
|
|
* (reassignment) so localfind them. Element WIDTH comes from the
|
|
* rhs tuple's element types (n->rhs->type->params) — the SAME
|
|
* producer source the SEND site walks and wwstage reads via the
|
|
* callee return type — NOT the binding type: a `_` lvalue is an
|
|
* N_IDENT with empty str the checker never type-stamps (it skips
|
|
* cexpr on `_`, cmd/wcc/check.c N_MASSIGN), so a binding-typed
|
|
* width would mis-size a wide `_` and DESYNC the cursor for the
|
|
* next element. harec `_` skips the store but CONSUMES its tuple
|
|
* offset; the cursor advance below honours that. A wide element's
|
|
* 3-word {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8) is stored
|
|
* at its slot. This bare-comma `a, s = f()` multi-assign is a
|
|
* retained ww-EXTENSION beyond Hare (Hare tuple-unpack is binding-
|
|
* only); ww keeps the Go/rob-pike multi-assign idiom — rule-9
|
|
* carve-out. Over-capacity is a loud stop, not a silent drop. */
|
|
cgexpr(c, n->rhs, *locals);
|
|
Type *rt = n->rhs ? n->rhs->type : NULL;
|
|
Type *ru = (rt && rt->kind == TY_NAMED) ? rt->under : rt;
|
|
Tparam *tp0 = (ru && ru->kind == TY_TUPLE) ? ru->params : NULL;
|
|
int cap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]);
|
|
int total = 0;
|
|
for (Tparam *tp = tp0; tp; tp = tp->next) {
|
|
Type *u = (tp->type && tp->type->kind == TY_NAMED)
|
|
? tp->type->under : tp->type;
|
|
total += tuple_ebytes(u && (u->kind == TY_SLICE
|
|
|| u->kind == TY_STR));
|
|
}
|
|
if (total > cap)
|
|
fatal("tuple destructure exceeds register-return ABI "
|
|
"capacity (%d eightbytes); see return-ABI #10", cap);
|
|
int cur = 0;
|
|
Tparam *tp = tp0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *et = tp ? tp->type : NULL;
|
|
Type *u = (et && et->kind == TY_NAMED) ? et->under : et;
|
|
int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR);
|
|
int off = (l->kind == N_IDENT)
|
|
? localfind(*locals, l->str) : 0;
|
|
if (off != 0) {
|
|
if (wide) {
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[cur + 0]),
|
|
amem(D_BP, off + 0)); /* .ptr */
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[cur + 1]),
|
|
amem(D_BP, off + 8)); /* .len */
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[cur + 2]),
|
|
amem(D_BP, off + 16)); /* .cap */
|
|
} else {
|
|
/* #105: f64/f32 element rides X0 (SSE),
|
|
* not its integer cursor reg — see N_MLET. */
|
|
int e_f32 = 0;
|
|
if (fld_isfloat(et, &e_f32))
|
|
ins2(c, e_f32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_BP, off));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
areg(tuple_rseq[cur]),
|
|
amem(D_BP, off));
|
|
}
|
|
}
|
|
cur += tuple_ebytes(wide);
|
|
if (tp) tp = tp->next;
|
|
}
|
|
break;
|
|
}
|
|
case N_DEFER:
|
|
if (ndefers < DEFER_MAX) {
|
|
defers[ndefers++] = n->lhs;
|
|
}
|
|
break;
|
|
case N_YIELD:
|
|
/* Evaluate the value into AX, then jump to the enclosing
|
|
* match's end label. str-typed yields land in (AX, BX);
|
|
* the consumer's let-init or call-arg site reads both. */
|
|
if (n->lhs) cgexpr(c, n->lhs, *locals);
|
|
if (nyields > 0)
|
|
ins1(c, A_JMP, abranch(yield_target[nyields - 1]));
|
|
break;
|
|
case N_BREAK:
|
|
if (nloops > 0)
|
|
ins1(c, A_JMP, abranch(loop_brk[nloops - 1]));
|
|
break;
|
|
case N_CONTINUE:
|
|
if (nloops > 0)
|
|
ins1(c, A_JMP, abranch(loop_cont[nloops - 1]));
|
|
break;
|
|
case N_SWITCH: {
|
|
/* Lower to a chain of compares. Scrutinee lands in a fresh
|
|
* local slot so case bodies can spill through SP without
|
|
* losing it. Cases are tried top-to-bottom; a `case:` arm
|
|
* with no exprs is the default and runs after all named
|
|
* arms fail. */
|
|
char *swname = aprintf(c->a, ".sw_%d", c->labelseq++);
|
|
int sloff = localoff(c, locals, swname, 8, frame);
|
|
cgexpr(c, n->lhs, *locals); /* AX = scrutinee */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sloff));
|
|
char *end = mklabel(c, "swend");
|
|
Node *defcase = NULL;
|
|
for (Node *cs = n->list; cs; cs = cs->next) {
|
|
if (cs->list == NULL) {
|
|
defcase = cs; /* save for last */
|
|
continue;
|
|
}
|
|
char *body = mklabel(c, "swcase");
|
|
char *next = mklabel(c, "swnext");
|
|
for (Node *e = cs->list; e; e = e->next) {
|
|
cgexpr(c, e, *locals); /* AX = case-expr */
|
|
ins2(c, A_MOVQ, amem(D_BP, sloff), areg(D_BX));
|
|
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
}
|
|
ins1(c, A_JMP, abranch(next));
|
|
label(c, body);
|
|
cgstmt(c, cs->body, locals, frame);
|
|
ins1(c, A_JMP, abranch(end));
|
|
label(c, next);
|
|
}
|
|
if (defcase)
|
|
cgstmt(c, defcase->body, locals, frame);
|
|
label(c, end);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
cgfn(Cg *c, FILE *out, Node *fn)
|
|
{
|
|
if (fn->body == NULL) return; /* extern decl, no body */
|
|
|
|
/* fresh per-fn state */
|
|
c->head = c->tail = NULL;
|
|
c->fnname = fn->str;
|
|
c->cur_mod = (fn->module && fn->module[0]) ? fn->module : NULL;
|
|
c->labelseq = 0;
|
|
cg_stack_arg_cursor = 0;
|
|
ndefers = 0;
|
|
nloops = 0;
|
|
cg_ret_type = fn->type ? fn->type->ret : NULL;
|
|
cg_retscr = 0;
|
|
cg_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;
|
|
/* str IS []u8: 3 eightbytes (ptr,len,cap), same as a slice
|
|
* — the caller pushes the triple (#1/Phase 3). */
|
|
int eightbytes = (slice || is_str) ? 3 :
|
|
(is_struct ? struct_eb :
|
|
(is_tagged ? tagged_eb : 1));
|
|
int regs_left = isf ? (8 - fargi) : (6 - argi);
|
|
if (regs_left >= eightbytes) {
|
|
/* #60: route slice/str slot widths through Type.size SSoT
|
|
* so #1's ty_str.size bump propagates without retouching
|
|
* this site (or its stack-stitch mirror below). */
|
|
int sz = (slice || is_str) ? (int)pu->size :
|
|
(is_struct ? (int)pu->size :
|
|
(is_tagged ? tagged_sz : 8));
|
|
int off = localoff(c, &locals, p->str, sz, &frame);
|
|
if (slice || is_str || is_struct || is_tagged) {
|
|
for (int k = 0; k < eightbytes; k++, argi++)
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off + k * 8));
|
|
} else if (isf) {
|
|
int mov = type_isf32(pt) ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
areg(sysv_fargregs[fargi]),
|
|
amem(D_BP, off));
|
|
fargi++;
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off));
|
|
argi++;
|
|
}
|
|
} else if (eightbytes > 1 && regs_left > 0 &&
|
|
(slice || is_str || is_struct || is_tagged)) {
|
|
/* Multi-word arg that partially fits in regs: caller
|
|
* filled (regs_left) registers greedily, the rest spilled
|
|
* to stack at positive BP offsets. Stitch a single local
|
|
* slot from both sources so the body sees a contiguous
|
|
* value. Mirrors the SysV greedy reg fill the caller
|
|
* does. */
|
|
/* #60: same SSoT routing as the regs-fit arm above. */
|
|
int sz = (slice || is_str) ? (int)pu->size :
|
|
(is_struct ? (int)pu->size :
|
|
(is_tagged ? tagged_sz : 8));
|
|
int off = localoff(c, &locals, p->str, sz, &frame);
|
|
extern int cg_stack_arg_cursor;
|
|
int k = 0;
|
|
for (; k < regs_left; k++, argi++)
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off + k * 8));
|
|
for (; k < eightbytes; k++) {
|
|
int stack_off = 16 +
|
|
cg_stack_arg_cursor * 8;
|
|
cg_stack_arg_cursor++;
|
|
ins2(c, A_MOVQ, amem(D_BP, stack_off),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + k * 8));
|
|
}
|
|
} else {
|
|
/* stack-spilled. Access in place via positive BP offset. */
|
|
static int stack_arg_off;
|
|
(void)stack_arg_off;
|
|
Local *l = amalloc(c->a, sizeof *l);
|
|
l->name = p->str;
|
|
/* spilled args layout: each takes 8B (ptr/len/etc); we
|
|
* only support the simple case of plain int/float here. */
|
|
extern int cg_stack_arg_cursor;
|
|
l->off = 16 + cg_stack_arg_cursor * 8;
|
|
cg_stack_arg_cursor += eightbytes;
|
|
l->next = locals;
|
|
locals = l;
|
|
}
|
|
if (tp) tp = tp->next;
|
|
}
|
|
|
|
/* Iterate the fn body's statements directly rather than dispatching
|
|
* the outermost N_BLOCK through cgstmt — N_BLOCK now save/restores
|
|
* the locals head to scope inner shadows, but the function body is
|
|
* not "an inner block": defers (queued during the body) and the
|
|
* implicit-return epilogue both call cgexpr after this loop and
|
|
* resolve identifiers via localfind, so the body's locals must
|
|
* still be in *locals when we get there. */
|
|
if (fn->body && fn->body->kind == N_BLOCK) {
|
|
for (Node *s = fn->body->list; s; s = s->next)
|
|
cgstmt(c, s, &locals, &frame);
|
|
} else {
|
|
cgstmt(c, fn->body, &locals, &frame);
|
|
}
|
|
|
|
/* implicit return for void functions */
|
|
if (c->tail->as != A_RET) {
|
|
for (int di = ndefers - 1; di >= 0; di--)
|
|
cgexpr(c, defers[di], locals);
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
}
|
|
|
|
/* round frame to 16; patch SUBQ */
|
|
if (frame & 15) frame = (frame + 15) & ~15;
|
|
subsp->from.offset = frame;
|
|
text->from.offset = frame;
|
|
|
|
txt_emit(out, c->head);
|
|
}
|
|
|
|
/* Escape one byte for an asm string literal — the same rules
|
|
* emit_data and emit_defs already use. */
|
|
static void
|
|
emit_data_byte(FILE *out, u8 b)
|
|
{
|
|
if (b == '"' || b == '\\')
|
|
fprintf(out, "\\%c", b);
|
|
else if (b < 0x20 || b >= 0x7f)
|
|
fprintf(out, "\\x%02x", b);
|
|
else
|
|
fputc(b, out);
|
|
}
|
|
|
|
/* Emit `DIR NAME(SB),"<8 LE bytes of v>"`. Used for scalar `def`
|
|
* constants (DATA) and scalar `let` globals (DATAW). */
|
|
static void
|
|
emit_data_row(FILE *out, const char *dir, const char *name, u64 v)
|
|
{
|
|
fprintf(out, "%s %s(SB),\"", dir, name);
|
|
for (int i = 0; i < 8; i++)
|
|
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
|
|
fputs("\"\n", out);
|
|
}
|
|
|
|
/* Emit `DIR NAME(SB),"<sz zero bytes>"`. Used for top-level str/
|
|
* slice/struct lets without a baked-in initialiser — the slot is
|
|
* pre-zeroed and the program writes the real value at runtime. */
|
|
static void
|
|
emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz)
|
|
{
|
|
fprintf(out, "%s %s(SB),\"", dir, name);
|
|
for (int i = 0; i < sz; i++)
|
|
emit_data_byte(out, 0);
|
|
fputs("\"\n", out);
|
|
}
|
|
|
|
/* Emit DATAW directives for top-level mutable `let` decls.
|
|
*
|
|
* Scalar lets (8B): emit the literal value, or 0 if no init.
|
|
* Non-literal init: skip — undefined symbol surfaces at link time.
|
|
*
|
|
* str lets (16B): three init shapes are wired:
|
|
* - no rhs / `nil` / `""` → 16 zero bytes
|
|
* - `"literal"` (non-empty) → 8 zero placeholder + 8 LE len,
|
|
* plus DATAR patching the ptr
|
|
* half with the interned strlit's
|
|
* runtime VA at link time.
|
|
*
|
|
* Slice lets (24B): no-init only — the slot is zero. There's no
|
|
* literal slice syntax to honour, so this is the natural shape.
|
|
*
|
|
* Struct lets (size from Type.size): no-init only. */
|
|
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. */
|
|
/* #43: gate via ty_str->size so #1 propagates. */
|
|
if (sz == (int)ty_str->size && r != NULL && r->kind == N_STRLIT
|
|
&& r->strlen > 0) {
|
|
const char *lab = intern_strlit(c, r->str, r->strlen);
|
|
const char *sym = mod_mangle(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) != (int)ty_str->size) continue;
|
|
Node *r = d->rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r == NULL || r->kind != N_STRLIT) continue;
|
|
if (r->strlen == 0) continue;
|
|
(void)intern_strlit(c, r->str, r->strlen);
|
|
}
|
|
}
|
|
|
|
void
|
|
cg_file(Cg *c, FILE *out, Node *file)
|
|
{
|
|
if (file == NULL || file->kind != N_FILE) return;
|
|
ffi_collect(c, file);
|
|
mod_collect(c, file);
|
|
sdef_collect(c, file);
|
|
let_collect(c, file);
|
|
strlits = NULL;
|
|
strlit_seq = 0;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_FNDECL) continue;
|
|
cgfn(c, out, d);
|
|
}
|
|
let_pre_intern(c, file);
|
|
emit_data(c, out);
|
|
emit_defs(c, out, file);
|
|
emit_lets(c, out, file);
|
|
}
|
|
|
|
void peephole(Cg *c) { (void)c; }
|
|
void regalloc_init(Cg *c) { (void)c; }
|