The free(x) builtin lowered to CALL ffi_resolve("free") in cstage and
fell through to a generic CALL free in wwstage (which had no free arm
at all) -- an undefined reference at w6l unless an @symbol decl
happened to be in scope. ww has no free by design (rt/alloc.s:30 --
the bump allocator cannot reclaim a mid-chunk pointer; process exit
does), so both stages now evaluate the operand for side effects
(Hare's free(expr) evaluates expr) and emit nothing else, letting
Hare code that calls free() port verbatim (regex fold-2b calls it at
4+ sites). The 2-arg os.free(p, n) public API is untouched: the
builtin gate requires exactly one bare-ident-callee arg.
930_free_noop_run pins per row: w6c/w6c_ww byte-id, no free symbol
in the .s, deref-after-free validity, and the operand side effect
running once per free() via a global counter.
12472 lines
437 KiB
C
12472 lines
437 KiB
C
/*
|
|
* cgen.c — typed AST → Prog list, expressed as Plan 9-flavoured
|
|
* amd64 assembly text. This is the simplest thing that works:
|
|
*
|
|
* - Every function gets a stack frame sized for spilled locals + a
|
|
* 16-byte alignment pad.
|
|
* - Expressions are evaluated stack-machine style: result in AX,
|
|
* intermediate stuff pushed on the hardware stack via PUSHQ AX.
|
|
* - The first six integer args go in DI, SI, DX, CX, R8, R9
|
|
* (SysV amd64 ABI). We don't yet handle struct-by-value or
|
|
* floats; floats and slices are deferred.
|
|
*
|
|
* Calling our own functions: emit CALL <name>(SB), let w6a/w6l resolve.
|
|
* Calling C externs: same — extern symbols are just unresolved CALLs.
|
|
*/
|
|
#include "gc.h"
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
|
|
static const int sysv_argregs[] = { D_DI, D_SI, D_DX, D_CX, D_R8, D_R9 };
|
|
static const int sysv_fargregs[] = { D_X0, D_X1, D_X2, D_X3, D_X4, D_X5, D_X6, D_X7 };
|
|
|
|
/* per-fn cursor, reset before each cgfn: counts how many 8-byte
|
|
* stack-arg slots above BP have been claimed. */
|
|
int cg_stack_arg_cursor;
|
|
|
|
/* return type of the current function, set by cgfn before walking
|
|
* the body. Drives tagged-union return construction and the `?` /
|
|
* `!` propagation paths. */
|
|
static Type *cg_ret_type;
|
|
/* Pointer to the current function's frame size accumulator. cgexpr
|
|
* needs this to allocate scratch slots (e.g. match bindings) without
|
|
* threading it through every signature. */
|
|
static int *cg_frame;
|
|
/* Per-fn @retscr offset (single-slot SSoT, task #14). Returns are
|
|
* terminal: at most one return path fires per call, so all retscr
|
|
* uses share one slot. Mirrors wwstage's `@retscr` convention
|
|
* (cgen.ww localadd '@'-prefix dedup; #38 ratified single-slot
|
|
* semantics for synthetic scratches). 0 means "not yet allocated";
|
|
* negative offsets returned by local_alloc are the live value. */
|
|
static int cg_retscr;
|
|
/* Per-fn @tupfscr offset (single-slot SSoT). A multi-float tuple return
|
|
* (#164/#107) spills each float out of X0 to this scratch as the L→R
|
|
* element walk clobbers X0, then reloads X0/X1 by SSE index after the
|
|
* integer POPQ dance. Sized to the SSE register cap (X0,X1). Mirrors the
|
|
* @retscr single-slot convention + wwstage's `@tupfscr` '@'-prefix dedup;
|
|
* 0 means "not yet allocated". */
|
|
static int cg_tupfscr;
|
|
/* Per-fn @tupargscr staging slot (#163, single-slot SSoT). A tuple
|
|
* PASSED AS AN ARGUMENT (the param twin of #164's tuple return) is left
|
|
* by its producing call in the return-ABI cursor (AX/DX/CX/R8 + X0/X1);
|
|
* the SEND restages it into this slot positionally (tuple_store), then
|
|
* pushes the slot words onto the stack so the pop drains them into the
|
|
* SysV ARG cursor (DI/SI/.. + X0..X7). A frame slot decouples the
|
|
* return-class regs (which overlap the arg-class regs) from the arg
|
|
* placement. Reused per tuple arg (drained to the stack before the next
|
|
* arg); 0 means "not yet allocated", cg_tupargscr_sz the cached width. */
|
|
static int cg_tupargscr;
|
|
static int cg_tupargscr_sz;
|
|
/* #271: per-fn @aggargscr scratch for a >24B (sret-class) aggregate
|
|
* arg sourced from a CALL — the result is sret'd here, then pushed
|
|
* word-by-word into the arg convention. 0 = not yet allocated. */
|
|
static int cg_aggargscr;
|
|
static int cg_aggargscr_sz;
|
|
/* Per-fn @-prefix scratch SSoT (task #26, follow-up to #15-cstage's
|
|
* @retscr). Pre-#26 each site allocated a labelseq-stamped fresh slot
|
|
* per call (mklabel "tagbase" / "tagscr" / "argscr" / "idxscr"); the
|
|
* labelseq bumps drifted cstage's ct/ce/end labels ahead of wwstage,
|
|
* and the per-call frame growth drifted cstage's framesize ahead too.
|
|
*
|
|
* Two cached slots match wwstage's `@`-prefix namespace exactly:
|
|
* cg_tagbase — 8B base-register spill for cg_widen_tagged_store
|
|
* via_outer (mirrors wwstage @tagbase, 1 site).
|
|
* @tagscr<sz> — sized scratch shared across THREE sites: cg_widen_
|
|
* tagged_store via_outer write target, cg_widen_tagged_
|
|
* push struct/tagged-source widen, N_INDEX tagged-element
|
|
* assign. Mirrors wwstage @tagscr<sz> — wwstage shares
|
|
* the slot via localadd `@`-prefix dedup against
|
|
* c.atlocals.
|
|
*
|
|
* Both stages size at first use (per name). Pre-#44 the tagged scratch
|
|
* was a SINGLE slot and a later site asking for a larger size fatal'd
|
|
* (rule 7 — pinned offset can't grow in place once neighbours are
|
|
* allocated); a fn mixing two tagged slot sizes smaller-first (regex
|
|
* compile(): 56B append-element widen then 64B sret return) was
|
|
* uncompilable. #44 keys the scratch by slot size — one cached slot
|
|
* per distinct size, allocated in first-use order in BOTH stages, so
|
|
* the grow-fatal is unreachable for @tagscr by construction. All
|
|
* three sites funnel through cg_tagscr_slot (no other alloc path).
|
|
* Per-fn convergence completed by #15 (#26c follow-up): wwstage
|
|
* dropped its scanlocals pre-pass and aligned DOWN to cstage's
|
|
* first-use shape. */
|
|
static int cg_tagbase;
|
|
static int cg_tagbase_sz;
|
|
enum { CG_NTAGSCR = 16 };
|
|
static int cg_tagscr_off[CG_NTAGSCR];
|
|
static int cg_tagscr_sz[CG_NTAGSCR];
|
|
static int cg_ntagscr;
|
|
/* #34: per-fn @appendscr — 8B dst-pointer spill for the append()
|
|
* struct-literal element fill (cg_structlit_fill DST_PTR_LOCAL needs
|
|
* a BP-rooted slot to reload BX from across its internal cgexprs).
|
|
* Cached per name per fn to mirror wwstage's localadd `@`-prefix
|
|
* dedup, else two struct appends in one fn diverge the frame. */
|
|
static int cg_appendscr;
|
|
/* System V AMD64 sret discipline (task #23). Plain TY_STRUCT returns
|
|
* with size > 24B are passed via a hidden first-arg pointer (RDI) to
|
|
* a caller-prealloc dest; the callee writes through that pointer and
|
|
* returns it in RAX. Tagged returns (slot ≤ 32B in AX/DX/CX/R8) and
|
|
* tuples (16/24B in AX/DX/CX) keep their existing register-return ABI.
|
|
*
|
|
* cg_sret_arg_off — callee-side @sretarg slot (8B, holds saved RDI).
|
|
* Set in cgfn prologue when ret > 24B plain struct.
|
|
* cg_sret_dest_off — caller-side dest offset, propagated from a receive
|
|
* site (N_LET / N_ASSIGN ident) to the nested N_CALL
|
|
* so the call emits `LEAQ off(BP), RDI` instead of
|
|
* allocating a scratch. 0 means no receiver wired.
|
|
* cg_sretscr_off — per-fn @sretscr discard slot for sret CALLs whose
|
|
* result is dropped (no named receiver). Single-slot
|
|
* SSoT mirroring cg_retscr. Sized to the largest
|
|
* discarded sret return type in the fn.
|
|
* cg_sret_forward — set by cgreturn `return f();` from an sret callee
|
|
* to signal cgcall: source RDI for inner from outer's
|
|
* saved @sretarg (MOVQ) instead of LEAQ'ing a local
|
|
* dest. Inner writes into outer's caller-prealloc;
|
|
* inner's RAX (the dest pointer) is already outer's
|
|
* return value. No temporary in outer's frame. */
|
|
static int cg_sret_arg_off;
|
|
static int cg_sret_dest_off;
|
|
/* #220: caller-side dest for an sret receive into a GLOBAL lvalue. A
|
|
* BP-relative i32 offset (cg_sret_dest_off) can't name a top-level let,
|
|
* so the symbol name is carried instead and emitted as LEAQ name(SB),DI.
|
|
* Mutually exclusive with cg_sret_dest_off. */
|
|
static const char *cg_sret_dest_sym;
|
|
static int cg_sretscr_off;
|
|
static int cg_sretscr_sz;
|
|
static int cg_sret_forward;
|
|
|
|
/* Per-fn defer stack: pushed in registration order, popped (emitted)
|
|
* in reverse at each return. */
|
|
#define DEFER_MAX 32
|
|
static Node *defers[DEFER_MAX];
|
|
static int ndefers;
|
|
|
|
/* Loop stack: each `for` records the labels its `break`/`continue`
|
|
* target. The continue label is where the iterator step + cond test
|
|
* happens; the end label sits past the loop. */
|
|
#define LOOP_MAX 16
|
|
static const char *loop_cont[LOOP_MAX];
|
|
static const char *loop_brk[LOOP_MAX];
|
|
static int nloops;
|
|
|
|
/* Yield-target stack. Each entry is the end label of an enclosing
|
|
* match-as-expression; `yield expr;` evaluates expr (AX) and JMPs
|
|
* to the topmost entry. */
|
|
#define YIELD_MAX 16
|
|
static const char *yield_target[YIELD_MAX];
|
|
static int nyields;
|
|
|
|
static int
|
|
cg_isfloat(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL) return 0;
|
|
return t->kind == TY_F32 || t->kind == TY_F64
|
|
|| t->kind == TY_UNTYPED_FLOAT;
|
|
}
|
|
|
|
/* type_chase_named — walk the TY_NAMED.under chain to the deepest non-
|
|
* named type. Chain-of-aliases (#22): `type b = a; type a = struct;`
|
|
* stacks two TY_NAMED layers — a single peel leaves `t` pointing at
|
|
* the inner alias (still TY_NAMED), so kind-gated arms (TY_STRUCT,
|
|
* TY_SLICE, TY_TAGGED, TY_PTR) miss and the codegen silently falls
|
|
* through to a scalar shape. Mirror of wwstage's structlookupchain. */
|
|
static Type *
|
|
type_chase_named(Type *t)
|
|
{
|
|
while (t && t->kind == TY_NAMED) t = t->under;
|
|
return t;
|
|
}
|
|
|
|
/* cg_sret_retsize — sret classifier; defined after the tuple register-
|
|
* return helpers (tuple_rseq / tuple_ebytes / fld_isfloat) it consults
|
|
* for the over-cap-tuple arm. Forward-declared here for the earlier
|
|
* callers (cgcall, fn prologue). Task #23 / #10. */
|
|
static int cg_sret_retsize(Type *rt);
|
|
|
|
static int
|
|
node_isfloat(Node *n)
|
|
{
|
|
return n && cg_isfloat(n->type);
|
|
}
|
|
|
|
static int
|
|
type_isstr(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL) return 0;
|
|
return t->kind == TY_STR || t->kind == TY_UNTYPED_STR;
|
|
}
|
|
|
|
static int
|
|
node_isstr(Node *n)
|
|
{
|
|
return n && type_isstr(n->type);
|
|
}
|
|
|
|
static int
|
|
type_isslice(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_SLICE;
|
|
}
|
|
|
|
static int
|
|
node_isslice(Node *n)
|
|
{
|
|
return n && type_isslice(n->type);
|
|
}
|
|
|
|
/* node_tuplearg — the underlying TY_TUPLE Type of a tuple-typed argument
|
|
* VALUE, else NULL. #163: scoped to an N_CALL producer — the only form
|
|
* that leaves a tuple in the return-ABI cursor (AX/DX/CX/R8 + X0/X1, per
|
|
* #164). A tuple ident / literal as a first-class value is a separate
|
|
* unimplemented gap (`let t = (1,2)` does not materialise a slot today),
|
|
* so the SEND restricts to the call form and loud-stops the rest rather
|
|
* than push stale registers (rule 7, never a silent drop). */
|
|
static Type *
|
|
node_tuplearg(Node *n)
|
|
{
|
|
if (n == NULL || n->kind != N_CALL) return NULL;
|
|
Type *t = n->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
return (u && u->kind == TY_TUPLE) ? u : NULL;
|
|
}
|
|
|
|
/* #83: positional tuple register-return ABI. Tuple elements ride
|
|
* consecutive eightbytes over tuple_rseq[]; a slice/str rides its 3-word
|
|
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8, ty_str->size SSoT), a
|
|
* scalar rides 1. SEND (N_RETURN) and RECEIVE (N_MLET/N_MASSIGN) walk the
|
|
* SAME widths so element->register agrees — mirrors harec's
|
|
* create_unpack_bindings element walk (ref/harec/src/check.c:1354-1416). */
|
|
static const int tuple_rseq[] = { D_AX, D_DX, D_CX, D_R8 };
|
|
|
|
/* #164 (#107): SysV dual register-class return. A tuple (and, per #171,
|
|
* a struct) return places each element by SysV class — a float rides the
|
|
* SSE row [X0,X1], everything else the INTEGER row [AX,DX,CX,R8]
|
|
* (tuple_rseq) — with the two rows advancing on INDEPENDENT counters, so
|
|
* a float lands in the next XMM regardless of its positional slot
|
|
* (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},{XMM0,XMM1}}).
|
|
* ww extends the INTEGER row to 4 eightbytes; the SSE row keeps SysV's 2.
|
|
* tuple_store is the shared per-element receive lowering so the struct-
|
|
* return convergence (#171) is a call-site swap, not a redesign. */
|
|
static const int tuple_sse_seq[] = { D_X0, D_X1 };
|
|
|
|
/* #10: the register-return-ABI caps — the SINGLE SSoT shared by the sret
|
|
* classifier (cg_sret_retsize over-cap-tuple arm) AND every emit/receive
|
|
* site (N_RETURN tuple SEND, N_MLET/N_MASSIGN destructure, cgcall guard).
|
|
* Classify and emit MUST agree on these, else a tuple gets classified
|
|
* sret by one and in-reg by the other → corruption. */
|
|
#define TUPLE_GPCAP ((int)nelem(tuple_rseq))
|
|
#define TUPLE_SSECAP ((int)nelem(tuple_sse_seq))
|
|
|
|
static int
|
|
tuple_ebytes(int wide)
|
|
{
|
|
return wide ? (int)(ty_str->size / 8) : 1;
|
|
}
|
|
|
|
static int
|
|
type_isf32(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_F32;
|
|
}
|
|
|
|
static int
|
|
node_isf32(Node *n)
|
|
{
|
|
return n && type_isf32(n->type);
|
|
}
|
|
|
|
/* fld_isfloat — true iff f's underlying type is f32, f64, or
|
|
* untyped_float. The cgen passes float values in X0 (via MOVSD/MOVSS),
|
|
* integer/ptr values in AX (via MOVQ). Without this check, a field
|
|
* store/load on an f64 slot runs through AX and the bits never reach
|
|
* the SSE side — see the vfloat / L.curfval traps documented in
|
|
* examples/lisp/CLAUDE.md.
|
|
*
|
|
* TY_UNTYPED_FLOAT defaults to f64 (no TY_UNTYPED_F32 exists). Every
|
|
* field/element/pointee caller passes a declared type that is never
|
|
* UNTYPED — adding the case is a no-op for them. The variant-widen
|
|
* call site (cg_widen_tagged_store) is the only one passing an
|
|
* expression type, where `let _: (i64|f64) = -2.5;` arrives with
|
|
* src->type = ty_untyped_float (cunop returns the operand type for
|
|
* TK_MINUS, untyped_float for an untyped float literal). The earlier
|
|
* narrow predicate dropped the payload via the AX scalar fallback —
|
|
* matches cg_isfloat's acceptance set now.
|
|
*
|
|
* Sets *isf32 to 1 for f32, 0 for f64 / untyped_float. */
|
|
static int
|
|
fld_isfloat(Type *t, int *isf32)
|
|
{
|
|
if (isf32) *isf32 = 0;
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_F64) return 1;
|
|
if (t->kind == TY_UNTYPED_FLOAT) return 1;
|
|
if (t->kind == TY_F32) { if (isf32) *isf32 = 1; return 1; }
|
|
return 0;
|
|
}
|
|
|
|
/* cg_sret_retsize — sret classification by natural return size:
|
|
* - plain TY_STRUCT > 24B → its natural size (the #23 threshold).
|
|
* - TY_TUPLE whose SysV register-return footprint exceeds the caps
|
|
* (> TUPLE_GPCAP integer eightbytes or > TUPLE_SSECAP float
|
|
* eightbytes) → its natural total size, so the callee returns it
|
|
* via sret instead of registers (#10). The element footprint walk
|
|
* matches the N_RETURN tuple SEND exactly (a float = 1 SSE
|
|
* eightbyte, a slice/str its 3-word header, a scalar 1 GP word).
|
|
* - TY_TAGGED whose slot exceeds the AX/DX/CX/R8 cursor
|
|
* (> TUPLE_GPCAP eightbytes) → its natural size (#38).
|
|
* Everything else (in-cap tuples, in-cap tagged unions, str, slices,
|
|
* scalars) routes through its register-return ABI → 0. */
|
|
static int
|
|
cg_sret_retsize(Type *rt)
|
|
{
|
|
rt = type_chase_named(rt);
|
|
if (rt == NULL) return 0;
|
|
if (rt->kind == TY_STRUCT)
|
|
return (int)rt->size <= 24 ? 0 : (int)rt->size;
|
|
/* #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP
|
|
* eightbytes; a wider slot was silently truncated (payload word
|
|
* 4+ died in the callee frame). The ≤cap boundary is load-bearing:
|
|
* (str|nomem)-shaped 32B slots MUST stay register-ABI or every
|
|
* such consumer in the tree flips. Nullable folds to one word. */
|
|
if (rt->kind == TY_TAGGED) {
|
|
if (rt->nullable) return 0;
|
|
return (int)rt->size <= TUPLE_GPCAP * 8 ? 0 : (int)rt->size;
|
|
}
|
|
/* #267: arrays ride the struct-return ABI — same ≤24 reg / >24 sret
|
|
* split. Pure-int element arrays only; no float-array-return
|
|
* consumer exists, so struct_float_class stays struct-only. */
|
|
if (rt->kind == TY_ARRAY)
|
|
return (int)rt->size <= 24 ? 0 : (int)rt->size;
|
|
if (rt->kind == TY_TUPLE) {
|
|
int gptotal = 0, ssecount = 0, f32;
|
|
for (Tparam *p = rt->params; p; p = p->next) {
|
|
Type *pu = type_chase_named(p->type);
|
|
int wide = pu && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR);
|
|
if (fld_isfloat(p->type, &f32))
|
|
ssecount++;
|
|
else
|
|
gptotal += tuple_ebytes(wide);
|
|
}
|
|
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
|
|
return (int)rt->size;
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* fld_issigned — true iff a sub-word field/element load needs sign
|
|
* extension (i8 → MOVSBQ, i16 → MOVSWQ, i32 → MOVSXD). Follows NAMED
|
|
* and ENUM aliases via type_isunsigned, then peels off the unsigned
|
|
* cases (u*, bool, rune) so what remains is the genuinely-signed
|
|
* narrow integers. The literal-kind ladder this replaces missed
|
|
* TY_ENUM aliases entirely (`type myflag = i8` silently emitted
|
|
* MOVZBQ on a field load). */
|
|
static int
|
|
fld_issigned(Type *t)
|
|
{
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (u == NULL) return 0;
|
|
if (u->kind == TY_BOOL) return 0;
|
|
if (type_isunsigned(u)) return 0;
|
|
return type_isint(u);
|
|
}
|
|
|
|
static int
|
|
fldloadop(Type *t, int sz)
|
|
{
|
|
int sigd = fld_issigned(t);
|
|
if (sz == 1) return sigd ? A_MOVSBQ : A_MOVZBQ;
|
|
if (sz == 2) return sigd ? A_MOVSWQ : A_MOVZWQ;
|
|
if (sz == 4) return sigd ? A_MOVSXD : A_MOVL;
|
|
return A_MOVQ;
|
|
}
|
|
|
|
static int
|
|
fldstoreop(Type *t, int sz)
|
|
{
|
|
(void)t;
|
|
if (sz == 1) return A_MOVB;
|
|
if (sz == 2) return A_MOVW;
|
|
if (sz == 4) return A_MOVL;
|
|
return A_MOVQ;
|
|
}
|
|
|
|
/* castsrcprim — structural (size, unsigned) of an N_CAST's source
|
|
* expression, mirroring wwstage's exprprimresolved in
|
|
* selfhost/cmd/wcc/cgenutil.ww. The cgen-stage match has to be
|
|
* structural, not "use n->type": cstage's checker decorates every
|
|
* node with a precise Type, but wwstage has no checker and must
|
|
* derive the source type from the AST shape. To keep cstage and
|
|
* wwstage emitting byte-identical asm under the #33 identity-width
|
|
* identity-sign clamp-skip, both must agree on what a "knowable
|
|
* source type" is. The shape menu:
|
|
* N_INTLIT — typed literal (`7u32`) via tsuffix.
|
|
* N_IDENT, N_CAST — type set by checker; trust it. Wwstage
|
|
* reaches the same answer via localfindnode +
|
|
* typenodeprimresolved (alias / enum walk)
|
|
* and via the cast's rhs type-node.
|
|
* N_UN — recurse on operand.
|
|
* N_DOT real field — base resolves to TY_STRUCT (or ptr-to);
|
|
* use the field's checker-set type. Pseudo-
|
|
* fields .len/.cap/.ptr are excluded — they
|
|
* are i32 / *T but wwstage's exprprimresolved
|
|
* doesn't recognise them, and asymmetry there
|
|
* breaks 995_self_rebuild. Tuple positional
|
|
* access likewise excluded.
|
|
* default — sz=0, identity check fails, clamp emits.
|
|
* Matches wwstage's conservative fallback. */
|
|
static void
|
|
castsrcprim(Node *n, int *sz, int *unsignd)
|
|
{
|
|
*sz = 0;
|
|
*unsignd = 0;
|
|
if (n == NULL) return;
|
|
Type *t = NULL;
|
|
switch (n->kind) {
|
|
case N_INTLIT:
|
|
/* tsuffix-typed literal: checker resolved n->type via
|
|
* lookup_builtin. Untyped int leaves n->type at
|
|
* TY_UNTYPED_INT — we conservatively skip those (wwstage
|
|
* matches: no tsuffix → sz=0). */
|
|
if (n->tsuffix && n->type) {
|
|
Type *u = (n->type->kind == TY_NAMED)
|
|
? n->type->under : n->type;
|
|
if (u && u->kind != TY_UNTYPED_INT
|
|
&& u->kind != TY_UNTYPED_RUNE
|
|
&& type_isint(u)) {
|
|
t = u;
|
|
}
|
|
}
|
|
break;
|
|
case N_IDENT:
|
|
case N_CAST:
|
|
t = n->type;
|
|
break;
|
|
case N_UN:
|
|
castsrcprim(n->lhs, sz, unsignd);
|
|
return;
|
|
case N_DOT: {
|
|
/* Real struct field only. .len / .cap / .ptr on str /
|
|
* slice / array are pseudo-fields wwstage doesn't see. */
|
|
Type *bt = n->lhs ? n->lhs->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
if (bu && bu->kind == TY_PTR) {
|
|
Type *st = bu->sub;
|
|
bu = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
}
|
|
if (bu && bu->kind == TY_STRUCT) {
|
|
t = n->type;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (u && type_isint(u)) {
|
|
*sz = (int)u->size;
|
|
*unsignd = type_isunsigned(u);
|
|
}
|
|
}
|
|
|
|
/* localloadop — read instruction for a scalar local/let load. Same
|
|
* dispatch as fldloadop, but keyed on the value's own type. Lets the
|
|
* caller emit MOVSXD / MOVSWQ / MOVSBQ on a signed-narrow slot instead
|
|
* of a raw MOVQ, so a slot that was last written by a narrow deref-
|
|
* store (`*p: *i32 = v` lowers to MOVL, only 4B) reads back as a
|
|
* properly-sign-extended i64. The natural N_ASSIGN / N_LET paths
|
|
* already store the value as a sign-extended 8B word so a MOVQ read
|
|
* accidentally works; deref-stores are the only path that touches
|
|
* fewer bytes than MOVQ reads. Fixing the read makes the slot's
|
|
* representation honest regardless of which store path wrote it. */
|
|
static int
|
|
localloadop(Type *t)
|
|
{
|
|
int sz = (t && t->size > 0) ? (int)t->size : 8;
|
|
if (sz != 1 && sz != 2 && sz != 4) return A_MOVQ;
|
|
return fldloadop(t, sz);
|
|
}
|
|
|
|
/* struct ≤16B all-INTEGER: 1 or 2 eightbyte regs.
|
|
* Returns 0 if not a struct or too large. */
|
|
static int
|
|
struct_arg_size(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_STRUCT) return 0;
|
|
return (int)t->size;
|
|
}
|
|
|
|
/* struct_float_class — SysV per-eightbyte classification for the #165
|
|
* float-bearing-struct param case (the param twin of #171's struct
|
|
* return, classifying per-eightbyte rather than #163's per-element).
|
|
* Fills cls[e] = 1 (SSE) / 0 (INTEGER) for each of the struct's 1-2
|
|
* eightbytes and returns the eightbyte count, but ONLY for a qualifying
|
|
* struct: every eightbyte is either pure-INTEGER or a lone f64 exactly
|
|
* filling it, AND at least one is f64. Returns 0 (caller keeps the all-
|
|
* GP transport, which is correct + byte-identical for those) when the
|
|
* type is not a <=16B struct, has an all-integer layout (no float to
|
|
* route), carries an f32 field, packs >1 float into an eightbyte, has a
|
|
* float straddling the 8-byte SysV eightbyte boundary, or holds an
|
|
* aggregate field (SysV would recurse — out of scope here). f32 / sub-
|
|
* eightbyte packing is deferred (#165b). */
|
|
static int
|
|
struct_float_class(Type *t, int *cls)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_STRUCT) return 0;
|
|
int sz = (int)t->size;
|
|
if (sz <= 0 || sz > 16) return 0;
|
|
/* SysV classifies aggregates in 8-byte eightbytes (§3.2.3); 8 is
|
|
* the eightbyte stride, not a type footprint. */
|
|
int nb = (sz > 8) ? 2 : 1;
|
|
int nflt[2], nint[2];
|
|
nflt[0] = nflt[1] = nint[0] = nint[1] = 0;
|
|
for (Tfield *f = t->fields; f; f = f->next) {
|
|
Type *fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (fu == NULL) return 0;
|
|
int foff = (int)f->offset;
|
|
int fsz = (int)fu->size;
|
|
int e = foff / 8;
|
|
if (e < 0 || e >= nb) return 0;
|
|
int f32;
|
|
if (fld_isfloat(f->type, &f32)) {
|
|
if (f32) return 0;
|
|
if (foff % 8 != 0 || fsz != 8) return 0;
|
|
nflt[e]++;
|
|
} else {
|
|
if (fu->kind == TY_STRUCT || fu->kind == TY_ARRAY
|
|
|| fu->kind == TY_SLICE || fu->kind == TY_STR
|
|
|| fu->kind == TY_TAGGED || fu->kind == TY_TUPLE)
|
|
return 0;
|
|
if (fsz > 8 || (foff + fsz - 1) / 8 != e) return 0;
|
|
nint[e]++;
|
|
}
|
|
}
|
|
int hasfloat = 0;
|
|
for (int e = 0; e < nb; e++) {
|
|
if (nflt[e] == 1 && nint[e] == 0) {
|
|
cls[e] = 1;
|
|
hasfloat = 1;
|
|
} else if (nflt[e] == 0) {
|
|
cls[e] = 0;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
return hasfloat ? nb : 0;
|
|
}
|
|
|
|
/* Tagged-union arg byte size: 16 (8B variants) or 24 (16B variants).
|
|
* Nullable-folded `(*T | void)` collapses to 8 bytes (just the
|
|
* pointer). Returns 0 if not a tagged union or too large to pass
|
|
* in registers. */
|
|
static int
|
|
tagged_arg_size(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
|
/* Param/let/struct contexts have 6 int regs (DI..R9) so a 48B
|
|
* tagged union (6 words) still fits in registers. Return values
|
|
* are stricter (AX:DX:CX, max 24B) — gated separately in
|
|
* cgreturn. */
|
|
if (t->size > 48) return 0;
|
|
return (int)t->size;
|
|
}
|
|
|
|
/* type_isnullable — TY_TAGGED with the (*T | void) one-word fold. */
|
|
static int
|
|
type_isnullable(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_TAGGED && t->nullable;
|
|
}
|
|
|
|
/* nullable_ptr_tag — index of the *T variant in a nullable union.
|
|
* Returns 0 or 1; the void variant takes the other slot. */
|
|
static int
|
|
nullable_ptr_tag(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
|
int i = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, i++) {
|
|
Type *pu = (p->type && p->type->kind == TY_NAMED)
|
|
? p->type->under : p->type;
|
|
if (pu && pu->kind == TY_PTR) return i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
node_istaggedarg(Node *n)
|
|
{
|
|
return n && tagged_arg_size(n->type) > 0;
|
|
}
|
|
|
|
static int
|
|
node_isstructarg(Node *n)
|
|
{
|
|
if (n == NULL) return 0;
|
|
int sz = struct_arg_size(n->type);
|
|
return sz > 0 && sz <= 16;
|
|
}
|
|
|
|
/* aggarg_size — byte size of a by-value aggregate (struct OR array)
|
|
* call arg, else 0. The size axis the ≤16B-struct node_isstructarg
|
|
* carve-out doesn't cover: arrays of any size and structs > 16B (#271).
|
|
* Pure-int transport only; a float-bearing struct keeps the #165 SSE
|
|
* eightbyte path (gated separately at the push/drain sites). */
|
|
static int
|
|
aggarg_size(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_STRUCT || t->kind == TY_ARRAY)
|
|
return (int)t->size;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
node_isaggarg(Node *n)
|
|
{
|
|
return n && aggarg_size(n->type) > 0;
|
|
}
|
|
|
|
/* Pick the appropriate scalar SSE opcode (SS vs SD) for a node's
|
|
* float type. Untyped float defaults to SD. */
|
|
static int
|
|
op_for(Node *n, int sd_op, int ss_op)
|
|
{
|
|
return node_isf32(n) ? ss_op : sd_op;
|
|
}
|
|
|
|
/* Strict variant matcher. Returns 1 iff a value of `src` should be
|
|
* tagged as variant `vt` in a tagged-union dispatch:
|
|
* - untyped src: first variant whose type can hold it (type_assignable)
|
|
* - both NAMED: pointer-identical (same `type` declaration node)
|
|
* - one NAMED, the other not: no match (different nominal types)
|
|
* - otherwise: structural type_eq
|
|
* The pointer-identity rule is what keeps `(str | linerr)` distinguishable
|
|
* even though linerr unwraps to str. */
|
|
static int
|
|
cg_variant_match(Type *vt, Type *src)
|
|
{
|
|
if (vt == NULL || src == NULL) return 0;
|
|
if (type_isuntyped(src)) return type_assignable(vt, src);
|
|
if (vt->kind == TY_NAMED && src->kind == TY_NAMED) return vt == src;
|
|
if (vt->kind == TY_NAMED || src->kind == TY_NAMED) {
|
|
/* #218: nominal identity is lost when the source's stamped
|
|
* type was collapsed to its unwrapped tagged (project
|
|
* tinfo_lossy_nominal). A NAMED multi-variant union variant vs
|
|
* an unwrapped-tagged source can still be THE nested variant —
|
|
* fall back to structural equality of the two unwrapped tagged
|
|
* unions so the outer widen tag (cg_tag_for_variant) computes.
|
|
* Sound only while the model is nominal-lossy; the collision
|
|
* guard at the widen site (cg_widen_tagged_store) enforces the
|
|
* invariant for when #199b/B-full lands true nominal layout. */
|
|
Type *vu = (vt->kind == TY_NAMED) ? vt->under : vt;
|
|
Type *su = (src->kind == TY_NAMED) ? src->under : src;
|
|
if (vu && su && vu->kind == TY_TAGGED && su->kind == TY_TAGGED)
|
|
return type_eq(vu, su);
|
|
return 0;
|
|
}
|
|
return type_eq(vt, src);
|
|
}
|
|
|
|
/* cg_variant_struct_match — structural equality of two variants ignoring
|
|
* nominal identity (peel NAMED, then type_eq). #218: the collision guard
|
|
* at the nested-widen site counts how many du variants share the source's
|
|
* *shape*; ≥2 means the structural fallback could not disambiguate them
|
|
* once nominal identity is lost. cg_variant_match (pointer-id for both-
|
|
* NAMED) would under-count here, so the guard needs the shape-only view. */
|
|
static int
|
|
cg_variant_struct_match(Type *vt, Type *src)
|
|
{
|
|
Type *vu = (vt && vt->kind == TY_NAMED) ? vt->under : vt;
|
|
Type *su = (src && src->kind == TY_NAMED) ? src->under : src;
|
|
if (vu == NULL || su == NULL) return 0;
|
|
return type_eq(vu, su);
|
|
}
|
|
|
|
/* cg_tagged_success_tag — index of the success variant in a tagged
|
|
* union. Mirrors check.c tagged_success_type: explicit-flag mode
|
|
* picks the first non-`!`-marked variant; legacy mode picks index 0. */
|
|
static int
|
|
cg_tagged_success_tag(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
|
int has_err = 0;
|
|
for (Tparam *p = t->params; p; p = p->next)
|
|
if (p->type && p->type->iserror) { has_err = 1; break; }
|
|
if (!has_err) return 0;
|
|
int idx = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, idx++)
|
|
if (p->type && !p->type->iserror) return idx;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
cg_variant_is_error(Type *t, int idx)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
|
int has_err = 0;
|
|
for (Tparam *p = t->params; p; p = p->next)
|
|
if (p->type && p->type->iserror) { has_err = 1; break; }
|
|
int i = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, i++) {
|
|
if (i == idx) {
|
|
if (has_err) return p->type && p->type->iserror;
|
|
/* legacy: index 0 is success, rest are errors */
|
|
return idx != 0;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Find the variant-tag index of `vt` inside the tagged-union type `t`.
|
|
* Returns -1 if `t` is not tagged or `vt` does not match a variant.
|
|
* Used by N_MATCH dispatch and by the let/assign/return tag synthesis. */
|
|
static int
|
|
cg_tag_for_variant(Type *t, Type *vt)
|
|
{
|
|
if (t == NULL || vt == NULL) return -1;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return -1;
|
|
/* Pass 1: exact match (NAMED-vs-NAMED pointer-id, tagged-vs-tagged,
|
|
* bare type_eq). Exact matches take precedence and need no guard —
|
|
* distinct variants don't exact-match the same source. */
|
|
int idx = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, idx++) {
|
|
if (cg_variant_match(p->type, vt)) return idx;
|
|
}
|
|
/* Pass 2 (#15): no exact variant matched — try a structural match of
|
|
* a BARE source against a NAMED-alias variant (e.g. a bare `*vtable`
|
|
* into the `stream` (= *vtable) variant of `(file | stream)`). The
|
|
* bare side has no nominal identity, so structure is the only
|
|
* discriminator; without this the widen found no variant and
|
|
* defaulted to tag 0, miscompiling every io.write(&...vt) in cgen's
|
|
* emit path. Exact-first (pass 1) keeps a bare `i64` into
|
|
* `(i64 | oserror)` binding the exact `i64`, not the alias. drew's
|
|
* proviso: guard the structural fallback like the #218 nested-widen
|
|
* site — if a bare source structurally matches >=2 NAMED variants,
|
|
* nominal layout is needed to disambiguate, so hard-error rather
|
|
* than silently first-pick. */
|
|
if (vt->kind != TY_NAMED) {
|
|
int found = -1, n = 0;
|
|
idx = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, idx++) {
|
|
/* One-level NAMED unwrap: a chained ptr-alias variant
|
|
* (type a=*X; type b=a) isn't reached here, so it would
|
|
* silently mis-tag — unexercised (zero in corpus), see
|
|
* task #17. */
|
|
Type *pu = p->type;
|
|
if (pu && pu->kind == TY_NAMED && pu->under
|
|
&& type_eq(pu->under, vt)) {
|
|
if (found < 0) found = idx;
|
|
n++;
|
|
}
|
|
}
|
|
if (n >= 2)
|
|
fatal("cg_tag_for_variant: bare source structurally "
|
|
"matches >=2 NAMED variants — ambiguous without "
|
|
"nominal layout (#15/#218/#199b/#10)");
|
|
return found;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
type_istagged(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_TAGGED;
|
|
}
|
|
|
|
/* FFI map: ww-side ident name → linker-side symbol name. Built from
|
|
* @symbol("real_name") attributes on fn declarations. */
|
|
typedef struct Ffi Ffi;
|
|
struct Ffi {
|
|
const char *ident;
|
|
const char *symbol;
|
|
Ffi *next;
|
|
};
|
|
static Ffi *ffi_map;
|
|
|
|
/* Def-as-string-literal map. `def NAME: str = "lit"` doesn't materialise
|
|
* as a real linker symbol; instead, references to NAME load the same
|
|
* (ptr, len) pair that the literal would. Avoids needing relocations
|
|
* inside DATA blocks for the ptr field of a str header. */
|
|
typedef struct Sdef Sdef;
|
|
struct Sdef {
|
|
const char *name;
|
|
const char *mod; /* raw `// MODULE:` directive on the decl,
|
|
* or NULL. Mirrors cgfn's c->cur_mod which
|
|
* stores the same raw form. */
|
|
const char *bytes;
|
|
u64 len;
|
|
Sdef *next;
|
|
};
|
|
static Sdef *sdefs;
|
|
|
|
/* Same-module-first match for Sdef walks. Mirrors wwstage deflookuprhs's
|
|
* first pass: returns 1 iff s belongs to the fn we're emitting. Caller
|
|
* still re-walks for the any-module fallback. */
|
|
static int
|
|
sdef_mod_match(Cg *c, Sdef *s)
|
|
{
|
|
const char *a = s->mod, *b = c->cur_mod;
|
|
if (a == b) return 1;
|
|
if (a == NULL || b == NULL) return 0;
|
|
return strcmp(a, b) == 0;
|
|
}
|
|
|
|
/* Explicit-hint variant for `mod.NAME` N_DOT mod-qualified Sdef walks
|
|
* (sister of wwstage deflookuprhsmod). Walk #2 needs n->lhs->str — a
|
|
* cross-module qualifier from a third module won't match c->cur_mod
|
|
* and would fall back to head-pick, possibly inlining the wrong-module
|
|
* strlit when both source modules export the same-leaf str def. */
|
|
static int
|
|
sdef_mod_match_hint(Sdef *s, const char *hint)
|
|
{
|
|
const char *a = s->mod;
|
|
if (a == hint) return 1;
|
|
if (a == NULL || hint == NULL) return 0;
|
|
return strcmp(a, hint) == 0;
|
|
}
|
|
|
|
/* Interned string literals — emitted as DATA directives after all
|
|
* function bodies, so the linker lays them out alongside .text. */
|
|
typedef struct Strlit Strlit;
|
|
struct Strlit {
|
|
const char *label;
|
|
const char *bytes;
|
|
u64 len;
|
|
Strlit *next;
|
|
};
|
|
static Strlit *strlits;
|
|
static int strlit_seq;
|
|
|
|
static const char *
|
|
intern_strlit(Cg *c, const char *bytes, u64 len)
|
|
{
|
|
for (Strlit *s = strlits; s; s = s->next)
|
|
if (s->len == len && memcmp(s->bytes, bytes, len) == 0)
|
|
return s->label;
|
|
Strlit *s = amalloc(c->a, sizeof *s);
|
|
s->label = aprintf(c->a, "_S_%d", strlit_seq++);
|
|
s->bytes = bytes;
|
|
s->len = len;
|
|
s->next = strlits;
|
|
strlits = s;
|
|
return s->label;
|
|
}
|
|
|
|
static void
|
|
emit_data(Cg *c, FILE *out)
|
|
{
|
|
for (Strlit *s = strlits; s; s = s->next) {
|
|
fprintf(out, "DATA %s(SB),\"", s->label);
|
|
for (u64 i = 0; i < s->len; i++) {
|
|
unsigned char b = (unsigned char)s->bytes[i];
|
|
switch (b) {
|
|
case '"': fputs("\\\"", out); break;
|
|
case '\\': fputs("\\\\", out); break;
|
|
case '\n': fputs("\\n", out); break;
|
|
case '\t': fputs("\\t", out); break;
|
|
case '\r': fputs("\\r", out); break;
|
|
default:
|
|
if (b < 0x20 || b >= 0x7f)
|
|
fprintf(out, "\\x%02x", b);
|
|
else
|
|
fputc(b, out);
|
|
}
|
|
}
|
|
/* Trailing NUL: lets `.ptr` be passed to libc / syscalls
|
|
* that expect a C string. The `len` field still excludes
|
|
* this byte, so iteration semantics are unchanged. */
|
|
fputs("\\x00", out);
|
|
fputs("\"\n", out);
|
|
}
|
|
(void)c;
|
|
}
|
|
|
|
static const char *
|
|
ffi_resolve(const char *ident)
|
|
{
|
|
for (Ffi *f = ffi_map; f; f = f->next)
|
|
if (strcmp(f->ident, ident) == 0) return f->symbol;
|
|
return ident;
|
|
}
|
|
|
|
static void
|
|
ffi_collect(Cg *c, Node *file)
|
|
{
|
|
ffi_map = NULL;
|
|
if (file == NULL) return;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_FNDECL) continue;
|
|
for (Node *a = d->attr; a; a = a->next) {
|
|
if (a->kind != N_ATTR) continue;
|
|
if (strcmp(a->str, "symbol") != 0) continue;
|
|
if (a->list == NULL || a->list->kind != N_STRLIT) continue;
|
|
Ffi *f = amalloc(c->a, sizeof *f);
|
|
f->ident = d->str;
|
|
f->symbol = a->list->str;
|
|
f->next = ffi_map;
|
|
ffi_map = f;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Module-private symbol map. Mirrors selfhost/cmd/wcc/cgen.ww. Every
|
|
* non-FFI top-level fn decl is mangled to <module>.<name> at emission
|
|
* time so two modules can each define the same fn leaf — including
|
|
* exported ones (lib/os and lib/io both ship `read`/`write`/`close`)
|
|
* — without colliding at link time. Non-fn decls (let/def/type) keep
|
|
* the older "non-exported only" rule: their export-side namespace is
|
|
* the user-facing data ABI and mangling them changes the surface. */
|
|
typedef struct Mod Mod;
|
|
struct Mod {
|
|
const char *name;
|
|
const char *module;
|
|
Mod *next;
|
|
};
|
|
static Mod *mod_map;
|
|
|
|
/* Top-level `let` map. Populated alongside mod_map; consulted by the
|
|
* N_IDENT store path and the &-of path to route reads/writes through
|
|
* a RIP-relative reference rather than dropping them as the (pre-
|
|
* writable-.data) compiler did. emit_lets emits a DATAW for each. */
|
|
typedef struct LetVar LetVar;
|
|
struct LetVar {
|
|
const char *name;
|
|
Type *type; /* #128b: imported-let type lookup for module-
|
|
* qualified N_INDEX base esz dispatch. */
|
|
LetVar *next;
|
|
};
|
|
static LetVar *letvars;
|
|
|
|
/* #129 A.2: struct-typed defs that now have DATA storage need the
|
|
* same LEAQ-and-field-offset N_DOT-load shape as struct-typed lets.
|
|
* Tracked separately so let_islet's existing callers (which gate
|
|
* scalar/float/str arms) don't pick up struct defs and re-route their
|
|
* narrow-load logic. */
|
|
typedef struct DefStruct DefStruct;
|
|
struct DefStruct {
|
|
const char *name;
|
|
Type *type;
|
|
DefStruct *next;
|
|
};
|
|
static DefStruct *defstructs;
|
|
|
|
/* #129 A.3: array-typed defs now have DATA storage and need the same
|
|
* LEAQ name(SB) + indexed-load shape as array-typed lets at cgindex
|
|
* and N_DOT base-resolution sites. Mirrors DefStruct (A.2). */
|
|
typedef struct DefArray DefArray;
|
|
struct DefArray {
|
|
const char *name;
|
|
Type *type;
|
|
DefArray *next;
|
|
};
|
|
static DefArray *defarrays;
|
|
|
|
/* #149: every top-level `def`, regardless of kind. Backs the address-of
|
|
* path's is-any-def check (loud error on `&<non-addressable def>`) and
|
|
* the scalar-addressable gate. Mirrors wwstage collectdefs / deflookup,
|
|
* which already track all N_DEF. */
|
|
typedef struct DefAny DefAny;
|
|
struct DefAny {
|
|
const char *name;
|
|
Type *type;
|
|
Node *rhs;
|
|
DefAny *next;
|
|
};
|
|
static DefAny *defall;
|
|
|
|
/* Slot size for a top-level `let` of type t, or 0 if the type isn't
|
|
* supported as a writable global yet. Tagged unions are deferred.
|
|
* enums route through their storage type.
|
|
* Keep this tight — extending it requires the matching load/store
|
|
* code below. */
|
|
static int
|
|
let_emit_size(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
if (u == NULL) return 0;
|
|
switch (u->kind) {
|
|
case TY_BOOL: case TY_RUNE:
|
|
case TY_I8: case TY_I16: case TY_I32: case TY_I64:
|
|
case TY_U8: case TY_U16: case TY_U32: case TY_U64:
|
|
case TY_INT: case TY_UINT: case TY_UINTPTR: case TY_SIZE:
|
|
case TY_PTR:
|
|
return 8;
|
|
case TY_F32:
|
|
return 4; /* MOVSS loads/stores 4B via LEAQ+indir. */
|
|
case TY_F64:
|
|
return 8; /* MOVSD loads/stores 8B via LEAQ+indir. */
|
|
case TY_STR:
|
|
case TY_SLICE:
|
|
return (int)u->size; /* #43: ty_str / ty_slice SSoT. */
|
|
case TY_STRUCT:
|
|
return (int)u->size; /* zero-init only; field reads/
|
|
* scalar-field writes only. */
|
|
case TY_ARRAY:
|
|
return (int)u->size; /* zero-init only; element
|
|
* loads/stores via cgindex. Mirror
|
|
* of selfhost letemitsize's
|
|
* N_TARRAY branch. */
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Is the unwrapped type a str? Used by the load/store paths so the
|
|
* (AX, BX) pair convention is preserved for str globals, mirroring
|
|
* what we already do for str locals. */
|
|
static int
|
|
let_isstr(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_STR;
|
|
}
|
|
|
|
/* Is the unwrapped type a slice? Slice globals flow as the (AX, BX,
|
|
* CX) triple — same as the local ABI. */
|
|
static int
|
|
let_isslice(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_SLICE;
|
|
}
|
|
|
|
/* Is the unwrapped type a struct? Struct globals only support field
|
|
* access (read + plain `=` write for scalar fields). Whole-struct
|
|
* by-value flow through expressions isn't wired. */
|
|
static int
|
|
let_isstruct(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_STRUCT;
|
|
}
|
|
|
|
/* Is the unwrapped type a fixed-length array? Array globals are
|
|
* zero-init DATAW slots; cgindex addresses them as LEAQ name(SB)
|
|
* and lets the element load/store run as usual. */
|
|
static int
|
|
let_isarray(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_ARRAY;
|
|
}
|
|
|
|
/* Is the unwrapped type a float (f32 or f64)? Float globals flow
|
|
* through X0 — load/store goes LEAQ name(SB),CX → MOVSS/MOVSD via the
|
|
* indirect, since the asm has no D_EXTERN form for SSE moves yet. */
|
|
static int
|
|
let_isfloat(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && (u->kind == TY_F32 || u->kind == TY_F64);
|
|
}
|
|
|
|
/* Returns the unwrapped Type — handy when we need to walk struct
|
|
* fields. NULL if t is NULL or unresolved. */
|
|
static Type *
|
|
type_unwrap(Type *t)
|
|
{
|
|
if (t == NULL) return NULL;
|
|
return (t->kind == TY_NAMED) ? t->under : t;
|
|
}
|
|
|
|
/* Element-effective type for indexing. For `*[N]T` we drill through
|
|
* the pointer to the underlying array so esz/esub reflect T, not the
|
|
* whole-array pointee. For everything else returns t unchanged. */
|
|
static Type *
|
|
idx_eff(Type *t)
|
|
{
|
|
if (t == NULL) return NULL;
|
|
Type *u = type_unwrap(t);
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *p = type_unwrap(u->sub);
|
|
if (p && p->kind == TY_ARRAY) return p;
|
|
}
|
|
return u;
|
|
}
|
|
|
|
static int
|
|
decl_has_ffisym(Node *d)
|
|
{
|
|
for (Node *a = d->attr; a; a = a->next) {
|
|
if (a->kind != N_ATTR) continue;
|
|
if (strcmp(a->str, "symbol") == 0) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Skip rule = {@symbol, main, empty-module}. Do NOT skip on `export` for fns.
|
|
* Both stages must match exactly — ww2/ww3/ww4 byte-identity depends on it. */
|
|
static void
|
|
mod_collect(Cg *c, Node *file)
|
|
{
|
|
mod_map = NULL;
|
|
if (file == NULL) return;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
int isfn = (d->kind == N_FNDECL);
|
|
int track = isfn || (d->kind == N_TYPEDECL)
|
|
|| (d->kind == N_DEF) || (d->kind == N_LET);
|
|
if (!track) continue;
|
|
/* Non-fn decls (let/def/type) still skip exported entries —
|
|
* their export-side namespace is the user-facing data ABI
|
|
* and mangling them changes the surface. Fns mangle
|
|
* unconditionally so cross-module same-leaf exports
|
|
* (os.read vs io.read) coexist at link time. */
|
|
if (!isfn && d->export) continue;
|
|
if (d->module == NULL || d->module[0] == '\0') continue;
|
|
if (decl_has_ffisym(d)) continue;
|
|
/* `main` is the linker entry-point convention. Even when not
|
|
* marked `export`, it must keep its bare name so w6l can
|
|
* resolve `_start`'s `CALL main(SB)`. */
|
|
if (d->str && strcmp(d->str, "main") == 0) continue;
|
|
Mod *m = amalloc(c->a, sizeof *m);
|
|
m->name = d->str;
|
|
m->module = d->module;
|
|
m->next = mod_map;
|
|
mod_map = m;
|
|
}
|
|
}
|
|
|
|
/* Returns the originating module for a name, or NULL if the name
|
|
* isn't a registered private decl. By-name only — works for non-fn
|
|
* refs (let/def/type) where the mod_collect skip rule keeps each leaf
|
|
* unique across the program. Fn refs go through mod_lookup_for_fn
|
|
* since multiple modules can now export the same fn leaf. */
|
|
static const char *
|
|
mod_lookup(const char *name)
|
|
{
|
|
for (Mod *m = mod_map; m; m = m->next)
|
|
if (strcmp(m->name, name) == 0) return m->module;
|
|
return NULL;
|
|
}
|
|
|
|
/* Hint-aware variant for fn names. Walks mod_map looking for a
|
|
* (name, hint) pair; returns NULL if there's no leaf-name match at
|
|
* all, the hinted module if a match exists, or the first leaf match
|
|
* when the caller had no hint. The hint comes from AST shape:
|
|
* - N_DOT call `m.fn(...)`: hint = the SK_USE module ident's str.
|
|
* - bare N_IDENT call `fn(...)`: hint = c->cur_mod (current fn's
|
|
* module — bare names resolve same-module by ww's rules).
|
|
* Falling back to the first leaf match preserves the legacy single-
|
|
* owner shape for callers that don't (yet) thread a hint. */
|
|
static const char *
|
|
mod_lookup_for_fn(const char *name, const char *hint)
|
|
{
|
|
const char *first = NULL;
|
|
for (Mod *m = mod_map; m; m = m->next) {
|
|
if (strcmp(m->name, name) != 0) continue;
|
|
if (hint != NULL && m->module != NULL
|
|
&& strcmp(m->module, hint) == 0)
|
|
return m->module;
|
|
if (first == NULL) first = m->module;
|
|
}
|
|
return first;
|
|
}
|
|
|
|
/* Value-global variant: mangle ONLY on an exact (name, hint) match;
|
|
* otherwise return NULL so the name stays bare. Unlike the fn variant
|
|
* there is NO first-leaf-match fallback — exported value globals are
|
|
* export-skipped from mod_map (mod_collect keeps their bare-name data
|
|
* ABI, see the skip at `!isfn && d->export`), so a first-match fallback
|
|
* would mis-mangle an exported `v` onto another module's private `v`
|
|
* (#1 cgen value-global module-qualifier, the cgen residual of #55).
|
|
* Bare-on-miss is correct: a missing entry means the leaf is either an
|
|
* exported global (its own bare symbol) or not module-private at all.
|
|
*
|
|
* HONEST BOUNDARY (rule 7) — do NOT "fix" the following into a
|
|
* workaround: if two modules BOTH export the same value leaf, both stay
|
|
* bare and the linker sees a duplicate symbol. That is a CORRECT, loud,
|
|
* link-time ABI clash (identical to C's two-extern-same-name rule), NOT
|
|
* a silent miscompile. A bare reference can never legitimately resolve
|
|
* to another module's PRIVATE global, so first-match is never wanted on
|
|
* the value path; the only ambiguity left is genuine duplicate exports,
|
|
* which belong to the linker, not to a cgen disambiguation heuristic. */
|
|
static const char *
|
|
mod_lookup_value(const char *name, const char *hint)
|
|
{
|
|
if (hint == NULL) return NULL;
|
|
for (Mod *m = mod_map; m; m = m->next) {
|
|
if (strcmp(m->name, name) != 0) continue;
|
|
if (m->module != NULL && strcmp(m->module, hint) == 0)
|
|
return m->module;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Collect every top-level `let` whose declared type we can store
|
|
* in a single .data slot. Names not in this map fall through to
|
|
* the old "drop assignment" path; with a clear link-time
|
|
* undefined-symbol error on any read. */
|
|
static void
|
|
let_collect(Cg *c, Node *file)
|
|
{
|
|
letvars = NULL;
|
|
defstructs = NULL;
|
|
defarrays = NULL;
|
|
defall = NULL;
|
|
if (file == NULL) return;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind == N_LET) {
|
|
if (d->str == NULL || d->str[0] == '\0') continue;
|
|
if (let_emit_size(d->type) == 0) continue;
|
|
LetVar *lv = amalloc(c->a, sizeof *lv);
|
|
lv->name = d->str;
|
|
lv->type = d->type;
|
|
lv->next = letvars;
|
|
letvars = lv;
|
|
continue;
|
|
}
|
|
if (d->kind == N_DEF) {
|
|
if (d->str == NULL || d->str[0] == '\0') continue;
|
|
/* #149: track every def (any kind) so the address-of
|
|
* path can tell a def from an unknown ident and loud-
|
|
* error on `&<non-addressable def>`. */
|
|
DefAny *dn = amalloc(c->a, sizeof *dn);
|
|
dn->name = d->str;
|
|
dn->type = d->type;
|
|
dn->rhs = d->rhs;
|
|
dn->next = defall;
|
|
defall = dn;
|
|
/* #129 A.2: struct-typed defs now have DATA storage
|
|
* (emit_defs struct arm); register them so the N_DOT
|
|
* struct-let LEAQ-and-offset shape widens to cover
|
|
* them too. Other def kinds (int / float / str)
|
|
* stay on their existing load paths. */
|
|
if (let_isstruct(d->type)) {
|
|
DefStruct *ds = amalloc(c->a, sizeof *ds);
|
|
ds->name = d->str;
|
|
ds->type = d->type;
|
|
ds->next = defstructs;
|
|
defstructs = ds;
|
|
continue;
|
|
}
|
|
/* #129 A.3: array-typed defs now have DATA storage
|
|
* (emit_defs array arm); register them so cgindex's
|
|
* `let_islet`-gated LEAQ name(SB) base-load widens
|
|
* to defs too (LOAD-side twin of the struct-def
|
|
* registry). */
|
|
if (let_isarray(d->type)) {
|
|
DefArray *da = amalloc(c->a, sizeof *da);
|
|
da->name = d->str;
|
|
da->type = d->type;
|
|
da->next = defarrays;
|
|
defarrays = da;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int
|
|
def_isstructdef(const char *name)
|
|
{
|
|
if (name == NULL) return 0;
|
|
for (DefStruct *ds = defstructs; ds; ds = ds->next)
|
|
if (strcmp(ds->name, name) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
def_isarraydef(const char *name)
|
|
{
|
|
if (name == NULL) return 0;
|
|
for (DefArray *da = defarrays; da; da = da->next)
|
|
if (strcmp(da->name, name) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* #149: rhs peels (N_CAST / unary ±) to a float literal — the exact
|
|
* shape emit_floatlit_data (cgen.c) emits a DATA symbol for. The scalar-
|
|
* def address-of gate MUST equal that emission set, or `&def` LEAQs a
|
|
* symbol the data pass never wrote. Keep in sync with the peel inside
|
|
* emit_floatlit_data. */
|
|
static int
|
|
floatlit_leaf(Node *rhs)
|
|
{
|
|
Node *r = rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r != NULL && r->kind == N_UN
|
|
&& (r->op == TK_MINUS || r->op == TK_PLUS)) {
|
|
r = r->lhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
}
|
|
return r != NULL && r->kind == N_FLOATLIT;
|
|
}
|
|
|
|
/* #149/#147: a scalar (int/float) def is addressable iff emit_defs emits
|
|
* a DATA symbol for it — int via fold_int_literal, float via the
|
|
* FLOATLIT-leaf shape. Gate is held identical to emit_defs's emission
|
|
* gate so the addressable set matches byte-for-byte. Computed-rhs floats
|
|
* (`def NAN = 0.0/0.0`, #147) fold to no symbol and are excluded → they
|
|
* route to the address-of loud error, never a LEAQ of a missing sym. */
|
|
static int
|
|
def_isscalardef(const char *name)
|
|
{
|
|
if (name == NULL) return 0;
|
|
for (DefAny *dn = defall; dn; dn = dn->next) {
|
|
if (strcmp(dn->name, name) != 0) continue;
|
|
if (dn->rhs == NULL) return 0;
|
|
u64 v;
|
|
if (fold_int_literal(dn->rhs, &v)) return 1;
|
|
if (let_isfloat(dn->type) && floatlit_leaf(dn->rhs)) return 1;
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
def_isanydef(const char *name)
|
|
{
|
|
if (name == NULL) return 0;
|
|
for (DefAny *dn = defall; dn; dn = dn->next)
|
|
if (strcmp(dn->name, name) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
let_islet(const char *name)
|
|
{
|
|
if (name == NULL) return 0;
|
|
for (LetVar *lv = letvars; lv; lv = lv->next)
|
|
if (strcmp(lv->name, name) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* #128b: look up a top-level let's type by leaf name. Sister of
|
|
* wwstage's letvartnode (selfhost/cmd/wcc/cgen.ww:999). Used at the
|
|
* cgindex / cg_dotbase_addr sites where a module-qualified base
|
|
* (`mod.arr`) leaves n->lhs->type NULL (SK_USE-bound module ident),
|
|
* so the imported array's element type / size must come through
|
|
* this let-map lookup instead. Returns NULL if name isn't a tracked
|
|
* top-level let. */
|
|
static Type *
|
|
let_var_type(const char *name)
|
|
{
|
|
if (name == NULL) return NULL;
|
|
for (LetVar *lv = letvars; lv; lv = lv->next)
|
|
if (strcmp(lv->name, name) == 0) return lv->type;
|
|
return NULL;
|
|
}
|
|
|
|
/* Glue `<module>.<ident>` into a fresh arena buffer. */
|
|
static const char *
|
|
mod_join(Cg *c, const char *mod, const char *ident)
|
|
{
|
|
size_t mn = strlen(mod), in = strlen(ident);
|
|
char *buf = amalloc(c->a, mn + 1 + in + 1);
|
|
memcpy(buf, mod, mn);
|
|
buf[mn] = '.';
|
|
memcpy(buf + mn + 1, ident, in);
|
|
buf[mn + 1 + in] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
/* Mangle an AST identifier into its asm linker symbol:
|
|
* - @symbol("...") binding wins (return mapped name).
|
|
* - module-private decl → <module>.<name>.
|
|
* - else → name unchanged.
|
|
* Used at every CALL/MOVQ/LEAQ site that targets an AST name. Plain
|
|
* `asym(s)` still emits `s` verbatim — use it for strlit labels and
|
|
* hard-coded runtime symbols like "rt_streq". */
|
|
static const char *
|
|
mod_mangle(Cg *c, const char *ident)
|
|
{
|
|
const char *resolved = ffi_resolve(ident);
|
|
if (resolved != ident) return resolved;
|
|
const char *mod = mod_lookup(ident);
|
|
if (mod == NULL) return ident;
|
|
return mod_join(c, mod, ident);
|
|
}
|
|
|
|
/* Fn-flavoured mangle: same shape as mod_mangle but consults
|
|
* mod_lookup_for_fn so the right module wins when multiple modules
|
|
* register the same fn leaf. `hint` is the explicit module from a
|
|
* N_DOT call site (or c->cur_mod for bare-ident calls); pass NULL
|
|
* to get the legacy first-match-wins behaviour. */
|
|
static const char *
|
|
mod_mangle_fn(Cg *c, const char *ident, const char *hint)
|
|
{
|
|
const char *resolved = ffi_resolve(ident);
|
|
if (resolved != ident) return resolved;
|
|
const char *mod = mod_lookup_for_fn(ident, hint);
|
|
if (mod == NULL) return ident;
|
|
return mod_join(c, mod, ident);
|
|
}
|
|
|
|
/* Value-global flavoured mangle: same shape as mod_mangle_fn but over
|
|
* mod_lookup_value (exact-(ident,hint)-or-bare, no first-match
|
|
* fallback). See mod_lookup_value for why value globals can't share the
|
|
* fn fallback. */
|
|
static const char *
|
|
mod_mangle_value(Cg *c, const char *ident, const char *hint)
|
|
{
|
|
const char *resolved = ffi_resolve(ident);
|
|
if (resolved != ident) return resolved;
|
|
const char *mod = mod_lookup_value(ident, hint);
|
|
if (mod == NULL) return ident;
|
|
return mod_join(c, mod, ident);
|
|
}
|
|
|
|
/* Forward decl — masym below depends on asym defined further down. */
|
|
static Adr asym(const char *s);
|
|
|
|
static Adr
|
|
masym(Cg *c, const char *ident)
|
|
{
|
|
return asym(mod_mangle(c, ident));
|
|
}
|
|
|
|
/* Fn-name address builder. Use at every CALL/LEAQ site whose target
|
|
* is a top-level fn — passes the hint so cross-module same-leaf
|
|
* exports resolve to the right module. */
|
|
static Adr
|
|
mafn(Cg *c, const char *ident, const char *hint)
|
|
{
|
|
return asym(mod_mangle_fn(c, ident, hint));
|
|
}
|
|
|
|
/* Value-global address builder. masym's non-hinted mod_lookup picks
|
|
* the first leaf-name match, so two modules with a same-leaf value
|
|
* global (`let v` in both) collapse onto one DATA label and a bare
|
|
* cross-module read resolves to the wrong module (#1 cgen value-global
|
|
* module-qualifier, the cgen residual of #55). Thread a per-site hint
|
|
* the way mafn does — curmod at a bare reference, the decl's own module
|
|
* at the definition label — over the same module-generic decl map.
|
|
* Kept distinct from mafn (vs renamed) to leave the fn-mangle path
|
|
* byte-for-byte untouched. Routes through mod_mangle_value (exact-or-
|
|
* bare) so an exported global stays bare instead of mis-mangling onto
|
|
* another module's same-leaf private global. */
|
|
static Adr
|
|
mahint(Cg *c, const char *ident, const char *hint)
|
|
{
|
|
return asym(mod_mangle_value(c, ident, hint));
|
|
}
|
|
|
|
void
|
|
cg_init(Cg *c, Arena *a)
|
|
{
|
|
memset(c, 0, sizeof *c);
|
|
c->a = a;
|
|
}
|
|
|
|
Prog *
|
|
newprog(Cg *c, int op)
|
|
{
|
|
Prog *p = amalloc(c->a, sizeof *p);
|
|
p->as = op;
|
|
return p;
|
|
}
|
|
|
|
void
|
|
emit(Cg *c, Prog *p)
|
|
{
|
|
if (c->head == NULL) c->head = p;
|
|
else c->tail->link = p;
|
|
c->tail = p;
|
|
}
|
|
|
|
static Adr
|
|
areg(int r)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = r;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
aimm(long long v)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_CONST;
|
|
a.offset = v;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
amem(int r, long long off)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_INDIR;
|
|
a.reg = r;
|
|
a.offset = off;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
asym(const char *s)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_EXTERN;
|
|
a.sym = s;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
abranch(const char *s)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_BRANCH;
|
|
a.sym = s;
|
|
return a;
|
|
}
|
|
|
|
static char *
|
|
mklabel(Cg *c, const char *prefix)
|
|
{
|
|
/* Module-qualified to avoid cross-module same-leaf collisions
|
|
* (task #13). w6a accepts '.' in label-cont (lex.c:18). */
|
|
return aprintf(c->a, "%s%s%s_%s_%d",
|
|
c->cur_mod ? c->cur_mod : "",
|
|
c->cur_mod ? "." : "",
|
|
c->fnname ? c->fnname : "_", prefix, c->labelseq++);
|
|
}
|
|
|
|
static void
|
|
ins2(Cg *c, int op, Adr from, Adr to)
|
|
{
|
|
Prog *p = newprog(c, op);
|
|
p->from = from;
|
|
p->to = to;
|
|
emit(c, p);
|
|
}
|
|
|
|
static void
|
|
ins1(Cg *c, int op, Adr to)
|
|
{
|
|
Prog *p = newprog(c, op);
|
|
p->to = to;
|
|
emit(c, p);
|
|
}
|
|
|
|
/* tuple_store — store one received tuple element at BP-relative `off`
|
|
* from its SysV-class register. A slice/str rides its 3-word
|
|
* {ptr,len,cap} header from the INTEGER cursor tuple_rseq[gp..]; a float
|
|
* rides tuple_sse_seq[sse] via MOVSD/MOVSS (#105 single-float widened to
|
|
* the SSE cursor for #164/#107 multi-float); a scalar rides one INTEGER
|
|
* word from tuple_rseq[gp]. The caller owns the dual cursor (validated +
|
|
* advanced); this just emits the store. Shared by N_LET/N_MLET/N_MASSIGN
|
|
* and, per #171, struct unpack — mirrors wwstage cgenstmt.ww tupstore. */
|
|
static void
|
|
tuple_store(Cg *c, Type *t, int wide, int gp, int sse, int off)
|
|
{
|
|
int f32 = 0;
|
|
|
|
if (wide) {
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[gp + 0]),
|
|
amem(D_BP, off + 0)); /* .ptr */
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[gp + 1]),
|
|
amem(D_BP, off + 8)); /* .len */
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[gp + 2]),
|
|
amem(D_BP, off + 16)); /* .cap */
|
|
return;
|
|
}
|
|
if (fld_isfloat(t, &f32)) {
|
|
ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(tuple_sse_seq[sse]),
|
|
amem(D_BP, off));
|
|
return;
|
|
}
|
|
ins2(c, A_MOVQ, areg(tuple_rseq[gp]), amem(D_BP, off));
|
|
}
|
|
|
|
static void
|
|
ins0(Cg *c, int op)
|
|
{
|
|
emit(c, newprog(c, op));
|
|
}
|
|
|
|
static void
|
|
label(Cg *c, const char *s)
|
|
{
|
|
Prog *p = newprog(c, A_NOP);
|
|
p->label = s;
|
|
emit(c, p);
|
|
}
|
|
|
|
/* cgslicehdr — load the 24B slice/str header at `base`+0 into the
|
|
* (AX=ptr, BX=len, CX=cap) triple. `base` holds the element address;
|
|
* the load that targets `base` destroys it, so that word is emitted
|
|
* LAST. Order otherwise mirrors the slice-FIELD arm (len, cap, ptr).
|
|
* Shared by the N_INDEX str-element arms (caller does the kind-gate)
|
|
* and, later, the typeassert str-variant leaf (#9). */
|
|
static void
|
|
cgslicehdr(Cg *c, int base)
|
|
{
|
|
if (base != D_BX) ins2(c, A_MOVQ, amem(base, 8), areg(D_BX));
|
|
if (base != D_CX) ins2(c, A_MOVQ, amem(base, 16), areg(D_CX));
|
|
if (base != D_AX) ins2(c, A_MOVQ, amem(base, 0), areg(D_AX));
|
|
if (base == D_BX) ins2(c, A_MOVQ, amem(base, 8), areg(D_BX));
|
|
else if (base == D_CX) ins2(c, A_MOVQ, amem(base, 16), areg(D_CX));
|
|
else if (base == D_AX) ins2(c, A_MOVQ, amem(base, 0), areg(D_AX));
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* per-fn local table: name → stack offset (positive = below FP) */
|
|
|
|
typedef struct Local Local;
|
|
struct Local {
|
|
const char *name;
|
|
int off; /* relative to BP; negative for locals */
|
|
Local *next;
|
|
};
|
|
|
|
/* localoff — push a fresh stack slot for this binding and return its
|
|
* BP offset. Never dedups by name (post-#27): two `let a: T` in disjoint
|
|
* scopes within one fn must each get their own slot, sized to their own
|
|
* declared T. Pre-fix the dedup loop returned the first-allocated slot
|
|
* regardless of the new declaration's size, so an outer `let a: [128]u8`
|
|
* after an inner `let a: i64` would collapse onto the 8B slot and
|
|
* `a[127]` would land at +119(BP), past the saved RIP, into the
|
|
* caller's frame. localfind walks from the head, so the most recent
|
|
* binding still wins lookups inside its scope. */
|
|
static int
|
|
localoff(Cg *c, Local **head, const char *name, int size, int *frame)
|
|
{
|
|
int al = 8;
|
|
*frame = (*frame + size + al - 1) & ~(al - 1);
|
|
int off = -*frame;
|
|
Local *l = amalloc(c->a, sizeof *l);
|
|
l->name = name;
|
|
l->off = off;
|
|
l->next = *head;
|
|
*head = l;
|
|
return off;
|
|
}
|
|
|
|
/* local_alloc — synonym for localoff. Pre-#27 localoff deduped by name
|
|
* and local_alloc was the always-fresh escape hatch (match-arm bindings,
|
|
* synthetic scratch slots). Post-#27 localoff is also always-fresh, so
|
|
* the two are functionally identical; both names are kept so the call
|
|
* sites read intentfully (let-decl vs scratch). */
|
|
static int
|
|
local_alloc(Cg *c, Local **head, const char *name, int size, int *frame)
|
|
{
|
|
int al = 8;
|
|
*frame = (*frame + size + al - 1) & ~(al - 1);
|
|
int off = -*frame;
|
|
Local *l = amalloc(c->a, sizeof *l);
|
|
l->name = name;
|
|
l->off = off;
|
|
l->next = *head;
|
|
*head = l;
|
|
return off;
|
|
}
|
|
|
|
static int
|
|
localfind(Local *head, const char *name)
|
|
{
|
|
for (Local *l = head; l; l = l->next)
|
|
if (strcmp(l->name, name) == 0) return l->off;
|
|
return 0; /* 0 = not found (caller must verify) */
|
|
}
|
|
|
|
/* cg_tagscr_slot — the ONLY alloc path for the per-fn tagged scratch
|
|
* (#44). One cached slot per distinct slot size, named "@tagscr<sz>"
|
|
* so wwstage's localadd name-dedup keys the same way; first-use
|
|
* allocation order is the source order in both stages (byte-id). */
|
|
static int
|
|
cg_tagscr_slot(Cg *c, Local **locals_p, int sz)
|
|
{
|
|
for (int i = 0; i < cg_ntagscr; i++)
|
|
if (cg_tagscr_sz[i] == sz)
|
|
return cg_tagscr_off[i];
|
|
if (cg_ntagscr >= CG_NTAGSCR)
|
|
fatal("cg_tagscr_slot: more than %d distinct tagged "
|
|
"scratch sizes in one fn", CG_NTAGSCR);
|
|
cg_tagscr_off[cg_ntagscr] = local_alloc(c, locals_p,
|
|
aprintf(c->a, "@tagscr%d", sz), sz, cg_frame);
|
|
cg_tagscr_sz[cg_ntagscr] = sz;
|
|
cg_ntagscr++;
|
|
return cg_tagscr_off[cg_ntagscr - 1];
|
|
}
|
|
|
|
/* cg_base_cap — load the capacity of a sub-slice's UNDERLYING storage
|
|
* into `dst` for the #20 cap = base_cap - lo formula (drew: harec
|
|
* eval.c:1017 slice cap-=start / eval.c:1024 array cap=length-start;
|
|
* ensure.ha:4-8 distinct capacity field). array [N]T -> N (literal);
|
|
* slice/str -> the .capacity word carried in the header at +16 (the
|
|
* +16 load mirrors the hi-default +8 length dispatch, but emitted
|
|
* unconditionally). Returns 0 when base_cap isn't cleanly available so
|
|
* the caller keeps the prior cap=len: a non-ident base (cgexpr already
|
|
* discarded its header cap; recomputing would re-evaluate a possibly
|
|
* side-effecting base -- #74, which also owns the pre-existing
|
|
* defaulted-hi len gap there), or a GLOBAL str base (wwstage cgslice
|
|
* has no global-str load, #73 -- matching it keeps the stages
|
|
* byte-identical rather than introducing a fresh divergence). */
|
|
static int
|
|
cg_base_cap(Cg *c, Node *base, Type *bu, Local *locals, int dst)
|
|
{
|
|
if (!base || base->kind != N_IDENT)
|
|
return 0;
|
|
if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_MOVQ, aimm((long long)bu->alen), areg(dst));
|
|
return 1;
|
|
}
|
|
if (bu && (bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) && let_islet(base->str);
|
|
if (isglobal && bu->kind == TY_STR)
|
|
return 0;
|
|
if (isglobal) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(dst));
|
|
ins2(c, A_MOVQ, amem(dst, 16), areg(dst));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 16), areg(dst));
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* expressions: result lands in AX. Returns 1 on success. */
|
|
|
|
static void cgexpr(Cg*, Node*, Local*);
|
|
static void cgstmt(Cg*, Node*, Local**, int*);
|
|
static void cg_widen_tagged_push(Cg*, Local**, Type*, Node*, int);
|
|
static void cg_widen_tagged_store(Cg*, Local**, Type*, Node*, int, int, int);
|
|
static void cg_widen_tag_remap(Cg*, Type*, Type*, int);
|
|
|
|
/* cg_dotchain_addr — compute the ADDRESS of a dot/ident lvalue chain
|
|
* into `dst_reg`, dereferencing pointer links mid-chain. Returns 1 on
|
|
* success, 0 if a link isn't a struct / ptr-to-struct it can resolve.
|
|
* Recursion mirrors the read spine (cgen.c:3722 value-struct field /
|
|
* :4033 ptr-field): for `x.f`, recurse to &x, deref if x is a *struct
|
|
* (so dst holds the pointee base), then add f's offset. Touches ONLY
|
|
* dst_reg — no AX, no stack — so it honours cg_dotbase_addr's caller-
|
|
* spill contract. The chained-base arm of cg_dotbase_addr (#253) is its
|
|
* sole caller. */
|
|
static int
|
|
cg_dotchain_addr(Cg *c, Node *node, int dst_reg, Local *locals)
|
|
{
|
|
if (node == NULL) return 0;
|
|
if (node->kind == N_IDENT) {
|
|
int off = localfind(locals, node->str);
|
|
if (off != 0) {
|
|
ins2(c, A_LEAQ, amem(D_BP, off), areg(dst_reg));
|
|
return 1;
|
|
}
|
|
if (let_islet(node->str) || def_isstructdef(node->str)) {
|
|
ins2(c, A_LEAQ, masym(c, node->str), areg(dst_reg));
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
if (node->kind != N_DOT) return 0;
|
|
Node *x = node->lhs;
|
|
if (x == NULL) return 0;
|
|
Type *xt = x->type;
|
|
if (xt == NULL || xt == ty_err) return 0;
|
|
Type *xu = type_chase_named(xt);
|
|
if (xu == NULL) return 0;
|
|
int xviaptr = 0;
|
|
Type *st = NULL;
|
|
if (xu->kind == TY_PTR) {
|
|
Type *p = type_chase_named(xu->sub);
|
|
if (p && p->kind == TY_STRUCT) { st = p; xviaptr = 1; }
|
|
} else if (xu->kind == TY_STRUCT) {
|
|
st = xu;
|
|
}
|
|
if (st == NULL) return 0;
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = st->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, node->str) == 0) { f = fl; break; }
|
|
if (f == NULL) return 0;
|
|
if (!cg_dotchain_addr(c, x, dst_reg, locals)) return 0;
|
|
if (xviaptr)
|
|
ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg));
|
|
if ((int)f->offset != 0)
|
|
ins2(c, A_ADDQ, aimm((int)f->offset), areg(dst_reg));
|
|
return 1;
|
|
}
|
|
|
|
/* cg_dotbase_addr — compute &(inner.field) into `dst_reg` for an
|
|
* N_DOT base where `inner` is an N_IDENT local (struct value OR *struct
|
|
* pointer) OR a chained N_DOT (#253: `o.p.m` / `o.i.m` / `o.a.b.m`).
|
|
* Returns 1 if emitted, 0 if base shape isn't supported (the
|
|
* caller falls back to its prior `cgexpr(base); MOVQ AX, dst_reg`).
|
|
*
|
|
* #135: cgexpr on an N_DOT whose .field is a `[N]T`-typed field auto-
|
|
* derefs and loads the field's 8-byte VALUE as if it were a pointer.
|
|
* For an LHS or index-base shape (`d.fld[i] = v` / `d.fld[i]` read /
|
|
* `d.fld[i] OP= v`), the caller wants the field's ADDRESS — this helper
|
|
* supplies it inline, avoiding the value-load. Mirror primitive of the
|
|
* inverse template at cgen.c arr[i].field (the cgdot N_INDEX-lhs
|
|
* branch).
|
|
*
|
|
* #253: a chained inner (`inner` is itself an N_DOT) routes through
|
|
* cg_dotchain_addr to recover the container's base — the pointer VALUE
|
|
* of inner when inner is a *struct (viaptr), else the ADDRESS of inner
|
|
* — then adds the array field's offset. Closes the whole array-field-
|
|
* base-address family across every op (index r/w, addr-of, slice,
|
|
* compound) since all of them route through this helper.
|
|
*
|
|
* Caller-spill contract: the helper does NOT touch AX unless
|
|
* dst_reg == D_AX. Safe to call where AX holds an unrelated live value
|
|
* (BX dst); cg_dotchain_addr keeps the same contract. */
|
|
static int
|
|
cg_dotbase_addr(Cg *c, Node *base, int dst_reg, Local *locals)
|
|
{
|
|
if (base == NULL || base->kind != N_DOT) return 0;
|
|
Node *inner = base->lhs;
|
|
if (inner == NULL) return 0;
|
|
int chained = (inner->kind == N_DOT);
|
|
if (inner->kind != N_IDENT && !chained) return 0;
|
|
Type *bt = inner->type;
|
|
/* #128b: module-qualified `mod.arr` where arr is an imported
|
|
* top-level `let X: [N]T`. The checker leaves SK_USE module-idents
|
|
* with NULL/ty_err type; detect via let_islet + let_var_type-of-
|
|
* TY_ARRAY and emit LEAQ X(SB) for the array's base address.
|
|
* Without this, the N_INDEX fallback at cgen.c:~6760 falls to
|
|
* cgexpr(base) which auto-MOVQs the symbol contents as if it
|
|
* were a pointer-var (= load 8 bytes of the array's first
|
|
* elements + treat as junk address) — segfault-class miscompile. */
|
|
if (bt == NULL || bt == ty_err) {
|
|
if (let_islet(base->str)) {
|
|
Type *lt = let_var_type(base->str);
|
|
Type *lu = type_chase_named(lt);
|
|
if (lu && lu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, masym(c, base->str),
|
|
areg(dst_reg));
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
Type *bu = type_chase_named(bt);
|
|
if (bu == NULL) return 0;
|
|
int viaptr = 0;
|
|
Type *struct_t = NULL;
|
|
if (bu->kind == TY_PTR) {
|
|
Type *st = type_chase_named(bu->sub);
|
|
if (st && st->kind == TY_STRUCT) { struct_t = st; viaptr = 1; }
|
|
} else if (bu->kind == TY_STRUCT) {
|
|
struct_t = bu;
|
|
}
|
|
if (struct_t == NULL) return 0;
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = struct_t->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, base->str) == 0) { f = fl; break; }
|
|
if (f == NULL) return 0;
|
|
/* Only fire on `[N]T` fields — the field's storage IS the array
|
|
* data inline, so taking the address-of-field gives `&arr[0]`.
|
|
* For `*T` / `[]T` / `str` fields, the existing cgexpr(base) path
|
|
* is correct (loads the pointer value, then adds the scaled
|
|
* index); over-firing here would skip the deref and treat the
|
|
* pointer/slice/str field as an inline array. */
|
|
Type *ft = type_chase_named(f->type);
|
|
if (ft == NULL || ft->kind != TY_ARRAY) return 0;
|
|
int foff = (int)f->offset;
|
|
/* #253: chained inner — compute the container base via the dot-chain
|
|
* spine (pointer VALUE of inner when viaptr, else its ADDRESS), then
|
|
* add the field offset. cg_dotchain_addr keeps the spill contract. */
|
|
if (chained) {
|
|
if (!cg_dotchain_addr(c, inner, dst_reg, locals)) return 0;
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg));
|
|
if (foff != 0)
|
|
ins2(c, A_ADDQ, aimm(foff), areg(dst_reg));
|
|
return 1;
|
|
}
|
|
int inner_off = localfind(locals, inner->str);
|
|
/* #249 (sibling of #135): a module-GLOBAL struct value base. localfind
|
|
* returns 0 for a global, so the BP-rel form below would emit `LEAQ
|
|
* (BP)` (read the stack frame, not the global). Resolve the same way
|
|
* the scalar N_DOT global-field read does (cgen.c:7532) — LEAQ
|
|
* name(SB) + field offset. const globals are def_isstructdef. */
|
|
if (viaptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, inner_off), areg(dst_reg));
|
|
if (foff != 0)
|
|
ins2(c, A_ADDQ, aimm(foff), areg(dst_reg));
|
|
} else if (inner_off == 0 && (let_islet(inner->str)
|
|
|| def_isstructdef(inner->str))) {
|
|
ins2(c, A_LEAQ, masym(c, inner->str), areg(dst_reg));
|
|
if (foff != 0)
|
|
ins2(c, A_ADDQ, aimm(foff), areg(dst_reg));
|
|
} else {
|
|
ins2(c, A_LEAQ, amem(D_BP, inner_off + foff),
|
|
areg(dst_reg));
|
|
}
|
|
return 1;
|
|
}
|
|
/* aggarg_srcaddr — land the ADDRESS of an addressable aggregate arg
|
|
* source in `dst`, reusing the closed #265/#268 let-init-copy dispatch:
|
|
* ident/global slot (LEAQ), deref operand (cgexpr of the pointer),
|
|
* N_DOT field (cg_dotchain_addr, #253), N_INDEX element (the &base[i]
|
|
* spine, #252/#270). Returns 0 for a source kind not covered (caller
|
|
* loud-stops, rule 7). The CALL source is handled separately at the
|
|
* push site (receive-to-regs / sret-to-scratch). */
|
|
static int
|
|
aggarg_srcaddr(Cg *c, Node *src, int dst, Local *locals)
|
|
{
|
|
if (src->kind == N_UN && src->op == TK_STAR) {
|
|
cgexpr(c, src->lhs, locals);
|
|
if (dst != D_AX)
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(dst));
|
|
return 1;
|
|
}
|
|
if (src->kind == N_IDENT) {
|
|
int soff = localfind(locals, src->str);
|
|
if (soff != 0) {
|
|
ins2(c, A_LEAQ, amem(D_BP, soff), areg(dst));
|
|
return 1;
|
|
}
|
|
/* global value source. Gated to a module-`let` (let_islet,
|
|
* the wwstage letvartnode twin); a const array/struct `def`
|
|
* aggregate ARG is untested and out of scope (#274) — both
|
|
* stages fall through to the caller's loud-stop, aligned DOWN
|
|
* to the leaner wwstage per rule-10. */
|
|
if (let_islet(src->str)) {
|
|
ins2(c, A_LEAQ, masym(c, src->str), areg(dst));
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
if (src->kind == N_DOT)
|
|
return cg_dotchain_addr(c, src, dst, locals);
|
|
if (src->kind == N_INDEX) {
|
|
Node *base = src->lhs;
|
|
Node *idx = src->rhs;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
if (base && base->kind == N_IDENT && bu
|
|
&& bu->kind == TY_ARRAY) {
|
|
int esz = (bu->sub) ? (int)bu->sub->size : 1;
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
int boff = localfind(locals, base->str);
|
|
if (boff != 0)
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ, masym(c, base->str),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
|
|
if (dst != D_AX)
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(dst));
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* cg_structlit_fill modes — see helper docstring. */
|
|
enum {
|
|
DST_BP = 0,
|
|
DST_PTR_LOCAL = 1,
|
|
DST_GLOBAL = 2,
|
|
};
|
|
static void cg_structlit_fill(Cg*, Local**, Type*, Node*, int, int, const char*, int);
|
|
static void cg_structlit_fill_bp(Cg*, Local**, Type*, Node*, int);
|
|
|
|
static void
|
|
cgexpr_int(Cg *c, long long v)
|
|
{
|
|
ins2(c, A_MOVQ, aimm(v), areg(D_AX));
|
|
}
|
|
|
|
/* Materialise a float constant in X0: MOVQ the IEEE bits into AX, PUSH,
|
|
* MOVSD off the stack into X0. Shared by N_FLOATLIT and the f64/f32-typed
|
|
* N_INTLIT arm (#103 FACE X): a no-decimal `0f64`/`8f64` is an N_INTLIT
|
|
* carrying float TYPE, so it must reach X0 like a true float literal does
|
|
* — the integer-immediate path left the value stranded in AX, so an SSE
|
|
* compare/mul read a stale X0. */
|
|
static void
|
|
cgexpr_float(Cg *c, double val)
|
|
{
|
|
union { double d; u64 u; } x;
|
|
x.d = val;
|
|
ins2(c, A_MOVQ, aimm((long long)x.u), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVSD, amem(D_SP, 0), areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
}
|
|
|
|
/* cg_widen_tag_remap — when widening from one tagged union to another,
|
|
* rewrite the source's variant tag at BP+slot_off+0 to use the dst
|
|
* union's variant indices. No-op when src and dst index orders coincide.
|
|
*
|
|
* Mirrors Hare's tagged-subset assignment: a value of type (A|B) flows
|
|
* into (A|B|C) by re-tagging the discriminator to the position the
|
|
* variant occupies in the wider union. Both must already match by
|
|
* cg_variant_match — the checker enforces that.
|
|
*
|
|
* Emits a CMPQ-chain switch over the source tag because w6a has no
|
|
* CMOVQ encoding. The chain is linear in nvariants; in practice tagged
|
|
* unions are small. */
|
|
static void
|
|
cg_widen_tag_remap(Cg *c, Type *du, Type *su, int slot_off)
|
|
{
|
|
if (du == NULL || du->kind != TY_TAGGED) return;
|
|
if (su == NULL || su->kind != TY_TAGGED) return;
|
|
int identity = 1, idx = 0;
|
|
for (Tparam *p = su->params; p; p = p->next, idx++) {
|
|
int di = cg_tag_for_variant(du, p->type);
|
|
if (di < 0) di = 0;
|
|
if (di != idx) { identity = 0; break; }
|
|
}
|
|
if (identity) return;
|
|
const char *done = mklabel(c, "remap_done");
|
|
ins2(c, A_MOVQ, amem(D_BP, slot_off + 0), areg(D_AX));
|
|
idx = 0;
|
|
for (Tparam *p = su->params; p; p = p->next, idx++) {
|
|
const char *next = mklabel(c, "remap_next");
|
|
int di = cg_tag_for_variant(du, p->type);
|
|
if (di < 0) di = 0;
|
|
ins2(c, A_CMPQ, aimm(idx), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(next));
|
|
ins2(c, A_MOVQ, aimm(di), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0));
|
|
ins1(c, A_JMP, abranch(done));
|
|
label(c, next);
|
|
}
|
|
label(c, done);
|
|
}
|
|
|
|
/* cg_widen_tagged_store — write the tagged-union slot bytes for `src`
|
|
* into base_reg+slot_off, sized to `sz` (8 for nullable fold, else
|
|
* 16/24+). Used by call-site widening (via cg_widen_tagged_push) and
|
|
* by the let/assign/return/struct-field-init paths.
|
|
*
|
|
* base_reg picks the addressing root for every write:
|
|
* - D_BP: function-frame slot. The original layout — callers pass
|
|
* a BP-relative slot_off and the function writes directly.
|
|
* - else (e.g. D_BX for a *struct field, D_CX for a top-level
|
|
* struct field): pointer-rooted dst. cgexpr inside this function
|
|
* trashes every GPR, so we can't carry base_reg across — instead
|
|
* we route every write through a fresh BP-rooted scratch slot,
|
|
* reload base_reg from a temp spill at the end, and word-copy
|
|
* scratch → (base_reg, slot_off). Caller is responsible for
|
|
* loading base_reg with the dst address before the call; the
|
|
* function preserves it across cgexpr via the spill.
|
|
*
|
|
* Branches by source shape (tagged_arg_size > 0 source counts as a
|
|
* tagged subset — possibly with different variant indices):
|
|
* - nullable: dst is folded (*T|void); store pointer at +0.
|
|
* - tagged ident: byte-copy slot words then remap tag at +0.
|
|
* - tagged expression: cgexpr leaves AX=tag, DX=val0, [CX=val1] —
|
|
* spill into slot then remap.
|
|
* - struct ident: zero-fill, byte-copy struct words to +8.
|
|
* - struct literal: zero-fill, store each field at slot+8+field_off.
|
|
* - str: cgexpr leaves AX=ptr, BX=len.
|
|
* - scalar: cgexpr leaves AX; store at +8 with zero pad. */
|
|
static void
|
|
cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
|
|
int base_reg, int slot_off, int sz)
|
|
{
|
|
/* For pointer-rooted dst, materialise into a BP-rooted scratch
|
|
* slot — body writes via `amem(D_BP, write_off + k)` — then copy
|
|
* out. Spill base_reg first so cgexpr can clobber freely. */
|
|
int via_outer = (base_reg != D_BP);
|
|
int base_spill = 0;
|
|
int write_off = slot_off;
|
|
if (via_outer) {
|
|
if (cg_tagbase != 0) {
|
|
base_spill = cg_tagbase;
|
|
} else {
|
|
base_spill = local_alloc(c, locals_p, "@tagbase", 8,
|
|
cg_frame);
|
|
cg_tagbase = base_spill;
|
|
cg_tagbase_sz = 8;
|
|
}
|
|
ins2(c, A_MOVQ, areg(base_reg), amem(D_BP, base_spill));
|
|
write_off = cg_tagscr_slot(c, locals_p, sz);
|
|
/* Pre-zero so str/scalar branches (which leave high words
|
|
* untouched when sz exceeds the variant's footprint) still
|
|
* deliver a clean slot to the copy-out. */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst;
|
|
if (du == NULL || du->kind != TY_TAGGED) return;
|
|
if (du->nullable) {
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* `expr: TaggedAlias` where the cast's destination IS the union
|
|
* itself is a widening, not a re-interpret. cgexpr on the cast
|
|
* leaves the inner expression's register shape (str: AX=ptr,
|
|
* BX=len), not the tagged AX/DX/CX triple — so route through the
|
|
* concrete-variant branches below by peeling the cast. Casts to
|
|
* a concrete variant (`7: i32`) keep their type for proper tag
|
|
* lookup and fall through to the matching branch. */
|
|
if (src && src->kind == N_CAST && src->lhs) {
|
|
Type *castt = src->type;
|
|
Type *castu = (castt && castt->kind == TY_NAMED)
|
|
? castt->under : castt;
|
|
Type *innert = src->lhs->type;
|
|
Type *innu = (innert && innert->kind == TY_NAMED)
|
|
? innert->under : innert;
|
|
int cast_is_widen = (castu == du) ||
|
|
(castu && castu->kind == TY_TAGGED && type_eq(castt, dst));
|
|
int inner_is_tagged = innu && innu->kind == TY_TAGGED;
|
|
if (cast_is_widen && !inner_is_tagged) {
|
|
src = src->lhs;
|
|
}
|
|
}
|
|
Type *st = src ? src->type : NULL;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
/* Tagged → tagged subset: copy slot words then tag-remap. */
|
|
if (su && su->kind == TY_TAGGED) {
|
|
int ssz = (int)su->size;
|
|
/* #218: is the source itself a single NESTED variant of du
|
|
* (its whole tagged type matches one du variant), rather than
|
|
* a flattened SUBSET whose members spread into du? If so, the
|
|
* inner tagged value is the payload: store it at slot+8 with
|
|
* the outer tag at slot+0, exactly like the scalar/struct/str
|
|
* single-variant arms below — NOT a copy-to-+0 + sub-variant
|
|
* remap. cg_tag_for_variant's structural fallback (cgen.c
|
|
* cg_variant_match) is what recovers the index after the
|
|
* nominal-lossy collapse. */
|
|
int nested = cg_tag_for_variant(du, st);
|
|
if (nested >= 0) {
|
|
/* drew collision guard: the structural fallback over-
|
|
* matches if ≥2 nominally-distinct du variants share the
|
|
* source's shape. Unreachable under today's nominal-lossy
|
|
* model, but INVERTS when #199b/B-full lands the nominal
|
|
* layer — hard-error NOW so a future collision STOPS the
|
|
* compiler instead of silently mis-tagging. */
|
|
int nmatch = 0;
|
|
for (Tparam *p = du->params; p; p = p->next)
|
|
if (cg_variant_struct_match(p->type, st))
|
|
nmatch++;
|
|
if (nmatch >= 2)
|
|
fatal("cg_widen_tagged_store: structural fallback "
|
|
"cannot disambiguate nominally-distinct same-"
|
|
"shape variants without nominal layout "
|
|
"(#218/#199b/B-full)");
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
if (src->kind == N_IDENT) {
|
|
int soff = localfind(*locals_p, src->str);
|
|
for (int k = 0; k < ssz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + k));
|
|
}
|
|
} else {
|
|
/* #38b: an sret-classified call result is in
|
|
* memory (AX = dest pointer), not the cursor —
|
|
* the spill below would store the pointer as
|
|
* the payload. Mem-to-mem widen is #40. */
|
|
if (src->kind == N_CALL
|
|
&& cg_sret_retsize(st) > 0)
|
|
fatal("#40: sret-class call result "
|
|
"cannot be cursor-widened into a "
|
|
"tagged slot (mem-to-mem widen "
|
|
"unwired)");
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, write_off + 16));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, write_off + 24));
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, write_off + 32));
|
|
}
|
|
ins2(c, A_MOVQ, aimm(nested),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
if (src->kind == N_IDENT) {
|
|
int soff = localfind(*locals_p, src->str);
|
|
for (int k = 0; k < ssz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
} else {
|
|
/* Tagged source returned via the tagged-return ABI
|
|
* (AX=tag, DX=word0, CX=word1, R8=word2). The unused
|
|
* ABI words are zeroed by the producer (#18 cgreturn
|
|
* variant-widen) so the unconditional store here is
|
|
* safe even when the source variant has fewer payload
|
|
* words than the dst slot. */
|
|
/* #38b: an sret-classified call result is in memory
|
|
* (AX = dest pointer), not the cursor. #40. */
|
|
if (src->kind == N_CALL && cg_sret_retsize(st) > 0)
|
|
fatal("#40: sret-class call result cannot be "
|
|
"cursor-widened into a tagged slot "
|
|
"(mem-to-mem widen unwired)");
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 0));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, write_off + 8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, write_off + 16));
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, write_off + 24));
|
|
}
|
|
if (ssz < sz) {
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = ssz; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
cg_widen_tag_remap(c, du, su, write_off);
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* #242: tuple payload. Each element rides ONE register-ABI
|
|
* eightbyte — scalar/float a single 8B word, a slice/str its 3-word
|
|
* {ptr,len,cap} header (24B) — matching the tagged-return load
|
|
* (AX=tag, DX=word0, CX=word1, R8=word2) and the cgmlet receive
|
|
* cursor. NOT the packed-by-size t.N field layout (#238). Mirror of
|
|
* the struct-literal field-flow below, but 8B-slotted, not field-
|
|
* offset. */
|
|
if (su && su->kind == TY_TUPLE && src->kind == N_TUPLE) {
|
|
int tag = cg_tag_for_variant(du, st);
|
|
/* #242: a tuple built from UNTYPED/literal elements (`(true,7)`)
|
|
* leaves the src tuple type un-matchable by type_eq, so the
|
|
* variant tag can't resolve — the supported shape is a tuple of
|
|
* TYPED expressions (the strconv parseint `(neg, n)` shape).
|
|
* Loud-stop rather than silently mis-tag (tag 0) — rule 7.
|
|
* Untyped tuple-element coercion is the #241 literal-init
|
|
* family. */
|
|
if (tag < 0)
|
|
fatal("cg_widen_tagged_store: tuple-in-union variant tag "
|
|
"unresolved (untyped/literal tuple element; "
|
|
"see #242 / #241)");
|
|
/* #242: this 8B-per-eightbyte packing is correct only when no
|
|
* two scalar elements share a SysV eightbyte — e.g. (bool,u64),
|
|
* where the sub-8 bool is padded out by u64's 8-alignment. A
|
|
* tuple whose natural aligned layout packs two narrows into one
|
|
* eightbyte (e.g. (i32,i32,u64)) would overflow the union
|
|
* payload the slotted write assumes. Loud-stop (rule 7); the
|
|
* SysV eightbyte tuple classification is a deferred follow-up. */
|
|
int total = 0;
|
|
for (Node *e = src->list; e; e = e->next)
|
|
total += (node_isstr(e) || node_isslice(e)) ? 24 : 8;
|
|
if (8 + total > sz)
|
|
fatal("cg_widen_tagged_store: tuple-in-union payload needs "
|
|
"SysV eightbyte packing (narrow elements share an "
|
|
"eightbyte; see #242 follow-up)");
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
int foff = 0;
|
|
for (Node *e = src->list; e; e = e->next) {
|
|
int e_isf32 = 0;
|
|
int isflt = fld_isfloat(e->type, &e_isf32);
|
|
int wide = node_isstr(e) || node_isslice(e);
|
|
int esz = e->type ? (int)e->type->size : 8;
|
|
cgexpr(c, e, *locals_p);
|
|
if (isflt) {
|
|
ins2(c, e_isf32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0),
|
|
amem(D_BP, write_off + 8 + foff));
|
|
} else if (wide) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, write_off + 8 + foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, write_off + 8 + foff + 16));
|
|
} else {
|
|
ins2(c, fldstoreop(e->type, esz),
|
|
areg(D_AX),
|
|
amem(D_BP, write_off + 8 + foff));
|
|
}
|
|
foff += wide ? 24 : 8;
|
|
}
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* Struct payload: zero the whole slot, then write fields/words
|
|
* at slot+8+ — keeping the tag word at slot+0 from the zero-fill,
|
|
* then patch it with the variant tag. */
|
|
if (su && su->kind == TY_STRUCT) {
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
int tag = cg_tag_for_variant(du, st);
|
|
if (src->kind == N_IDENT) {
|
|
int soff = localfind(*locals_p, src->str);
|
|
int ssz = (int)su->size;
|
|
int k = 0;
|
|
while (k + 8 <= ssz) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + k));
|
|
k += 8;
|
|
}
|
|
if (k < ssz) {
|
|
/* Tail word: load with the right width to
|
|
* avoid stepping past the source slot. The
|
|
* zero-fill above means trailing slop is
|
|
* already clean. */
|
|
int tail = ssz - k;
|
|
int lop = (tail == 4) ? A_MOVL :
|
|
(tail == 1 ? A_MOVB : A_MOVQ);
|
|
ins2(c, lop,
|
|
amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, lop, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + k));
|
|
}
|
|
} else if (src->kind == N_STRUCTLIT) {
|
|
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;
|
|
/* str IS []u8 and a slice is the same 3-word
|
|
* {ptr,len,cap} header from cgexpr's AX/BX/CX
|
|
* (#1/Phase 3). The slice arm rides #38b's
|
|
* regex-shaped consumer (slice fields inside a
|
|
* union-payload struct literal); the prior
|
|
* str-only gate dropped .len/.cap via the
|
|
* scalar store below — the #24 gap's widener
|
|
* twin. */
|
|
if (fu && (fu->kind == TY_STR
|
|
|| fu->kind == TY_SLICE)) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, write_off + 8 + (int)foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, write_off + 8 + (int)foff + 16));
|
|
continue;
|
|
}
|
|
int op = A_MOVQ;
|
|
if (fsz == 1) op = A_MOVB;
|
|
else if (fsz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + (int)foff));
|
|
}
|
|
}
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* str IS []u8 — same 32B payload as a slice: cgexpr leaves
|
|
* (AX=ptr, BX=len, CX=cap); slot layout tag@+0, ptr@+8, len@+16,
|
|
* cap@+24, destination slot >= 32B. str folds onto the slice arm
|
|
* (#1/Phase 3 collapse). */
|
|
if (type_isslice(st) || (su && su->kind == TY_SLICE) ||
|
|
type_isstr(st) || (su && su->kind == TY_STR)) {
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 16));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, write_off + 24));
|
|
int tag = cg_tag_for_variant(du, st);
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* Float arm: cgexpr on an f64/f32 source leaves the bit pattern in
|
|
* X0 only — the AX-store below would silently write whatever was
|
|
* loaded into AX before the SSE conversion. Literal `1.0` works by
|
|
* coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX
|
|
* before MOVSD'ing into X0); every runtime f64 shape (cast, call,
|
|
* unary, ident, struct-field load) needs the explicit MOVSD path.
|
|
* Same kind-specific dispatch as the str/slice branches above and
|
|
* the structlit field-flow at the top of this function. */
|
|
int wid_isf32 = 0;
|
|
if (fld_isfloat(st, &wid_isf32)) {
|
|
int mov = wid_isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, mov, areg(D_X0), amem(D_BP, write_off + 8));
|
|
/* #227: zero the pad words (+16..sz) so a >16B union slot
|
|
* carries the dst's full payload width, not just the 1-word
|
|
* float value. The BP/let/assign/return-scratch path never
|
|
* pre-zeroes, so a passthrough return or a *u8 reinterpret of
|
|
* the narrow-tagged value otherwise reads stack garbage at
|
|
* slot+16/+24. Mirrors the tagged-subset tail-zero; symmetric
|
|
* with wwstage cgwidentaggedstorebp. */
|
|
if (sz > 16) {
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 16; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
int tag = cg_tag_for_variant(du, st);
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* Scalar / pointer / etc. #227: zero the pad words (+16..sz) — see
|
|
* the float arm above. The old code left the pad uninitialised on
|
|
* the BP path (relying on cg_widen_tagged_push's pre-zero), but
|
|
* let/assign/return-scratch never pre-zeroes, so a passthrough
|
|
* return / *u8 reinterpret of the narrow-tagged value read stack
|
|
* garbage in slot+16/+24. */
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
|
|
if (sz > 16) {
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 16; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
int tag = cg_tag_for_variant(du, st);
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, write_off + 0));
|
|
copy_out:
|
|
if (via_outer) {
|
|
/* cgexpr above clobbered base_reg — reload from spill, then
|
|
* word-copy scratch → caller's (base_reg, slot_off). */
|
|
ins2(c, A_MOVQ, amem(D_BP, base_spill), areg(base_reg));
|
|
for (int k = 0; k < sz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, write_off + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(base_reg, slot_off + k));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* cg_widen_tagged_push — call-site widening. For shapes where cgexpr
|
|
* leaves the value directly in registers (str: AX=ptr, BX=len; slice:
|
|
* AX=ptr, BX=len, CX=cap; scalar: AX), push from registers without a
|
|
* scratch slot. Struct payload and tagged-subset re-layout still
|
|
* route through a scratch slot. The direct-push form keeps wwstage's
|
|
* asm byte-identical to cstage on the byteindex / index family. */
|
|
static void
|
|
cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
|
|
{
|
|
Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst;
|
|
if (du && du->nullable) {
|
|
/* Single 8B slot: just push the pointer/null. */
|
|
cgexpr(c, src, *locals_p);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
return;
|
|
}
|
|
Type *st = src ? src->type : NULL;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int src_is_struct = su && su->kind == TY_STRUCT;
|
|
int src_is_tagged = su && su->kind == TY_TAGGED;
|
|
if (!src_is_struct && !src_is_tagged) {
|
|
/* Direct-push fast path: str / slice / scalar / pointer. */
|
|
cgexpr(c, src, *locals_p);
|
|
int tag = cg_tag_for_variant(du, st);
|
|
if (tag < 0) tag = 0;
|
|
if (type_isstr(st) || (su && su->kind == TY_STR)) {
|
|
/* str IS []u8: slot 32 [+0]=tag, [+8]=ptr, [+16]=len,
|
|
* [+24]=cap — same shape as the slice arm below. Push
|
|
* cap, len, ptr, tag (high→low so pop drains tag first)
|
|
* (#1/Phase 3). */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
|
|
return;
|
|
}
|
|
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
|
|
/* slot 32: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap. */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
|
|
return;
|
|
}
|
|
/* Scalar / pointer variant. Pad with zero high words when
|
|
* the slot has room for a wider variant. */
|
|
int nwords = sz / 8;
|
|
for (int k = nwords - 1; k >= 2; k--) {
|
|
ins2(c, A_XORQ, areg(D_DX), areg(D_DX));
|
|
ins1(c, A_PUSHQ, areg(D_DX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */
|
|
return;
|
|
}
|
|
int scr = cg_tagscr_slot(c, locals_p, sz);
|
|
/* Zero the scratch slot first so any pad word the store path
|
|
* leaves untouched (struct payload shorter than the slot's value
|
|
* area) reads as 0 on the callee. The store path then writes the
|
|
* variant bytes over the zeros. */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k));
|
|
cg_widen_tagged_store(c, locals_p, dst, src, D_BP, scr, sz);
|
|
int nwords = sz / 8;
|
|
for (int k = nwords - 1; k >= 0; k--) {
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + k * 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
}
|
|
|
|
/* cg_structlit_fill — fill a struct-typed slot from an N_STRUCTLIT
|
|
* value into one of three destination flavors. Used by N_LET, N_ASSIGN
|
|
* N_IDENT-lhs, N_RETURN N_STRUCTLIT (BP-rel), and N_ASSIGN N_DOT-lhs
|
|
* (BP-rel / via *struct local / via struct global) at single-dot and
|
|
* chained-dot sites.
|
|
*
|
|
* Destination modes:
|
|
* DST_BP — base = BP, no reload. Stores at disp+i(BP).
|
|
* srcoff/name unused.
|
|
* DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before the
|
|
* ELLIPSIS zero-fill loop and before EVERY field
|
|
* store (cgexpr clobbers BX between fields).
|
|
* Stores at disp+i(BX). name unused.
|
|
* DST_GLOBAL — base = BX, reloaded via `LEAQ name(SB), BX` with
|
|
* the same reload cadence as DST_PTR_LOCAL.
|
|
* srcoff unused.
|
|
*
|
|
* Param semantics (locked in here so the recursion contract is clear):
|
|
* - `disp` is the per-recursion accumulator — grows by `foff` as
|
|
* we descend into a nested struct-typed structlit field.
|
|
* - `srcoff` (DST_PTR_LOCAL) and `name` (DST_GLOBAL) are *constant*
|
|
* across the whole call tree — they identify the root dst, which
|
|
* doesn't change with depth. Recursion passes them through.
|
|
*
|
|
* Why a helper? The inline field-walk at each call site previously
|
|
* did `cgexpr(f->lhs); store AX (sized)`. For struct-typed fields
|
|
* whose value is itself a nested N_STRUCTLIT, cgexpr has no whole-
|
|
* struct-in-register convention — it lands AX = first qword and the
|
|
* trailing bytes silently stay zero (or stack garbage). #17 fixed
|
|
* the BP-rel sites; #18 extends the same recursion to the four
|
|
* N_ASSIGN N_DOT-lhs structlit walks (single-dot via_ptr/global/
|
|
* local + chained depth>=2).
|
|
*
|
|
* The non-BP modes emit a redundant BX reload at the start of each
|
|
* recursive nested zero-fill / each recursive scalar store — this is
|
|
* correctness-by-construction (BX is always freshly loaded right
|
|
* before use), and the redundancy only fires on the nested-STRUCTLIT
|
|
* shapes that didn't compile before. Byte-identity for the no-nested
|
|
* case (the only shape selfhost source uses today) is preserved
|
|
* because the existing inline code's reload-before-each-store pattern
|
|
* matches the helper's per-store reload exactly.
|
|
*
|
|
* The scalar store dispatch stays at the explicit {1->MOVB, 4->MOVL,
|
|
* else MOVQ} shape (not fieldstoreop, which emits MOVW for fsz==2) to
|
|
* stay byte-identical with cstage pending task #13. */
|
|
static void
|
|
cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit,
|
|
int mode, int srcoff, const char *name, int disp)
|
|
{
|
|
int sz = (int)lu->size;
|
|
int base_reg = (mode == DST_BP) ? D_BP : D_BX;
|
|
if (lit->op == TK_ELLIPSIS) {
|
|
/* `..., ...` autofill — zero the entire slot first so
|
|
* unmentioned fields read as 0. Sized stores: 8/4/1. For
|
|
* non-BP modes, reload BX once before the loop (cgexpr-free
|
|
* region between iterations, so one reload is enough). */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff), areg(D_BX));
|
|
else if (mode == DST_GLOBAL)
|
|
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
|
|
int zi = 0;
|
|
while (zi + 8 <= sz) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(base_reg, disp + zi));
|
|
zi += 8;
|
|
}
|
|
while (zi + 4 <= sz) {
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(base_reg, disp + zi));
|
|
zi += 4;
|
|
}
|
|
while (zi < sz) {
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(base_reg, disp + zi));
|
|
zi += 1;
|
|
}
|
|
}
|
|
for (Node *f = lit->list; f; f = f->next) {
|
|
u64 foff = 0;
|
|
int fsz = 8;
|
|
Type *ft = NULL;
|
|
for (Tfield *fl = lu->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, f->str) == 0) {
|
|
foff = fl->offset;
|
|
fsz = (int)(fl->type ? fl->type->size : 8);
|
|
ft = fl->type;
|
|
break;
|
|
}
|
|
}
|
|
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). A slice is the same 24B
|
|
* {ptr,len,cap} shape, so it rides this arm; without it the
|
|
* generic scalar tail stored only the ptr word (#24). */
|
|
if (fu && (fu->kind == TY_STR || fu->kind == TY_SLICE)) {
|
|
cgexpr(c, f->lhs, *locals_p);
|
|
if (mode == DST_BP) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, disp + (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, disp + (int)foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, disp + (int)foff + 16));
|
|
} else {
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff),
|
|
areg(D_DX));
|
|
else
|
|
ins2(c, A_LEAQ, masym(c, name),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_DX, disp + (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_DX, disp + (int)foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_DX, disp + (int)foff + 16));
|
|
}
|
|
continue;
|
|
}
|
|
/* #249: array-typed field initialised from an N_ARRLIT. No prior
|
|
* arm matched, so without this the generic scalar tail below
|
|
* would cgexpr the N_ARRLIT (→ AX≈0) and store one sized word,
|
|
* silently DROPPING every element. Store element-wise at
|
|
* disp+foff+i*esz, reusing the N_LET array-init shape (cgen.c:
|
|
* 8467) for int/float elements and its `...` repeat. For non-BP
|
|
* modes cgexpr clobbers BX, so reload the base before each store
|
|
* (the X0/AX value reg survives the reload). str/slice/struct/
|
|
* tagged ELEMENT arrays are the N_LET path's documented multi-
|
|
* word gap (cgen.c:8462) — loud rule-7 error, not a silent drop. */
|
|
if (fu && fu->kind == TY_ARRAY
|
|
&& f->lhs && f->lhs->kind == N_ARRLIT) {
|
|
Type *esub = fu->sub;
|
|
Type *esubu = (esub && esub->kind == TY_NAMED)
|
|
? esub->under : esub;
|
|
int esz = esub ? (int)esub->size : 1;
|
|
int al_isf32 = 0;
|
|
int is_float_el = fld_isfloat(esub, &al_isf32);
|
|
if (type_isstr(esub) || type_isslice(esub)
|
|
|| (esubu && (esubu->kind == TY_STRUCT
|
|
|| esubu->kind == TY_TAGGED)))
|
|
fatal("cg_structlit_fill: array field '%s' has a "
|
|
"str/slice/struct/tagged element — multi-word "
|
|
"element store is out of #249 scope (N_LET "
|
|
"array-init gap, cgen.c:8462)",
|
|
f->str ? f->str : "?");
|
|
int eop = A_MOVQ;
|
|
if (esz == 1) eop = A_MOVB;
|
|
else if (esz == 2) eop = A_MOVW;
|
|
else if (esz == 4) eop = A_MOVL;
|
|
int fmov = al_isf32 ? A_MOVSS : A_MOVSD;
|
|
int idx = 0;
|
|
Node *last = NULL;
|
|
int repeat = 0;
|
|
for (Node *e = f->lhs->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
cgexpr(c, e, *locals_p);
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff),
|
|
areg(D_BX));
|
|
else if (mode == DST_GLOBAL)
|
|
ins2(c, A_LEAQ, masym(c, name),
|
|
areg(D_BX));
|
|
int eoff = disp + (int)foff + idx * esz;
|
|
if (is_float_el)
|
|
ins2(c, fmov, areg(D_X0),
|
|
amem(base_reg, eoff));
|
|
else
|
|
ins2(c, eop, areg(D_AX),
|
|
amem(base_reg, eoff));
|
|
last = e;
|
|
idx++;
|
|
}
|
|
if (repeat && last) {
|
|
while (idx < (int)fu->alen) {
|
|
if (mode == DST_PTR_LOCAL)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, srcoff),
|
|
areg(D_BX));
|
|
else if (mode == DST_GLOBAL)
|
|
ins2(c, A_LEAQ, masym(c, name),
|
|
areg(D_BX));
|
|
int eoff = disp + (int)foff + idx * esz;
|
|
if (is_float_el)
|
|
ins2(c, fmov, areg(D_X0),
|
|
amem(base_reg, eoff));
|
|
else
|
|
ins2(c, eop, areg(D_AX),
|
|
amem(base_reg, eoff));
|
|
idx++;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
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);
|
|
}
|
|
|
|
/* cg_tuple_lit_to_cursor — #241: materialise an N_TUPLE literal's elements
|
|
* into the SysV register-return cursor — integer words L→R over tuple_rseq
|
|
* (AX,DX,CX,R8), floats over tuple_sse_seq (X0,X1), a slice/str's
|
|
* {ptr,len,cap} header over three consecutive INTEGER regs — the SAME ABI a
|
|
* tuple-returning CALL leaves, which every tuple consumer (tuple_store at
|
|
* the N_LET/N_MLET sites) already reads. cgexpr otherwise can't make a tuple
|
|
* value (the default arm zeroed AX), so a literal/yield rvalue tuple bound
|
|
* or destructured read garbage past word0. Each element's cgexpr clobbers
|
|
* AX/X0, so integer words spill L→R and pop into the cursor reversed, floats
|
|
* spill to @tupfscr and reload by SSE index — INDEPENDENT counters (ref/qbe/
|
|
* amd64/sysv.c retr). Byte-identical extraction of cgreturn's N_TUPLE arm,
|
|
* now shared with cgexpr. Over-cap loud-stops (rule 7); a bare expression
|
|
* value can't sret, so the >cap rvalue-tuple materialisation is the #10
|
|
* follow-up. */
|
|
static void
|
|
cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple)
|
|
{
|
|
int f32;
|
|
int gptotal = 0, ssecount = 0;
|
|
for (Node *e = tuple->list; e; e = e->next) {
|
|
if (fld_isfloat(e->type, &f32))
|
|
ssecount++;
|
|
else
|
|
gptotal += tuple_ebytes(node_isstr(e)
|
|
|| node_isslice(e));
|
|
}
|
|
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
|
|
fatal("tuple literal exceeds register-return ABI capacity "
|
|
"(integer %d/%d, SSE %d/%d); over-cap rvalue-tuple "
|
|
"materialisation is the #10 sret follow-up",
|
|
gptotal, TUPLE_GPCAP, ssecount, TUPLE_SSECAP);
|
|
int fscr = 0;
|
|
if (ssecount > 0) {
|
|
if (cg_tupfscr != 0)
|
|
fscr = cg_tupfscr;
|
|
else {
|
|
fscr = local_alloc(c, locals, "@tupfscr",
|
|
TUPLE_SSECAP * 8, cg_frame);
|
|
cg_tupfscr = fscr;
|
|
}
|
|
}
|
|
int sseidx = 0;
|
|
for (Node *e = tuple->list; e; e = e->next) {
|
|
int isflt = fld_isfloat(e->type, &f32);
|
|
cgexpr(c, e, *locals);
|
|
if (isflt) {
|
|
ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0),
|
|
amem(D_BP, fscr + sseidx * 8));
|
|
sseidx++;
|
|
continue;
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (node_isstr(e) || node_isslice(e)) {
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_CX));
|
|
}
|
|
}
|
|
for (int i = gptotal - 1; i >= 0; i--)
|
|
ins1(c, A_POPQ, areg(tuple_rseq[i]));
|
|
int j = 0;
|
|
for (Node *e = tuple->list; e; e = e->next) {
|
|
if (!fld_isfloat(e->type, &f32))
|
|
continue;
|
|
ins2(c, f32 ? A_MOVSS : A_MOVSD,
|
|
amem(D_BP, fscr + j * 8),
|
|
areg(tuple_sse_seq[j]));
|
|
j++;
|
|
}
|
|
}
|
|
|
|
/* cg_tuple_slot_to_cursor — #241: load a tuple already materialised in a
|
|
* BP-relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound
|
|
* union payload) into the SAME register-return cursor. The slot uses the
|
|
* register-ABI stride the tuple-init / #242 destructure write (a scalar 8B,
|
|
* a slice/str its 3-word header), NOT the packed t.N field layout (#238).
|
|
* All sources are memory, so each word loads straight into its cursor reg —
|
|
* no spill dance (unlike the literal arm whose element cgexpr clobbers). So
|
|
* `yield t` / `return t` / `let q = t` over a tuple ident leave the whole
|
|
* tuple in the cursor, not just word0 in AX. Over-cap loud-stops (rule 7;
|
|
* the #10 sret follow-up). */
|
|
static void
|
|
cg_tuple_slot_to_cursor(Cg *c, int srcoff, Type *tu)
|
|
{
|
|
int f32;
|
|
int gptotal = 0, ssecount = 0;
|
|
for (Tparam *p = tu->params; p; p = p->next) {
|
|
Type *pu = type_chase_named(p->type);
|
|
int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR);
|
|
if (fld_isfloat(p->type, &f32))
|
|
ssecount++;
|
|
else
|
|
gptotal += tuple_ebytes(wide);
|
|
}
|
|
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
|
|
fatal("tuple ident exceeds register-return ABI capacity "
|
|
"(integer %d/%d, SSE %d/%d); over-cap rvalue-tuple "
|
|
"materialisation is the #10 sret follow-up",
|
|
gptotal, TUPLE_GPCAP, ssecount, TUPLE_SSECAP);
|
|
int gp = 0, sse = 0, foff = 0;
|
|
for (Tparam *p = tu->params; p; p = p->next) {
|
|
Type *pu = type_chase_named(p->type);
|
|
int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR);
|
|
int isflt = fld_isfloat(p->type, &f32);
|
|
if (isflt) {
|
|
ins2(c, f32 ? A_MOVSS : A_MOVSD,
|
|
amem(D_BP, srcoff + foff),
|
|
areg(tuple_sse_seq[sse]));
|
|
sse++;
|
|
foff += 8;
|
|
} else if (wide) {
|
|
for (int k = 0; k < 3; k++)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, srcoff + foff + k * 8),
|
|
areg(tuple_rseq[gp + k]));
|
|
gp += 3;
|
|
foff += (int)pu->size;
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, srcoff + foff),
|
|
areg(tuple_rseq[gp]));
|
|
gp += 1;
|
|
foff += 8;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* cg_tagged_tuple_payload_shift — #241: a `?`-unwrapped tuple payload is an
|
|
* rvalue tuple that must fill the register cursor the let/destructure
|
|
* consumer reads. A tagged return leaves AX=tag, DX=word0, CX=word1,
|
|
* R8=word2; the scalar/str unwrap lifts only word0->AX, stranding word1+ in
|
|
* CX/R8. Shift the whole payload DOWN one INTEGER reg so element i lands in
|
|
* tuple_rseq[i]. A float/slice/str payload element rides a different SysV
|
|
* class (X regs / 3-word header) the flat down-shift can't place — loud-stop
|
|
* (rule 7); the per-eightbyte tagged-tuple-payload classification is the
|
|
* #243 follow-up. */
|
|
static void
|
|
cg_tagged_tuple_payload_shift(Cg *c, Type *tup)
|
|
{
|
|
static const int seq[] = { D_AX, D_DX, D_CX, D_R8 };
|
|
int f32;
|
|
int words = 0;
|
|
for (Tparam *p = tup->params; p; p = p->next) {
|
|
Type *pu = type_chase_named(p->type);
|
|
int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR);
|
|
if (fld_isfloat(p->type, &f32) || wide)
|
|
fatal("tuple-in-union ? unwrap: float/slice/str payload "
|
|
"element needs SysV per-eightbyte classification "
|
|
"(see #243); only integer tuple payloads supported");
|
|
words += tuple_ebytes(0);
|
|
}
|
|
/* tag occupies AX, so only DX/CX/R8 carry payload words. */
|
|
if (words > (int)nelem(seq) - 1)
|
|
fatal("tuple-in-union ? unwrap payload exceeds the 3 integer "
|
|
"return regs past the tag (%d words); see #10/#243", words);
|
|
for (int i = 0; i < words; i++)
|
|
ins2(c, A_MOVQ, areg(seq[i + 1]), areg(seq[i]));
|
|
}
|
|
|
|
/* cg_arrlit_fill_bp — #31: fill the [count]T destination at BP-relative
|
|
* `off` from an N_ARRLIT, extracted verbatim from the N_LET array-init
|
|
* path so the slice-borrow base materialisation (the N_SLICE-over-
|
|
* N_ARRLIT arm) reuses the identical element-store sequence. `lu` is the
|
|
* [count]T array type the checker re-stamped (#25); `arrlit` the literal. */
|
|
static void
|
|
cg_arrlit_fill_bp(Cg *c, Local **locals, Type *lu, Node *arrlit, int off)
|
|
{
|
|
Type *esub = lu->sub;
|
|
int esz = esub ? (int)esub->size : 1;
|
|
/* #270-1c: an AGGREGATE (struct/array/tuple) element
|
|
* of an array literal — the scalar per-element MOVQ
|
|
* below stores only the first 8 bytes (unpopulated
|
|
* tail). Fill each element slot from its literal
|
|
* (cg_structlit_fill_bp) or source ident (word-copy). */
|
|
Type *esubu = type_chase_named(esub);
|
|
int is_agg = esubu && (esubu->kind == TY_STRUCT
|
|
|| esubu->kind == TY_ARRAY
|
|
|| esubu->kind == TY_TUPLE);
|
|
/* #12: a tagged-union element. NOT folded into is_agg —
|
|
* is_agg's body does N_STRUCTLIT/N_IDENT word-copy and
|
|
* FATALs on the literal/scalar case, never boxing the
|
|
* tag+payload. Route each element through the same
|
|
* cg_widen_tagged_store choke-point every other tagged
|
|
* store uses (let-init, vararg gather, struct-field). */
|
|
int is_tagged_el = esubu && esubu->kind == TY_TAGGED;
|
|
int is_str_el = type_isstr(esub);
|
|
/* #20/#270 str-slice arm: a slice element is a 24B
|
|
* {ptr,len,cap} header just like str; cgexpr lowers it
|
|
* into AX/BX/CX. Both must store all three words — the
|
|
* scalar 1-word MOVQ below drops .len and .cap. */
|
|
int is_slice_el = type_isslice(esub);
|
|
/* float element → store FROM X0; the AX path stores
|
|
* raw double low-bits, garbage for f32 (#122, twin of
|
|
* the arr[i]= store fix and the cgen.c:6423 read). */
|
|
int is_float_el = type_isfloat(esub);
|
|
int fmov = type_isf32(esub) ? A_MOVSS : A_MOVSD;
|
|
int op = A_MOVQ;
|
|
if (!is_str_el) {
|
|
if (esz == 1) op = A_MOVB;
|
|
else if (esz == 2) op = A_MOVW;
|
|
else if (esz == 4) op = A_MOVL;
|
|
/* #128a: esz==2 routes to MOVW (A_MOVW landed in
|
|
* both stages' w6a). Pre-fix the 2-byte case fell
|
|
* through to MOVQ, over-writing 6B into the next
|
|
* element's slot; sequential adjacent writes
|
|
* accident-corrected fully-init arrays but
|
|
* partial inits clobbered neighbours. */
|
|
}
|
|
int idx = 0;
|
|
Node *last = NULL;
|
|
int repeat = 0;
|
|
for (Node *e = arrlit->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str &&
|
|
strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
int base = off + idx * esz;
|
|
if (is_agg) {
|
|
if (e->kind == N_STRUCTLIT) {
|
|
cg_structlit_fill_bp(c, locals,
|
|
esubu, e, base);
|
|
} else if (e->kind == N_IDENT) {
|
|
int soff = localfind(*locals,
|
|
e->str);
|
|
int k = 0;
|
|
for (; k + 8 <= esz; k += 8) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
areg(D_AX),
|
|
amem(D_BP, base + k));
|
|
}
|
|
if (k + 4 <= esz) {
|
|
ins2(c, A_MOVL,
|
|
amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL,
|
|
areg(D_AX),
|
|
amem(D_BP, base + k));
|
|
k += 4;
|
|
}
|
|
if (k + 2 <= esz) {
|
|
ins2(c, A_MOVW,
|
|
amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVW,
|
|
areg(D_AX),
|
|
amem(D_BP, base + k));
|
|
k += 2;
|
|
}
|
|
if (k + 1 <= esz) {
|
|
ins2(c, A_MOVB,
|
|
amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB,
|
|
areg(D_AX),
|
|
amem(D_BP, base + k));
|
|
k += 1;
|
|
}
|
|
} else {
|
|
fatal("#270-1c: array-literal "
|
|
"aggregate element shape "
|
|
"unsupported (rule-7)");
|
|
}
|
|
last = e;
|
|
idx++;
|
|
continue;
|
|
}
|
|
if (is_tagged_el) {
|
|
cg_widen_tagged_store(c, locals, esub,
|
|
e, D_BP, base, esz);
|
|
last = e;
|
|
idx++;
|
|
continue;
|
|
}
|
|
cgexpr(c, e, *locals);
|
|
if (is_str_el || is_slice_el) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, base));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, base + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, base + 16));
|
|
} else if (is_float_el) {
|
|
ins2(c, fmov, areg(D_X0),
|
|
amem(D_BP, base));
|
|
} else {
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, base));
|
|
}
|
|
last = e;
|
|
idx++;
|
|
}
|
|
if (repeat && is_agg)
|
|
fatal("#270-1c: `...` repeat of an aggregate "
|
|
"array-literal element not wired (rule-7)");
|
|
/* #12: `...` re-stores from AX, but cg_widen_tagged_store
|
|
* consumed the node and trashed AX — a repeat-fill would
|
|
* write garbage. No consumer needs `[N]tagged=[x,...]`. */
|
|
if (repeat && is_tagged_el)
|
|
fatal("#12: `...` repeat of a tagged-union "
|
|
"array-literal element not wired (rule-7)");
|
|
if (repeat && last) {
|
|
/* fill remaining slots with the value still in
|
|
* AX (and BX for str). */
|
|
while (idx < (int)lu->alen) {
|
|
int base = off + idx * esz;
|
|
if (is_str_el || is_slice_el) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, base));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, base + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, base + 16));
|
|
} else if (is_float_el) {
|
|
ins2(c, fmov, areg(D_X0),
|
|
amem(D_BP, base));
|
|
} else {
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, base));
|
|
}
|
|
idx++;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
cgexpr(Cg *c, Node *n, Local *locals)
|
|
{
|
|
if (n == NULL) {
|
|
cgexpr_int(c, 0);
|
|
return;
|
|
}
|
|
switch (n->kind) {
|
|
case N_INTLIT:
|
|
case N_RUNELIT:
|
|
if (node_isfloat(n)) {
|
|
cgexpr_float(c, (double)(long long)n->uval);
|
|
/* #104: cgexpr_float materialises a DOUBLE in X0; an
|
|
* f32-typed literal must narrow with hardware single-
|
|
* rounding so the downstream MOVSS reads a true single. */
|
|
if (node_isf32(n))
|
|
ins2(c, A_CVTSD2SS, areg(D_X0), areg(D_X0));
|
|
break;
|
|
}
|
|
cgexpr_int(c, (long long)n->uval);
|
|
break;
|
|
case N_FLOATLIT:
|
|
cgexpr_float(c, n->fval);
|
|
/* #104: narrow the double in X0 to single for an f32 literal. */
|
|
if (node_isf32(n))
|
|
ins2(c, A_CVTSD2SS, areg(D_X0), areg(D_X0));
|
|
break;
|
|
case N_STRLIT: {
|
|
/* str IS []u8: the (ptr, len, cap) triple — ptr in AX, len in
|
|
* BX, cap in CX. A static literal has no spare storage, so
|
|
* cap = len (#1/Phase 3, task (b)). */
|
|
const char *lab = intern_strlit(c, n->str, n->strlen);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_BX));
|
|
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_CX));
|
|
break;
|
|
}
|
|
case N_TRUE: cgexpr_int(c, 1); break;
|
|
case N_FALSE:
|
|
case N_NIL:
|
|
case N_VOIDLIT: cgexpr_int(c, 0); break;
|
|
case N_IDENT: {
|
|
int off = localfind(locals, n->str);
|
|
if (off != 0) {
|
|
Type *itu = type_chase_named(n->type);
|
|
if (itu && itu->kind == TY_TUPLE) {
|
|
/* #241: a tuple ident is a value — leave the whole
|
|
* tuple in the register cursor (`yield t` / `return
|
|
* t` / `let q = t`), not just word0 in AX. */
|
|
cg_tuple_slot_to_cursor(c, off, itu);
|
|
} else if (node_isfloat(n)) {
|
|
int op = op_for(n, A_MOVSD, A_MOVSS);
|
|
ins2(c, op, amem(D_BP, off), areg(D_X0));
|
|
} else if (node_isstr(n)) {
|
|
/* str IS []u8: flow as (AX=ptr, BX=len, CX=cap),
|
|
* mirroring the slice local load below (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
|
|
} else if (node_isslice(n)) {
|
|
/* slice values flow as (AX=ptr, BX=len, CX=cap)
|
|
* — mirror the global-slice load so a slice
|
|
* local can be reassigned, returned, or copied
|
|
* with the same triple convention. */
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
|
|
} else {
|
|
ins2(c, localloadop(n->type),
|
|
amem(D_BP, off), areg(D_AX));
|
|
}
|
|
} else {
|
|
/* Non-local: function symbols load by address (LEAQ),
|
|
* str-typed `def`s expand to (ptr, len) of the literal,
|
|
* other globals (def constants) load by value (MOVQ). */
|
|
Type *t = n->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (u && u->kind == TY_FN) {
|
|
/* Take the address of a function. Apply
|
|
* @symbol resolution so taking the address
|
|
* of a body-less FFI binding yields the C
|
|
* symbol, not the ww-side ident. Hare emits
|
|
* the same `$symname` for both call and
|
|
* address-of via QBE; here we mirror that.
|
|
* Bare ident → same-module by ww's resolver,
|
|
* so c->cur_mod is the right disambiguation
|
|
* hint. */
|
|
ins2(c, A_LEAQ,
|
|
mafn(c, n->str, c->cur_mod), areg(D_AX));
|
|
break;
|
|
}
|
|
{
|
|
/* Same-module-first walk over Sdef. Without
|
|
* the prefer pass two modules with same-leaf
|
|
* `def MSG: str = "..."` silently fold the
|
|
* wrong strlit into the caller's bare-ident
|
|
* load (sister callsite of cgdot's str-def
|
|
* field fold + wwstage deflookuprhs #4c). */
|
|
Sdef *s;
|
|
for (s = sdefs; s; s = s->next) {
|
|
if (strcmp(s->name, n->str) != 0)
|
|
continue;
|
|
if (sdef_mod_match(c, s)) break;
|
|
}
|
|
if (s == NULL) {
|
|
for (s = sdefs; s; s = s->next)
|
|
if (strcmp(s->name, n->str) == 0)
|
|
break;
|
|
}
|
|
if (s != NULL) {
|
|
const char *lab = intern_strlit(c,
|
|
s->bytes, s->len);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
aimm((long long)s->len),
|
|
areg(D_BX));
|
|
/* str IS []u8: cap = len for a static
|
|
* def literal (#1/Phase 3). */
|
|
ins2(c, A_MOVQ,
|
|
aimm((long long)s->len),
|
|
areg(D_CX));
|
|
goto ident_done;
|
|
}
|
|
}
|
|
if (let_islet(n->str)
|
|
&& (let_isstr(n->type) || let_isslice(n->type))) {
|
|
/* Top-level str/slice global: load each word
|
|
* via its address (the asm has no `name+8(SB)`
|
|
* operand form). str IS []u8 now — both carry a
|
|
* third 8B (cap); the address holder CX gets
|
|
* overwritten by the cap as the last step, after
|
|
* we no longer need it (#1/Phase 3). */
|
|
ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX));
|
|
goto ident_done;
|
|
}
|
|
if (let_isfloat(n->type)) {
|
|
/* Top-level float global (let OR def): same
|
|
* LEAQ-indirect shape as str/slice, since
|
|
* MOVSS/MOVSD have no D_EXTERN operand form in
|
|
* w6a. Pre-#129 this gated on `let_islet` so
|
|
* float defs fell through to the MOVQ-AX
|
|
* integer-convention fallback below; that
|
|
* load-shape mismatched the float storage emit
|
|
* (#129 Phase A.1 LOAD-side twin of the
|
|
* emit_floatlit_data DATA-side SSoT). */
|
|
int op = type_isf32(n->type) ? A_MOVSS : A_MOVSD;
|
|
ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod),
|
|
areg(D_CX));
|
|
ins2(c, op, amem(D_CX, 0), areg(D_X0));
|
|
goto ident_done;
|
|
}
|
|
/* Top-level lets can be the target of `*p` deref-stores
|
|
* (via `&letname: *iN`), so a signed-narrow scalar let
|
|
* needs MOVSXD/MOVSWQ/MOVSBQ on the read. Defs are
|
|
* read-only constants — their address cannot escape,
|
|
* so they keep the simpler MOVQ shape (and the wwstage
|
|
* defent registry, which doesn't track the declared
|
|
* type, agrees byte-for-byte). */
|
|
int gop = let_islet(n->str)
|
|
? localloadop(n->type) : A_MOVQ;
|
|
if (gop == A_MOVQ) {
|
|
ins2(c, A_MOVQ, mahint(c, n->str, c->cur_mod),
|
|
areg(D_AX));
|
|
} else {
|
|
/* w6a has no MOVSXD/MOVSWQ/MOVSBQ D_EXTERN
|
|
* source form, so route through a LEAQ scratch
|
|
* the same way top-level str/slice/float lets
|
|
* do. */
|
|
ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod),
|
|
areg(D_CX));
|
|
ins2(c, gop, amem(D_CX, 0), areg(D_AX));
|
|
}
|
|
}
|
|
ident_done:
|
|
break;
|
|
}
|
|
case N_UN:
|
|
/* Address-of has its own evaluation strategy — we want the
|
|
* address of the operand, not its value. Special-case before
|
|
* the cgexpr pre-eval below so `&arr[i]` doesn't compile the
|
|
* value load and then discard it. */
|
|
if (n->op == TK_AMP) {
|
|
Node *opnd = n->lhs;
|
|
if (opnd && opnd->kind == N_IDENT) {
|
|
int off = localfind(locals, opnd->str);
|
|
Type *ot = opnd->type;
|
|
Type *ou = (ot && ot->kind == TY_NAMED)
|
|
? ot->under : ot;
|
|
if (off != 0) {
|
|
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
|
|
} else if (ou && ou->kind == TY_FN) {
|
|
/* #180: address-of a top-level fn name.
|
|
* Twin of the N_IDENT TY_FN read-arm at
|
|
* line 2330 (mafn with c->cur_mod hint).
|
|
* Previously this fell through silently —
|
|
* the AX-store at the assign site picked
|
|
* up whatever AX held from prior code, so
|
|
* `let f = &add1; (*f)(7)` jumped through
|
|
* stale AX. */
|
|
ins2(c, A_LEAQ,
|
|
mafn(c, opnd->str, c->cur_mod),
|
|
areg(D_AX));
|
|
} else if (let_islet(opnd->str)
|
|
|| def_isstructdef(opnd->str)
|
|
|| def_isarraydef(opnd->str)
|
|
|| def_isscalardef(opnd->str)) {
|
|
/* #149/#147: address-of a top-level def
|
|
* with DATA storage. emit_defs / emit_
|
|
* struct_data / emit_array_data all emit
|
|
* to mod_mangle(name), so the address is
|
|
* the same LEAQ name(SB) as a let. The
|
|
* address-of twin of A.2/A.3's LOAD-side
|
|
* widening. */
|
|
ins2(c, A_LEAQ, masym(c, opnd->str),
|
|
areg(D_AX));
|
|
} else if (def_isanydef(opnd->str)) {
|
|
/* #149/#147 rule-7: the name IS a def but
|
|
* has no DATA symbol (str def inlined, or
|
|
* computed-rhs float like `def NAN =
|
|
* 0.0/0.0`). Loud, not a wild deref. */
|
|
fatal("cannot take address of non-"
|
|
"addressable def '%s': no DATA symbol "
|
|
"(str/computed-rhs def; #149/#147)",
|
|
opnd->str);
|
|
}
|
|
break;
|
|
}
|
|
if (opnd && opnd->kind == N_DOT) {
|
|
/* #149 Shape 2: `&mod.G` — module-qualified
|
|
* address-of of an exported global (let or def).
|
|
* The checker leaves SK_USE module idents untyped
|
|
* (NULL/ty_err); detect that and LEAQ the leaf
|
|
* symbol. Kind-agnostic (covers cross-module &let
|
|
* / &def / &scalar) — the address-of twin of the
|
|
* value-read mod-qual path below. A TY_FN leaf
|
|
* resolves via mafn (fn address), mirroring the
|
|
* read path's TY_FN branch. Placed before the
|
|
* spine walk, which aborts on the untyped base
|
|
* anyway. */
|
|
if (opnd->lhs && opnd->lhs->kind == N_IDENT
|
|
&& (opnd->lhs->type == NULL
|
|
|| opnd->lhs->type == ty_err)) {
|
|
Type *lt = opnd->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED)
|
|
? lt->under : lt;
|
|
if (lu && lu->kind == TY_FN)
|
|
ins2(c, A_LEAQ,
|
|
mafn(c, opnd->str,
|
|
opnd->lhs->str),
|
|
areg(D_AX));
|
|
else
|
|
/* #229: dotted-module value
|
|
* mangle (twin of the read), so
|
|
* &aa.v takes aa's global, not a
|
|
* same-leaf collision. */
|
|
ins2(c, A_LEAQ,
|
|
mahint(c, opnd->str,
|
|
opnd->lhs->str),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
/* Address-of through a DOT chain. The early-exit
|
|
* above handled `&ident` and `&base[i]`; everything
|
|
* else was silently dropped. Three shapes converge
|
|
* here, all returning an 8B address (so no
|
|
* fldloadop dispatch — just LEAQ).
|
|
*
|
|
* 1. Value-struct fields, any depth (`&o.f`,
|
|
* `&o.i.a`, `&o.a.b.c`): walk the spine to a
|
|
* root ident, sum field offsets, emit LEAQ at
|
|
* base + sum. Mirror of the read at line 3722.
|
|
* 2. Slice/str pseudo-field tail (`&s.len`,
|
|
* `&b.buf.len`): folds into the spine walk
|
|
* with slice_delta 0/8/16.
|
|
* 3. Pointer-field (`&p.f` where p:*T): the spine
|
|
* walk aborts at the *T base; the fallback
|
|
* below loads p into AX and adds field_off.
|
|
*/
|
|
int amped = 0;
|
|
/* Spine walk — same shape as the read at 3722.
|
|
* Records (parent_struct, field_name) leaf-first,
|
|
* then iterates root-first to sum offsets. */
|
|
struct { Type *pu; const char *name; } steps[16];
|
|
int nsteps = 0;
|
|
Node *cur = opnd;
|
|
int abort = 0;
|
|
while (cur && cur->kind == N_DOT && cur->lhs) {
|
|
Type *pt = cur->lhs->type;
|
|
Type *pu = (pt && pt->kind == TY_NAMED)
|
|
? pt->under : pt;
|
|
if (!pu) { abort = 1; break; }
|
|
if (cur == opnd && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR)) {
|
|
/* leaf pseudo on slice/str header */
|
|
} else if (pu->kind != TY_STRUCT) {
|
|
abort = 1;
|
|
break;
|
|
}
|
|
if (nsteps >= 16) { abort = 1; break; }
|
|
steps[nsteps].pu = pu;
|
|
steps[nsteps].name = cur->str;
|
|
nsteps++;
|
|
cur = cur->lhs;
|
|
}
|
|
if (!abort && cur && cur->kind == N_IDENT
|
|
&& nsteps > 0) {
|
|
int total_off = 0;
|
|
int slice_delta = -1;
|
|
int ok = 1;
|
|
for (int i = nsteps - 1; i >= 0; i--) {
|
|
Type *pu = steps[i].pu;
|
|
if (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR) {
|
|
if (strcmp(steps[i].name, "ptr") == 0)
|
|
slice_delta = 0;
|
|
else if (strcmp(steps[i].name, "len") == 0)
|
|
slice_delta = 8;
|
|
else if (strcmp(steps[i].name, "cap") == 0)
|
|
slice_delta = 16;
|
|
else { ok = 0; break; }
|
|
} else {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = pu->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, steps[i].name) == 0)
|
|
{ f = fl; break; }
|
|
if (!f) { ok = 0; break; }
|
|
total_off += (int)f->offset;
|
|
}
|
|
}
|
|
if (ok) {
|
|
int extra = (slice_delta >= 0)
|
|
? slice_delta : 0;
|
|
int root_off = localfind(locals, cur->str);
|
|
if (root_off != 0) {
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP,
|
|
root_off + total_off + extra),
|
|
areg(D_AX));
|
|
amped = 1;
|
|
} else if (let_islet(cur->str)) {
|
|
/* Two-step global form mirrors the
|
|
* read path's `LEAQ name,CX → MOVQ
|
|
* disp(CX),AX`, swapping the MOVQ
|
|
* for LEAQ. */
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str), areg(D_CX));
|
|
ins2(c, A_LEAQ,
|
|
amem(D_CX, total_off + extra),
|
|
areg(D_AX));
|
|
amped = 1;
|
|
}
|
|
}
|
|
}
|
|
/* Pointer-field fallback for `&p.f` where p:*T —
|
|
* the spine walker aborts on the *T base. Load p
|
|
* into AX, then LEAQ field_off(AX),AX. Mirror of
|
|
* the read at line 4033. */
|
|
if (!amped && opnd->lhs
|
|
&& opnd->lhs->kind == N_IDENT) {
|
|
Type *bt = opnd->lhs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
if (bu && bu->kind == TY_PTR && bu->sub) {
|
|
Type *inner = bu->sub;
|
|
if (inner->kind == TY_NAMED)
|
|
inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
for (Tfield *f = inner->fields;
|
|
f; f = f->next) {
|
|
if (strcmp(f->name, opnd->str) != 0)
|
|
continue;
|
|
int off = localfind(locals,
|
|
opnd->lhs->str);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_AX));
|
|
ins2(c, A_LEAQ,
|
|
amem(D_AX, (int)f->offset),
|
|
areg(D_AX));
|
|
amped = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (amped) break;
|
|
/* Fall through to silent-drop fallback below. */
|
|
}
|
|
if (opnd && opnd->kind == N_INDEX) {
|
|
/* &base[i] = base + i*esz, no dereference. */
|
|
Node *base = opnd->lhs;
|
|
Node *idx = opnd->rhs;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
int esz = (bu && bu->sub)
|
|
? (int)bu->sub->size : 1;
|
|
cgexpr(c, idx, locals); /* idx → AX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (base && base->kind == N_IDENT) {
|
|
int boff = localfind(locals,
|
|
base->str);
|
|
int is_arr = bu &&
|
|
bu->kind == TY_ARRAY;
|
|
if (boff != 0) {
|
|
if (is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
}
|
|
} else if (let_islet(base->str)) {
|
|
if (is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
}
|
|
} else {
|
|
ins2(c, A_XORQ, areg(D_BX),
|
|
areg(D_BX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_BX),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
/* Complex base: eval to AX, swap into BX,
|
|
* then add the saved scaled idx. #252: an
|
|
* N_DOT `[N]T`-field base needs the field
|
|
* ADDRESS (cg_dotbase_addr LEAQ) — cgexpr would
|
|
* auto-deref + load the field VALUE as a pointer
|
|
* (segfault). Sibling of the #135 read-side wiring. */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (!cg_dotbase_addr(c, base, D_AX, locals))
|
|
cgexpr(c, base, locals);
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
}
|
|
/* Other shapes (& on a complex expr): silent drop,
|
|
* mirrors the pre-existing fallback. */
|
|
break;
|
|
}
|
|
cgexpr(c, n->lhs, locals);
|
|
switch (n->op) {
|
|
case TK_MINUS:
|
|
if (node_isfloat(n->lhs)) {
|
|
/* Float negate: X0 = 0 - X0. cgexpr left the
|
|
* value in X0; AX-only NEGQ wouldn't touch it. */
|
|
int isf32 = node_isf32(n->lhs);
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
int sub = isf32 ? A_SUBSS : A_SUBSD;
|
|
/* save orig X0 → stack */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
/* load 0.0 into X0 (zero bit pattern == 0.0) */
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
/* X1 = orig; X0 = X0 - X1 = -orig */
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, sub, areg(D_X1), areg(D_X0));
|
|
} else {
|
|
ins1(c, A_NEGQ, areg(D_AX));
|
|
}
|
|
break;
|
|
case TK_TILDE:
|
|
/* NOTQ inverts the whole 64-bit register. For unsigned
|
|
* narrow types we clamp to the type width so the
|
|
* upper bits are 0, matching how zero-extended loads
|
|
* leave the register. Signed narrow types already
|
|
* end up sign-extended (NOTQ on a sign-extended
|
|
* positive becomes sign-extended negative), so they
|
|
* need no fix-up. u32 uses MOVL r,r (zero-extends
|
|
* upper 32) because ANDQ $0xFFFFFFFF would sign-extend
|
|
* the imm32 to all-ones and act as a no-op. */
|
|
ins1(c, A_NOTQ, areg(D_AX));
|
|
if (n->type && type_isunsigned(n->type)
|
|
&& n->type->size < 8) {
|
|
if (n->type->size == 4) {
|
|
ins2(c, A_MOVL, areg(D_AX), areg(D_AX));
|
|
} else {
|
|
u64 mask = ((u64)1 << (n->type->size * 8)) - 1;
|
|
ins2(c, A_ANDQ, aimm((i64)mask), areg(D_AX));
|
|
}
|
|
}
|
|
break;
|
|
case TK_NOT: {
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
char *t = mklabel(c, "tt");
|
|
char *e = mklabel(c, "te");
|
|
ins1(c, A_JE, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
case TK_AMP:
|
|
/* Handled in the pre-cgexpr early-exit above. */
|
|
break;
|
|
case TK_STAR: /* deref */
|
|
{
|
|
/* #185: deref of *fn — the pointer value IS
|
|
* the fn address. cgexpr(opnd) already left
|
|
* AX = fn-addr; a generic MOVQ (AX),AX would
|
|
* load the first instruction word and CALL
|
|
* would segfault on that junk. Mirror
|
|
* ref/harec/src/check.c expr_call's
|
|
* STORAGE_POINTER→STORAGE_FUNCTION skip. */
|
|
Type *rt = n->type;
|
|
Type *ru = (rt && rt->kind == TY_NAMED)
|
|
? rt->under : rt;
|
|
if (ru && ru->kind == TY_FN)
|
|
break;
|
|
}
|
|
/* f64/f32 result rides X0 (SSE), not AX — an integer
|
|
* MOVQ strands the value off the float ABI and the
|
|
* caller's MOVSD X0 reads stale bits (#96). Mirrors the
|
|
* float field/ident load idiom at 1462/1838. */
|
|
if (node_isfloat(n)) {
|
|
ins2(c, node_isf32(n) ? A_MOVSS : A_MOVSD,
|
|
amem(D_AX, 0), areg(D_X0));
|
|
} else {
|
|
/* Load-twin of the signed-narrow-scalar-reads
|
|
* sweep (project_cgen_int_cast_no_truncate);
|
|
* TK_STAR was the omitted site, refiled as
|
|
* #116. A raw MOVQ pulls 8 bytes through a
|
|
* narrow `*iN` and overlaps the next element
|
|
* — the `*p` value reads honest only when the
|
|
* caller's sink happens to truncate (i32 store,
|
|
* i32 return). Width-preserving sinks (CMPQ,
|
|
* 64-bit arith) saw garbage in the high bytes.
|
|
* localloadop keys MOVSXD/MOVSWQ/MOVSBQ +
|
|
* MOVL/MOVZWQ/MOVZBQ off n->type, with the
|
|
* TY_NAMED / TY_ENUM peel pre-folded so an
|
|
* aliased narrow (`type err = !i32`) lands on
|
|
* the right opcode. */
|
|
ins2(c, localloadop(n->type),
|
|
amem(D_AX, 0), areg(D_AX));
|
|
}
|
|
break;
|
|
default: break;
|
|
}
|
|
break;
|
|
case N_BIN: {
|
|
/* Short-circuit `&&` / `||`. Operands are bool (0/1); the
|
|
* type checker enforces it. Eval LHS into AX, branch over
|
|
* RHS on the short-circuit polarity, otherwise eval RHS
|
|
* into AX. The surviving AX is the result. Must precede
|
|
* any eager-eval path below — `if (p != nil && p.x > 0)`
|
|
* would segfault on a nil deref otherwise. */
|
|
if (n->op == TK_AND || n->op == TK_OR) {
|
|
char *end = mklabel(c, n->op == TK_AND ? "andend" : "orend");
|
|
int jshrt = (n->op == TK_AND) ? A_JE : A_JNE;
|
|
cgexpr(c, n->lhs, locals);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, jshrt, abranch(end));
|
|
cgexpr(c, n->rhs, locals);
|
|
label(c, end);
|
|
break;
|
|
}
|
|
/* str == str / str != str — delegate to rt_streq, which
|
|
* does the byte-by-byte compare. */
|
|
if ((n->op == TK_EQ || n->op == TK_NEQ) &&
|
|
node_isstr(n->lhs) && node_isstr(n->rhs)) {
|
|
/* Push rhs (len, then ptr top) */
|
|
if (n->rhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->rhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
} else {
|
|
cgexpr(c, n->rhs, locals); /* AX=ptr, BX=len */
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
/* Push lhs */
|
|
if (n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
} else {
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_DI));
|
|
ins1(c, A_POPQ, areg(D_SI));
|
|
ins1(c, A_POPQ, areg(D_DX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins1(c, A_CALL, asym("rt_streq"));
|
|
if (n->op == TK_NEQ)
|
|
ins2(c, A_XORQ, aimm(1), areg(D_AX));
|
|
break;
|
|
}
|
|
/* Float comparison: operands are float but the BIN node's
|
|
* type is bool, so node_isfloat(n) is false — we have to
|
|
* inspect n->lhs. UCOMISD/UCOMISS sets ZF/CF as if an
|
|
* unsigned compare, so the JA family is the right Jcc set
|
|
* regardless of how the operand types are signed. Plan 9's
|
|
* own w6c picks the same pattern (txt.c around AUCOMISD).
|
|
* NaN handling: UCOMI sets PF=ZF=CF=1 on unordered (a NaN
|
|
* operand). IEEE-754: any relop with a NaN operand is
|
|
* unordered — `!=` true, the other five false. PF must steer
|
|
* `!=`/`==`/`<`/`<=` (#97): JNE keys on ZF=0 so `nan != nan`
|
|
* came out false; JE/JB/JBE all fire on the unordered ZF/CF.
|
|
* `>`/`>=` (JA/JAE) need CF=0, which unordered never gives,
|
|
* so they are ALREADY NaN-correct and stay byte-identical to
|
|
* the pre-#97 single-template arm — no redundant PF guard. */
|
|
if (n->lhs && node_isfloat(n->lhs) &&
|
|
(n->op == TK_EQ || n->op == TK_NEQ
|
|
|| n->op == TK_LT || n->op == TK_LE
|
|
|| n->op == TK_GT || n->op == TK_GE)) {
|
|
int isf32 = node_isf32(n->lhs);
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
int ucomi = isf32 ? A_UCOMISS : A_UCOMISD;
|
|
cgexpr(c, n->rhs, locals); /* rhs → X0 */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
cgexpr(c, n->lhs, locals); /* lhs → X0 */
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, ucomi, areg(D_X1), areg(D_X0));
|
|
if (n->op == TK_NEQ) {
|
|
/* not-equal OR unordered -> true */
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, A_JNE, abranch(t));
|
|
ins1(c, A_JP, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
if (n->op == TK_EQ || n->op == TK_LT || n->op == TK_LE) {
|
|
/* unordered -> false; otherwise the ordered Jcc decides */
|
|
int op = (n->op == TK_EQ) ? A_JE
|
|
: (n->op == TK_LT) ? A_JB : A_JBE;
|
|
char *fl = mklabel(c, "cf");
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, A_JP, abranch(fl));
|
|
ins1(c, op, abranch(t));
|
|
label(c, fl);
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
/* `>`/`>=`: JA/JAE already reject unordered (CF=1), so
|
|
* keep the pre-#97 single-template shape verbatim. */
|
|
int op = (n->op == TK_GT) ? A_JA : A_JAE;
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, op, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
if (node_isfloat(n)) {
|
|
int isf32 = node_isf32(n);
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals); /* X0 */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
cgexpr(c, n->lhs, locals); /* X0 */
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
switch (n->op) {
|
|
case TK_PLUS:
|
|
ins2(c, isf32 ? A_ADDSS : A_ADDSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_MINUS:
|
|
ins2(c, isf32 ? A_SUBSS : A_SUBSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_STAR:
|
|
ins2(c, isf32 ? A_MULSS : A_MULSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_SLASH:
|
|
ins2(c, isf32 ? A_DIVSS : A_DIVSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
default: break;
|
|
}
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUS: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUS: ins2(c, A_SUBQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_STAR: ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_SLASH: {
|
|
/* Use DIV (unsigned) when either operand is an unsigned
|
|
* integer type — IDIV would sign-extend a u64 with high
|
|
* bit set into a negative i64 and produce wrong results
|
|
* (see strconv.u64tos with v = 1 << 63). Signed IDIV
|
|
* needs CQO to sign-extend RAX into RDX:RAX; zeroing
|
|
* DX would treat a negative dividend as a huge unsigned
|
|
* 128-bit value. */
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX));
|
|
break;
|
|
}
|
|
case TK_PERCENT: {
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case TK_AMP: ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_PIPE: ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_CARET: ins2(c, A_XORQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_LSHIFT: case TK_RSHIFT: {
|
|
/* shift amount must be in CL. #136: signed RSHIFT uses
|
|
* SAR (arithmetic, sign-extends MSB); unsigned uses SHR
|
|
* (logical, zero-fill). LSHIFT is signedness-agnostic
|
|
* (SHL == SAL at the encoder). */
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
int rop = unsignd ? A_SHRQ : A_SARQ;
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
ins2(c, n->op == TK_LSHIFT ? A_SHLQ : rop,
|
|
areg(D_CX), areg(D_AX));
|
|
break;
|
|
}
|
|
case TK_EQ: case TK_NEQ: case TK_LT: case TK_LE:
|
|
case TK_GT: case TK_GE: {
|
|
/* For ordered comparisons on unsigned operands we must
|
|
* use the JA/JAE/JB/JBE family — signed Jcc would treat
|
|
* a u64 with the high bit set as negative (e.g. the
|
|
* loop guard `n > 0` in strconv.u64tos with n=1<<63). */
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
|
|
int op = A_JE;
|
|
switch (n->op) {
|
|
case TK_EQ: op = A_JE; break;
|
|
case TK_NEQ:op = A_JNE; break;
|
|
case TK_LT: op = unsignd ? A_JB : A_JL; break;
|
|
case TK_LE: op = unsignd ? A_JBE : A_JLE; break;
|
|
case TK_GT: op = unsignd ? A_JA : A_JG; break;
|
|
case TK_GE: op = unsignd ? A_JAE : A_JGE; break;
|
|
default: break;
|
|
}
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, op, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
/* TK_AND / TK_OR handled with short-circuit codegen at the
|
|
* top of N_BIN — they never reach this eager-eval switch. */
|
|
default: break;
|
|
}
|
|
break;
|
|
}
|
|
case N_ASSIGN: {
|
|
/* Discard lvalue `_ = expr;` — evaluate rhs for side effects,
|
|
* write nothing. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT &&
|
|
n->lhs->str && n->lhs->str[0] == '\0' &&
|
|
n->op == TK_ASSIGN) {
|
|
cgexpr(c, n->rhs, locals);
|
|
break;
|
|
}
|
|
/* p.x = v or p.x += v where p.x is a struct field
|
|
* (direct or via *struct). For compound ops we read-modify-
|
|
* write the field; for plain `=` we just write. The base
|
|
* accepts two parser shapes: a bare IDENT (auto-deref when
|
|
* the IDENT's type is *T, value-struct otherwise) and the
|
|
* explicit-deref form `(*p).f = ...` where the parser emits
|
|
* N_UN(STAR, IDENT(p)). For (*p).f, retarget base to the
|
|
* inner IDENT so the via_ptr branch fires identically to
|
|
* `p.f = v`. v1 scope: bare-IDENT inner only; (*expr).f
|
|
* (non-IDENT inner) falls through to the existing drop
|
|
* behaviour pending follow-up task. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs &&
|
|
(n->lhs->lhs->kind == N_IDENT ||
|
|
(n->lhs->lhs->kind == N_UN && n->lhs->lhs->op == TK_STAR
|
|
&& n->lhs->lhs->lhs
|
|
&& n->lhs->lhs->lhs->kind == N_IDENT))) {
|
|
Node *base = n->lhs->lhs;
|
|
if (base->kind == N_UN) base = base->lhs;
|
|
Type *bt = base->type;
|
|
/* type_chase_named (#22): a chain `type b = a; a = struct`
|
|
* left u at TY_NAMED a after a single peel, missing the
|
|
* TY_STRUCT field-walk gate below — the assignment
|
|
* silently dropped (the `break` at the bottom of the
|
|
* N_DOT-lhs arm). */
|
|
Type *u = type_chase_named(bt);
|
|
int via_ptr = 0;
|
|
if (u && u->kind == TY_PTR) {
|
|
via_ptr = 1;
|
|
u = type_chase_named(u->sub);
|
|
}
|
|
/* slice/str pseudo-field write (.ptr/.len/.cap) */
|
|
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)) {
|
|
const char *fld = n->lhs->str;
|
|
int delta = -1;
|
|
if (strcmp(fld, "ptr") == 0) delta = 0;
|
|
else if (strcmp(fld, "len") == 0) delta = 8;
|
|
else if (strcmp(fld, "cap") == 0) delta = 16;
|
|
if (delta < 0) goto after_dot_assign;
|
|
int boff = localfind(locals, base->str);
|
|
if (n->op != TK_ASSIGN) {
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + delta), areg(D_BX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
if (n->op != TK_ASSIGN) {
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUSEQ:
|
|
/* old in BX, rhs in AX; want AX = old-rhs.
|
|
* SUBQ src,dst is dst -= src in Plan 9. */
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, delta));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + delta));
|
|
}
|
|
break;
|
|
}
|
|
after_dot_assign:
|
|
if (u && u->kind == TY_STRUCT) {
|
|
/* find field metadata */
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = u->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, n->lhs->str) == 0)
|
|
{ f = fl; break; }
|
|
if (f == NULL) break;
|
|
/* Tagged-union field: synthesise tag and store
|
|
* value bytes. Compound ops on tagged fields are
|
|
* not meaningful, so only plain `=` is wired.
|
|
* Three base shapes:
|
|
* - via_ptr: base is *struct local; address
|
|
* pre-loaded into BX. Buggy with a str
|
|
* variant since cgexpr will overwrite BX,
|
|
* but matches the existing pre-global
|
|
* behaviour.
|
|
* - is_global: struct global. LEAQ after
|
|
* cgexpr drops the slot address into CX
|
|
* without touching AX/BX, so str variants
|
|
* work cleanly.
|
|
* - else: struct local, BP-relative. */
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
/* Tagged-union field — full slot rewrite via the
|
|
* shared widener so every rhs shape (whole-tagged
|
|
* ident or expr with tag-remap, concrete-variant
|
|
* widening of str/slice/struct/scalar/void) lands
|
|
* the right tag + payload bytes. The pre-#26
|
|
* branch synthesised a single tag from
|
|
* cg_tag_for_variant and stored only AX at +8, so
|
|
* whole-tagged rhs (vt == fu, no concrete tag)
|
|
* silently wrote tag 0 and dropped trailing words.
|
|
* cg_widen_tagged_store handles every shape by
|
|
* branching on the source's resolved type. */
|
|
if (fu && fu->kind == TY_TAGGED
|
|
&& n->op == TK_ASSIGN) {
|
|
int boff = localfind(locals, base->str);
|
|
int is_global = (boff == 0 && !via_ptr
|
|
&& let_islet(base->str));
|
|
int foff = (int)f->offset;
|
|
int fsz = (int)fu->size;
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BX, foff, fsz);
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BX, foff, fsz);
|
|
} else {
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BP,
|
|
boff + foff, fsz);
|
|
}
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int load_op = fldloadop(f->type, fsz);
|
|
int store_op = fldstoreop(f->type, fsz);
|
|
int boff = localfind(locals, base->str);
|
|
int is_global = (boff == 0 && !via_ptr
|
|
&& let_islet(base->str));
|
|
int foff = (int)f->offset;
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
/* str/slice field: str IS []u8, so both store the full
|
|
* 3-word {ptr,len,cap} that rhs cgexpr leaves in
|
|
* (AX,BX,CX) at field+0/+8/+16. Address scratch must
|
|
* dodge CX (holds cap), so via_ptr/is_global stage the
|
|
* struct base in DX (#1/Phase 3). Without this the
|
|
* generic store_op below writes only AX, silently
|
|
* dropping .len/.cap. Only plain `=` is wired; compound
|
|
* on a str/slice field is not meaningful. */
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& (str_fu->kind == TY_SLICE || str_fu->kind == TY_STR)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, boff + foff + 16));
|
|
}
|
|
break;
|
|
}
|
|
/* #234: over-cap sret STORE into a struct field —
|
|
* `s.f = wide();` where f's type returns via sret
|
|
* (cg_sret_retsize > 0: a >24B struct OR an over-cap
|
|
* tuple — Fold A made the callee sret it). The STORE-
|
|
* twin of the Fold-B sret RECEIVE (a937d67): point the
|
|
* callee's hidden RDI dest straight at the field slot
|
|
* (cg_sret_dest_off) so it writes the WHOLE value there.
|
|
* Without this the generic scalar store below emits a
|
|
* truncated `MOVQ AX, off(BP)` and silently drops the
|
|
* sret body. cg_sret_dest_off is BP-relative ONLY, so
|
|
* this covers a LOCAL struct base; a via_ptr (`p.f`) or
|
|
* global base needs the runtime RDI-pointer dest variant
|
|
* deferred to #234-tail and HARD-STOPS loud (rule 7 —
|
|
* never fall through to the truncating store). */
|
|
if (n->op == TK_ASSIGN && n->rhs
|
|
&& n->rhs->kind == N_CALL
|
|
&& cg_sret_retsize(f->type) > 0) {
|
|
if (via_ptr || is_global || boff == 0)
|
|
fatal("#234-tail: over-cap tuple "
|
|
"sret store to non-local dest "
|
|
"unsupported");
|
|
cg_sret_dest_off = boff + foff;
|
|
cgexpr(c, n->rhs, locals);
|
|
cg_sret_dest_off = 0;
|
|
break;
|
|
}
|
|
/* struct-typed field, three rhs shapes:
|
|
* - N_IDENT: word-copy from the rhs slot directly
|
|
* onto the destination field. cgexpr cannot
|
|
* materialise a whole struct value in registers
|
|
* for an arbitrary local, so we read field words
|
|
* straight from the source slot.
|
|
* - N_CALL (added with #5): cgexpr leaves the value
|
|
* in AX/DX/CX per #4's cgreturn ABI; sized stores
|
|
* write only the declared field size — MOVQ for
|
|
* full 8B chunks plus MOVL/MOVW/MOVB tail. See
|
|
* the N_LET receive site for the ASYMMETRY
|
|
* rationale. cgreturn touches only AX/DX/CX, so
|
|
* BX stays free for the dst-addr load after the
|
|
* call.
|
|
* - N_STRUCTLIT (added with #5): field-by-field
|
|
* store; for via_ptr/is_global the dst base addr
|
|
* is reloaded into BX before each store so cgexpr
|
|
* can clobber AX/BX between fields. */
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& (int)str_fu->size <= 24
|
|
&& n->rhs && n->rhs->kind == N_CALL
|
|
&& (str_fu->size % 8 == 0
|
|
|| str_fu->size % 8 == 1
|
|
|| str_fu->size % 8 == 2
|
|
|| str_fu->size % 8 == 4)) {
|
|
int ssz = (int)str_fu->size;
|
|
cgexpr(c, n->rhs, locals);
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = ssz / 8;
|
|
int tail = ssz % 8;
|
|
int base_reg, base_disp;
|
|
if (via_ptr || is_global) {
|
|
if (via_ptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
base_reg = D_BX;
|
|
base_disp = foff;
|
|
} else {
|
|
base_reg = D_BP;
|
|
base_disp = boff + foff;
|
|
}
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(base_reg,
|
|
base_disp + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW
|
|
: A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(base_reg,
|
|
base_disp + full * 8));
|
|
}
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
|
|
/* Delegate to the shared structlit fill
|
|
* helper. For via_ptr/is_global, helper
|
|
* reloads BX before zero-fill loop + each
|
|
* field store. For local BP-rel, helper
|
|
* stores direct off BP. AND nested struct-
|
|
* typed structlit values recurse instead
|
|
* of silently dropping trailing bytes
|
|
* (#18 fix). */
|
|
int mode = via_ptr ? DST_PTR_LOCAL
|
|
: is_global ? DST_GLOBAL : DST_BP;
|
|
int disp = (mode == DST_BP)
|
|
? (boff + foff) : foff;
|
|
cg_structlit_fill(c, &locals, str_fu,
|
|
n->rhs, mode, boff,
|
|
is_global ? base->str : NULL, disp);
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_IDENT
|
|
&& localfind(locals, n->rhs->str) != 0) {
|
|
int soff = localfind(locals, n->rhs->str);
|
|
int ssz = (int)str_fu->size;
|
|
if (via_ptr)
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
else if (is_global)
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
int k = 0;
|
|
while (k + 8 <= ssz) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX));
|
|
if (via_ptr || is_global)
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + k));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + k));
|
|
k += 8;
|
|
}
|
|
if (k < ssz) {
|
|
int tail = ssz - k;
|
|
int lop = (tail == 4) ? A_MOVL
|
|
: (tail == 1 ? A_MOVB : A_MOVQ);
|
|
ins2(c, lop, amem(D_BP, soff + k), areg(D_AX));
|
|
if (via_ptr || is_global)
|
|
ins2(c, lop, areg(D_AX), amem(D_BX, foff + k));
|
|
else
|
|
ins2(c, lop, areg(D_AX), amem(D_BP, boff + foff + k));
|
|
}
|
|
break;
|
|
}
|
|
/* compound: load current value into BX */
|
|
if (n->op != TK_ASSIGN) {
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, foff), areg(D_BX));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, foff), areg(D_BX));
|
|
} else {
|
|
ins2(c, load_op, amem(D_BP, boff + foff), areg(D_BX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
}
|
|
cgexpr(c, n->rhs, locals); /* AX = rhs */
|
|
if (n->op != TK_ASSIGN) {
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUSEQ:
|
|
/* old in BX, rhs in AX; want AX=old-rhs */
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
default: break; /* others rare */
|
|
}
|
|
}
|
|
/* f64/f32 field, plain `=`: cgexpr left the value in
|
|
* X0, not AX. Route the store via MOVSD/MOVSS.
|
|
* Compound ops on float fields aren't wired here —
|
|
* see CLAUDE.md #8 in examples/lisp; same in the
|
|
* structlit-init path below. */
|
|
int b_isf32 = 0;
|
|
if (n->op == TK_ASSIGN
|
|
&& fld_isfloat(f->type, &b_isf32)) {
|
|
int mov = b_isf32 ? A_MOVSS : A_MOVSD;
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, foff));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str), areg(D_BX));
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, foff));
|
|
} else {
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP, boff + foff));
|
|
}
|
|
break;
|
|
}
|
|
/* now store AX into target */
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, foff));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, foff));
|
|
} else {
|
|
ins2(c, store_op, areg(D_AX), amem(D_BP, boff + foff));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric
|
|
* write-side of the cgdot N_INDEX-lhs branch. Compute &arr[i]
|
|
* inline (LEAQ for `[N]Struct`, MOVQ-load for `[N]*Struct` /
|
|
* `[]Struct` / `*Struct`), deref once when the element is
|
|
* `*Struct`, then store rhs at `field.offset(addr)`. The
|
|
* chained-pointer-field branch below catches `[N]*Struct`
|
|
* writes via its `!= N_IDENT` guard, but `[N]Struct` value-arrays
|
|
* fall through and silently drop the store. Placed before the
|
|
* `!= N_IDENT` branch so both shapes share one path. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
|
|
&& n->lhs->lhs->kind == N_INDEX) {
|
|
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. #133-expanded: all 10 integer
|
|
* compound ops wired; SLASHEQ/PERCENTEQ via
|
|
* CQO+IDIV (signed) or zero-DX+DIV (unsigned);
|
|
* LSHIFTEQ via SHLQ on CX; RSHIFTEQ via SARQ
|
|
* (signed) or SHRQ (unsigned) on CX per #136.
|
|
* Float / str / slice / tagged element compound
|
|
* hard-errors LOUD (rule-7). */
|
|
{
|
|
int compound_isf32 = 0;
|
|
if (fld_isfloat(ft, &compound_isf32))
|
|
fatal("chained-ptr-field compound on "
|
|
"float element not wired "
|
|
"(#133/rule-7); field='%s'",
|
|
n->lhs->str);
|
|
Type *fchk = type_chase_named(ft);
|
|
if (fchk && fchk->kind == TY_STR)
|
|
fatal("chained-ptr-field compound on "
|
|
"str element not wired "
|
|
"(#133/rule-7); field='%s'",
|
|
n->lhs->str);
|
|
if (fchk && fchk->kind == TY_SLICE)
|
|
fatal("chained-ptr-field compound on "
|
|
"slice element not wired "
|
|
"(#133/rule-7); field='%s'",
|
|
n->lhs->str);
|
|
if (fchk && fchk->kind == TY_TAGGED)
|
|
fatal("chained-ptr-field compound on "
|
|
"tagged element not wired "
|
|
"(#133/rule-7); field='%s'",
|
|
n->lhs->str);
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
int load_op = fldloadop(ft, fsz);
|
|
ins2(c, load_op, amem(D_AX, foff),
|
|
areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
int unsignd = type_isunsigned(ft);
|
|
switch (n->op) {
|
|
case TK_PLUSEQ:
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_MINUSEQ:
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_STAREQ:
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_AMPEQ:
|
|
ins2(c, A_ANDQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_PIPEEQ:
|
|
ins2(c, A_ORQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_CARETEQ:
|
|
ins2(c, A_XORQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_SLASHEQ:
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ,
|
|
areg(D_CX));
|
|
break;
|
|
case TK_PERCENTEQ:
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ,
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_LSHIFTEQ:
|
|
ins2(c, A_SHLQ, areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_RSHIFTEQ:
|
|
ins2(c, unsignd ? A_SHRQ : A_SARQ,
|
|
areg(D_CX), areg(D_AX));
|
|
break;
|
|
default:
|
|
fatal("chained-ptr-field compound: "
|
|
"unknown op tk=%d (#133/rule-7); "
|
|
"field='%s'", n->op,
|
|
n->lhs->str);
|
|
}
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_BX, foff));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* Chained `<chain>.field = v` where <chain> spans value-struct
|
|
* dots ending at a root ident — `o.i.a = 10`, `v.a.b.c = …`.
|
|
* Also handles a slice/str pseudo-field leaf (`b.buf.len = 5`):
|
|
* spine walks down to the slice/str header, then the +0/+8/+16
|
|
* delta selects ptr/len/cap. Sibling of the chained-pointer-
|
|
* field branch above; without this the LHS is silently dropped
|
|
* (the existing 1-deep branch only fires for `ident.field = …`).
|
|
* Only plain `=` is wired — compound on a chained value-struct
|
|
* field is rare and stays unhandled. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
|
|
&& n->lhs->lhs->kind == N_DOT && n->op == TK_ASSIGN) {
|
|
struct { Type *pu; const char *name; } steps[16];
|
|
int nsteps = 0;
|
|
int ptr_root = 0;
|
|
Node *cur = n->lhs;
|
|
int abort = 0;
|
|
while (cur && cur->kind == N_DOT && cur->lhs) {
|
|
Type *pt = cur->lhs->type;
|
|
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 = cg_tagscr_slot(c, &locals, ssz);
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < ssz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
cg_widen_tagged_store(c, &locals, esubu,
|
|
n->rhs, D_BP, scr, ssz);
|
|
/* Compute &arr[i] → BX. */
|
|
cgexpr(c, n->lhs->rhs, locals);
|
|
if (ssz > 1) {
|
|
ins2(c, A_MOVQ, aimm(ssz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
if (base->kind == N_IDENT && is_arr) {
|
|
int boff = localfind(locals, base->str);
|
|
ins2(c, A_LEAQ, amem(D_BP, boff),
|
|
areg(D_BX));
|
|
} else if (base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, boff),
|
|
areg(D_BX));
|
|
} else if (cg_dotbase_addr(c, base, D_BX, locals)) {
|
|
/* #259: N_DOT base resolved inline to the
|
|
* field address; cgexpr fallback would
|
|
* auto-deref + load the array field as a
|
|
* VALUE (the broken shape). dst BX keeps the
|
|
* scaled index live in AX (spill contract). */
|
|
} else {
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, base, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
/* Copy scratch slot → dest. */
|
|
for (int k = 0; k < ssz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BX, k));
|
|
}
|
|
break;
|
|
}
|
|
/* #234: over-cap sret STORE into an indexed lvalue —
|
|
* `arr[i] = wide();` STORE-twin of the Fold-B sret RECEIVE
|
|
* (a937d67). cg_sret_dest_off is a STATIC BP-relative
|
|
* offset, so only a CONSTANT index into a LOCAL value array
|
|
* yields a static dest slot (boff + idx*esz) the callee can
|
|
* sret straight into. Every other indexed form — runtime
|
|
* index, slice/ptr base, global base — needs the runtime
|
|
* RDI-pointer dest variant deferred to #234-tail and HARD-
|
|
* STOPS loud (rule 7 — never the truncating store below). */
|
|
if (n->op == TK_ASSIGN && n->rhs
|
|
&& n->rhs->kind == N_CALL
|
|
&& esub && cg_sret_retsize(esub) > 0) {
|
|
int cidx = (n->lhs->rhs
|
|
&& n->lhs->rhs->kind == N_INTLIT)
|
|
? (int)n->lhs->rhs->uval : -1;
|
|
int sboff = (base->kind == N_IDENT)
|
|
? localfind(locals, base->str) : 0;
|
|
if (!is_arr || cidx < 0 || sboff == 0)
|
|
fatal("#234-tail: over-cap tuple sret "
|
|
"store to non-local dest "
|
|
"unsupported");
|
|
cg_sret_dest_off = sboff + cidx * esz;
|
|
cgexpr(c, n->rhs, locals);
|
|
cg_sret_dest_off = 0;
|
|
break;
|
|
}
|
|
/* #270-1b: aggregate (struct/array/tuple >8B) element
|
|
* STORE `a[i] = val`. The scalar store path below copies
|
|
* only the first 8 bytes (fldstoreop MOVQ) — a silent
|
|
* truncation. Compute &a[i] (dest) and the rhs SOURCE
|
|
* address, then word-copy esz bytes: the WRITE-twin of the
|
|
* #268 let-init copy loop. Source shapes mirror that loop
|
|
* (ident local/global, N_DOT field via cg_dotchain_addr,
|
|
* `*p` deref); a by-value call result is the deferred #271,
|
|
* so N_CALL/literal sources fall through unchanged. */
|
|
if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN
|
|
&& esubu && (esubu->kind == TY_STRUCT
|
|
|| esubu->kind == TY_ARRAY
|
|
|| esubu->kind == TY_TUPLE)
|
|
&& esz > 8
|
|
&& ((n->rhs->kind == N_IDENT)
|
|
|| (n->rhs->kind == N_DOT)
|
|
|| (n->rhs->kind == N_UN
|
|
&& n->rhs->op == TK_STAR))) {
|
|
/* dest &a[i] → BX */
|
|
cgexpr(c, n->lhs->rhs, locals); /* idx → AX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */
|
|
if (base->kind == N_IDENT) {
|
|
int off = localfind(locals, base->str);
|
|
int isglobal = (off == 0)
|
|
&& let_islet(base->str);
|
|
if (isglobal && is_arr)
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
else if (isglobal)
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
else if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off), areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off), areg(D_BX));
|
|
} else if (cg_dotbase_addr(c, base, D_BX, locals)) {
|
|
/* N_DOT array-field base resolved inline. */
|
|
} else {
|
|
cgexpr(c, base, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* spill dest */
|
|
/* rhs source address → SI */
|
|
if (n->rhs->kind == N_UN
|
|
&& n->rhs->op == TK_STAR) {
|
|
cgexpr(c, n->rhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_SI));
|
|
} else if (n->rhs->kind == N_IDENT) {
|
|
int soff = localfind(locals,
|
|
n->rhs->str);
|
|
if (soff != 0)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, soff),
|
|
areg(D_SI));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, n->rhs->str),
|
|
areg(D_SI));
|
|
} else {
|
|
cg_dotchain_addr(c, n->rhs, D_SI, locals);
|
|
}
|
|
ins1(c, A_POPQ, areg(D_BX)); /* dest */
|
|
int k = 0;
|
|
for (; k + 8 <= esz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BX, k));
|
|
}
|
|
if (k + 4 <= esz) {
|
|
ins2(c, A_MOVL, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 4;
|
|
}
|
|
if (k + 2 <= esz) {
|
|
ins2(c, A_MOVW, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVW, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 2;
|
|
}
|
|
if (k + 1 <= esz) {
|
|
ins2(c, A_MOVB, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 1;
|
|
}
|
|
break;
|
|
}
|
|
if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN) {
|
|
cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len,CX=cap if str) */
|
|
/* str/slice: stash cap+len so all three store
|
|
* (#1/Phase 3). */
|
|
if (elem_is_str || elem_is_slice) {
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
}
|
|
/* Float element: spill X0 (not AX — AX is junk
|
|
* for floats) across the idx/base eval. A call-
|
|
* index (`a[geti()]=v`) clobbers X0 and would
|
|
* otherwise lose the value. Mirrors the *p=v
|
|
* float deref store at cgen.c:4187 (#125). */
|
|
int sp_isfloat = type_isfloat(esub);
|
|
int sp_mov = sp_isfloat
|
|
? (type_isf32(esub) ? A_MOVSS : A_MOVSD) : 0;
|
|
if (sp_isfloat) {
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, sp_mov, areg(D_X0), amem(D_SP, 0));
|
|
} else {
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
cgexpr(c, n->lhs->rhs, locals); /* idx → AX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */
|
|
/* base address → BX. Top-level array → LEAQ
|
|
* name(SB); top-level ptr → MOVQ name(SB); locals
|
|
* route off BP. */
|
|
if (base->kind == N_IDENT) {
|
|
int off = localfind(locals, base->str);
|
|
int isglobal = (off == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal && is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
} else if (is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
}
|
|
} else if (cg_dotbase_addr(c, base, D_BX, locals)) {
|
|
/* #135 site: N_DOT base resolved inline to
|
|
* the field address; cgexpr fallback below
|
|
* would auto-deref + load the field as a
|
|
* VALUE (the broken shape). */
|
|
} else {
|
|
cgexpr(c, base, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
/* Reload value: float reloads X0 from the spill
|
|
* slot; non-float pops AX. Twin of the value-spill
|
|
* site above (#125). */
|
|
if (sp_isfloat) {
|
|
ins2(c, sp_mov, amem(D_SP, 0), areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
} else {
|
|
ins1(c, A_POPQ, areg(D_AX)); /* value (ptr if str) */
|
|
}
|
|
if (elem_is_str || elem_is_slice) {
|
|
/* str/slice: store ptr/len/cap (#1/Phase 3, #7). */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16));
|
|
break;
|
|
}
|
|
/* float element → store FROM X0 (MOVSS/MOVSD): cgexpr
|
|
* leaves a float value in X0, and for f32 the #104
|
|
* CVTSD2SS narrowing only touches X0 — the AX path
|
|
* below would store the raw double low-bits (garbage
|
|
* for f32). Float-ness from esub, mirroring the read
|
|
* side at cgen.c:6423 (#122). #125: the value-spill
|
|
* pair above keeps X0 live across the idx/base eval
|
|
* so this MOVSS/MOVSD is correct even on call-index
|
|
* shapes. */
|
|
if (type_isfloat(esub)) {
|
|
int mov = type_isf32(esub) ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, 0));
|
|
break;
|
|
}
|
|
int store_op = fldstoreop(esub, esz);
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
break;
|
|
}
|
|
/* Compound assign on an indexed scalar element
|
|
* (`arr[i] OP= v`). Pre-#133 this branch had no TK_ASSIGN
|
|
* gate above and silently DEMOTED compound ops to plain
|
|
* stores (no load, no op). Mirror the chained-pointer-
|
|
* field compound template at cgen.c:3281-3317: same
|
|
* address computation as the ASSIGN body above, then
|
|
* load_op (BX)→AX, pop rhs→CX, combine, store_op.
|
|
* #133-expanded: all 10 integer compound ops wired;
|
|
* float/str/slice/tagged element compound HARD-ERRORS
|
|
* loud (rule-7, replaces prior silent fall-through).
|
|
* #136: signed RSHIFTEQ now uses A_SARQ (arithmetic
|
|
* shift). */
|
|
if ((is_arr || is_sl || is_ptr) && n->op != TK_ASSIGN) {
|
|
if (elem_is_str)
|
|
fatal("indexed-lvalue compound on "
|
|
"str element not wired "
|
|
"(#133/rule-7)");
|
|
if (elem_is_slice)
|
|
fatal("indexed-lvalue compound on "
|
|
"slice element not wired "
|
|
"(#133/rule-7)");
|
|
if (elem_tagged)
|
|
fatal("indexed-lvalue compound on "
|
|
"tagged element not wired "
|
|
"(#133/rule-7)");
|
|
if (esub && type_isfloat(esub))
|
|
fatal("indexed-lvalue compound on "
|
|
"float element not wired "
|
|
"(#133/rule-7)");
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->rhs, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (base->kind == N_IDENT) {
|
|
int off = localfind(locals, base->str);
|
|
int isglobal = (off == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal && is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
} else if (is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
}
|
|
} else if (cg_dotbase_addr(c, base, D_BX, locals)) {
|
|
/* #135 site: N_DOT base resolved inline to the
|
|
* field address. */
|
|
} else {
|
|
cgexpr(c, base, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
int load_op = fldloadop(esub, esz);
|
|
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
int unsignd_c = esub && type_isunsigned(esub);
|
|
switch (n->op) {
|
|
case TK_PLUSEQ:
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_MINUSEQ:
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_STAREQ:
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_AMPEQ:
|
|
ins2(c, A_ANDQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_PIPEEQ:
|
|
ins2(c, A_ORQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_CARETEQ:
|
|
ins2(c, A_XORQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_SLASHEQ:
|
|
if (unsignd_c)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd_c ? A_DIVQ : A_IDIVQ,
|
|
areg(D_CX));
|
|
break;
|
|
case TK_PERCENTEQ:
|
|
if (unsignd_c)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd_c ? A_DIVQ : A_IDIVQ,
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
case TK_LSHIFTEQ:
|
|
ins2(c, A_SHLQ, areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_RSHIFTEQ:
|
|
ins2(c, unsignd_c ? A_SHRQ : A_SARQ,
|
|
areg(D_CX), areg(D_AX));
|
|
break;
|
|
default:
|
|
fatal("indexed-lvalue compound: "
|
|
"unknown op tk=%d (#133/rule-7)",
|
|
n->op);
|
|
}
|
|
int store_op_c = fldstoreop(esub, esz);
|
|
ins2(c, store_op_c, areg(D_AX), amem(D_BX, 0));
|
|
break;
|
|
}
|
|
}
|
|
/* Plain `r = expr;` where r is a tagged-union local.
|
|
* Delegates to cg_widen_tagged_store: covers nullable fold,
|
|
* tagged→tagged (with tag remap), struct payload (ident or
|
|
* literal), str payload, and scalar payload.
|
|
*
|
|
* #38b: an sret-classified tagged CALL result is in memory,
|
|
* not the cursor — an exact-type reassign falls through to
|
|
* the generic sret receive below; a widening receive needs
|
|
* mem-to-mem tag-remap (#40, unwired). */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
|
|
&& n->lhs->type) {
|
|
Type *lt = n->lhs->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
if (lu && lu->kind == TY_TAGGED) {
|
|
int rhs_sret_call = n->rhs
|
|
&& n->rhs->kind == N_CALL
|
|
&& cg_sret_retsize(n->rhs->type) > 0;
|
|
if (!rhs_sret_call) {
|
|
int off = localfind(locals,
|
|
n->lhs->str);
|
|
if (off == 0) break;
|
|
cg_widen_tagged_store(c, &locals, lu,
|
|
n->rhs, D_BP, off, (int)lu->size);
|
|
break;
|
|
}
|
|
Type *ru = type_chase_named(n->rhs->type);
|
|
if (!(ru == lu || type_eq(n->rhs->type, lt)))
|
|
fatal("#40: sret-class call result "
|
|
"cannot be widened into a tagged "
|
|
"slot (mem-to-mem widen unwired)");
|
|
if (localfind(locals, n->lhs->str) == 0)
|
|
fatal("#38b: sret receive into a "
|
|
"tagged GLOBAL lvalue unwired");
|
|
}
|
|
}
|
|
/* Deref-target assignment `*p = v;`. The size of the store is
|
|
* determined by the type *p points at; the pointer expression
|
|
* is evaluated after the value so we don't need to spill BX. */
|
|
if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR
|
|
&& n->op == TK_ASSIGN) {
|
|
Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL;
|
|
Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
|
|
Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL;
|
|
if (vt && vt->kind == TY_NAMED) vt = vt->under;
|
|
/* `*p = v` for *f64 / *f32: cgexpr leaves the value in X0,
|
|
* not AX. Spill X0 to the stack, evaluate the pointer
|
|
* (clobbers AX/BX freely), then reload X0 and MOVSD/MOVSS
|
|
* through the pointer. */
|
|
int deref_isf32 = 0;
|
|
if (vt && fld_isfloat(vt, &deref_isf32)) {
|
|
int mov = deref_isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, 0));
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len, CX=cap if str/slice) */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (vt && (vt->kind == TY_STR || vt->kind == TY_SLICE)) {
|
|
/* str IS []u8 and a slice is the same 3-word
|
|
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8):
|
|
* stash len + cap across the pointer eval, which
|
|
* clobbers BX/CX (#1/Phase 3; slice arm #79). */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
}
|
|
cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
if (vt && (vt->kind == TY_STR || vt->kind == TY_SLICE)) {
|
|
ins1(c, A_POPQ, areg(D_CX)); /* cap */
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16));
|
|
ins1(c, A_POPQ, areg(D_CX)); /* len */
|
|
ins1(c, A_POPQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
|
|
} else {
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
int sz = vt ? (int)vt->size : 8;
|
|
int store_op = fldstoreop(vt, sz);
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* `*p OP= v` — compound assign through a pointer deref. The
|
|
* plain-assign branch above only fires for TK_ASSIGN; without
|
|
* this, compound ops fall through the switch and emit nothing
|
|
* (silent no-op). Evaluate rhs → save, evaluate ptr → BX, load
|
|
* *BX (sized + extended), combine with rhs in CX, sized store
|
|
* back. Scalar deref targets only — float and aggregate deref
|
|
* compounds (rare) still fall through. */
|
|
if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR
|
|
&& n->op != TK_ASSIGN) {
|
|
Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL;
|
|
Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
|
|
Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL;
|
|
if (vt && vt->kind == TY_NAMED) vt = vt->under;
|
|
int sz = vt ? (int)vt->size : 8;
|
|
int load_op = fldloadop(vt, sz);
|
|
int store_op = fldstoreop(vt, sz);
|
|
int handled = (sz == 1 || sz == 2 || sz == 4 || sz == 8);
|
|
if (handled) {
|
|
cgexpr(c, n->rhs, locals); /* AX = rhs */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals); /* AX = ptr */
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_LSHIFTEQ: ins2(c, A_SHLQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_RSHIFTEQ: {
|
|
/* #136: signed RSHIFTEQ → SARQ. */
|
|
int unsignd = (vt && type_isunsigned(vt))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
ins2(c, unsignd ? A_SHRQ : A_SARQ,
|
|
areg(D_CX), areg(D_AX));
|
|
break;
|
|
}
|
|
case TK_SLASHEQ:
|
|
case TK_PERCENTEQ: {
|
|
int unsignd = (vt && type_isunsigned(vt))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_CX));
|
|
if (n->op == TK_PERCENTEQ)
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
default:
|
|
/* unknown compound: legacy fallback —
|
|
* store rhs only. */
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
}
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
break;
|
|
}
|
|
}
|
|
/* `name = expr;` reassignment of a str/slice/struct local or
|
|
* top-level let. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
|
|
&& n->lhs->type) {
|
|
Type *lt = n->lhs->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
/* str/slice local/let: str IS []u8, so both store the full
|
|
* 3-word {ptr,len,cap} from (AX,BX,CX) at off+0/+8/+16
|
|
* (local) or via &name(SB) → DI scratch (global — CX holds
|
|
* the cap, and the asm has no `name+8(SB)` operand form, so
|
|
* a different address register is needed) (#1/Phase 3). */
|
|
if (lu && (lu->kind == TY_SLICE || lu->kind == TY_STR)) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off != 0) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
if (let_islet(n->lhs->str)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_DI));
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, 8));
|
|
ins2(c, A_MOVQ, areg(D_DI), amem(D_CX, 16));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
/* sret receive (#23 / #10 Fold B): `s = f();` where s's
|
|
* own slot IS the caller-prealloc dest; the callee writes
|
|
* through hidden RDI. Mirrors the cglet branch above and
|
|
* keys on cg_sret_retsize (the shared sret SSoT), NOT a
|
|
* kind — so an over-cap tuple reassign materialises its
|
|
* whole slot exactly like a >24B struct. */
|
|
if (cg_sret_retsize(lt) > 0
|
|
&& n->rhs && n->rhs->kind == N_CALL
|
|
&& n->op == TK_ASSIGN) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off != 0) {
|
|
cg_sret_dest_off = off;
|
|
cgexpr(c, n->rhs, locals);
|
|
cg_sret_dest_off = 0;
|
|
break;
|
|
}
|
|
/* #220: `g = f();` where g is a GLOBAL struct >24B.
|
|
* No BP slot to use as the sret dest, so route RDI
|
|
* to g's symbol address. Mirrors the str/slice
|
|
* global arm above (let_islet + LEAQ masym). The
|
|
* scalar fall-through below would emit a truncated
|
|
* 8-byte `MOVQ AX, g(SB)` and drop the struct body.
|
|
* Kept aggregate-only (struct + #272 array): a
|
|
* tuple-typed global reassign has no sret-to-symbol
|
|
* path in wwstage either, so leaving it to fall
|
|
* through keeps the stages aligned (rule-10). */
|
|
if (lu && (lu->kind == TY_STRUCT
|
|
|| lu->kind == TY_ARRAY)
|
|
&& let_islet(n->lhs->str)) {
|
|
cg_sret_dest_sym = n->lhs->str;
|
|
cgexpr(c, n->rhs, locals);
|
|
cg_sret_dest_sym = NULL;
|
|
break;
|
|
}
|
|
}
|
|
/* 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 || lu->kind == TY_ARRAY)
|
|
&& (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;
|
|
}
|
|
} else if (lu->kind == TY_ARRAY
|
|
&& let_islet(n->lhs->str)
|
|
&& n->rhs && n->rhs->kind == N_CALL
|
|
&& n->op == TK_ASSIGN) {
|
|
/* #272: `g = f();` where g is a GLOBAL
|
|
* aggregate ≤24B. The callee leaves the result
|
|
* in AX/DX/CX (#272 reg-return); the scalar IDENT
|
|
* fall-through below would store only MOVQ AX,
|
|
* g(SB) = the first word. The asm has no `g+8(SB)`
|
|
* operand form, so LEAQ the symbol into DI and
|
|
* store the full+tail words. Mirrors the str/slice
|
|
* global arm above and the #220 sret-to-symbol path.
|
|
* #276: this arm is TY_ARRAY-only — a ≤24B STRUCT
|
|
* global receive can be float-class (X0/X1, not
|
|
* AX/DX/CX) so it stays at its pre-existing symmetric
|
|
* fall-through; closing it needs struct_float_class
|
|
* here. No consumer. Arrays are never float-class, so
|
|
* AX/DX/CX is always correct for this arm. */
|
|
int sz = (int)lu->size;
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_DI));
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = sz / 8;
|
|
int tail = sz % 8;
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(D_DI, i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW
|
|
: A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(D_DI, full * 8));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off == 0) {
|
|
/* Top-level let target — RIP-relative store
|
|
* (or load→combine→store for compound). Names
|
|
* we don't recognise as scalar lets fall through
|
|
* to the existing drop behaviour, which produces
|
|
* a clean link-time undefined-symbol error if
|
|
* the binding was ever supposed to exist. */
|
|
if (!let_islet(n->lhs->str)) break;
|
|
cgexpr(c, n->rhs, locals);
|
|
if (n->op == TK_ASSIGN) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
masym(c, n->lhs->str));
|
|
break;
|
|
}
|
|
/* Compound: BX = load; combine with AX; store
|
|
* BX. The asm has no RIP-relative ADDQ/SUBQ
|
|
* mem-form, so we use the explicit load→
|
|
* combine→store sequence uniformly. Narrow
|
|
* lets go through LEAQ + indirect load with
|
|
* localloadop so a prior `*(&letname): *iN`
|
|
* deref-store doesn't leave stale upper bytes
|
|
* in the read. */
|
|
int glop = localloadop(n->lhs->type);
|
|
if (glop == A_MOVQ) {
|
|
ins2(c, A_MOVQ, masym(c, n->lhs->str),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, glop, amem(D_CX, 0),
|
|
areg(D_BX));
|
|
}
|
|
int did_compound = 1;
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_LSHIFTEQ:
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, A_SHLQ, areg(D_CX), areg(D_BX));
|
|
break;
|
|
case TK_RSHIFTEQ: {
|
|
/* #136: signed RSHIFTEQ → SARQ. */
|
|
int unsignd_r = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, unsignd_r ? A_SHRQ : A_SARQ,
|
|
areg(D_CX), areg(D_BX));
|
|
break;
|
|
}
|
|
case TK_SLASHEQ:
|
|
case TK_PERCENTEQ: {
|
|
/* Sister site of the IDENT-local path
|
|
* below. Park rhs (AX) in CX, slot value
|
|
* (BX) into AX, CQO sign-extend (or
|
|
* MOVQ $0, DX zero-extend), IDIVQ (or
|
|
* DIVQ) CX, ferry AX (quotient) or DX
|
|
* (remainder) back to BX for the shared
|
|
* store-BX tail. */
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_CX));
|
|
if (n->op == TK_SLASHEQ)
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_BX));
|
|
break;
|
|
}
|
|
default:
|
|
/* unknown compound: legacy fallback —
|
|
* store rhs only. */
|
|
did_compound = 0;
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
masym(c, n->lhs->str));
|
|
break;
|
|
}
|
|
if (did_compound)
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
masym(c, n->lhs->str));
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
if (n->op == TK_ASSIGN) {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
|
|
break;
|
|
}
|
|
/* Compound: load → combine into BX → store. The two
|
|
* direct mem-form combines (ADDQ/SUBQ) are kept for
|
|
* the simple cases; the rest go through the generic
|
|
* register form. Signed-narrow slots take the explicit
|
|
* load-combine-store path so the load can sign-extend
|
|
* through localloadop — ADDQ/SUBQ on amem would read
|
|
* the raw 8B, which is wrong when the slot was last
|
|
* written by a 4B deref-store. */
|
|
int lop = localloadop(n->lhs->type);
|
|
if (lop == A_MOVQ && n->op == TK_PLUSEQ) {
|
|
ins2(c, A_ADDQ, areg(D_AX), amem(D_BP, off));
|
|
break;
|
|
}
|
|
if (lop == A_MOVQ && n->op == TK_MINUSEQ) {
|
|
ins2(c, A_SUBQ, areg(D_AX), amem(D_BP, off));
|
|
break;
|
|
}
|
|
ins2(c, lop, amem(D_BP, off), areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_LSHIFTEQ:
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, A_SHLQ, areg(D_CX), areg(D_BX));
|
|
break;
|
|
case TK_RSHIFTEQ: {
|
|
/* #136: signed RSHIFTEQ → SARQ. */
|
|
int unsignd_r = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, unsignd_r ? A_SHRQ : A_SARQ,
|
|
areg(D_CX), areg(D_BX));
|
|
break;
|
|
}
|
|
case TK_SLASHEQ:
|
|
case TK_PERCENTEQ: {
|
|
/* IDIV/DIV needs dividend in RDX:RAX, divisor
|
|
* in a GPR. Park rhs (currently AX) in CX, move
|
|
* slot value (BX) into AX, sign- or zero-extend
|
|
* into RDX:RAX, divide, then ferry the quotient
|
|
* (AX) or remainder (DX) back into BX for the
|
|
* shared store-BX-to-slot tail below. Post-#16:
|
|
* CQO is now in the assembler. */
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
if (unsignd)
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
else
|
|
ins0(c, A_CQO);
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_CX));
|
|
if (n->op == TK_SLASHEQ)
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_BX));
|
|
break;
|
|
}
|
|
default:
|
|
/* unknown: just store rhs (legacy fallback) */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
|
|
goto skip_assign_store;
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off));
|
|
skip_assign_store: ;
|
|
}
|
|
break;
|
|
}
|
|
case N_CALL: {
|
|
/* abort([msg]) — call rt_abort. Empty msg becomes (NULL, 0).
|
|
* Only fires when the checker tagged the callee as a builtin
|
|
* (lhs->type == ty_err); a user-declared `abort` in scope is
|
|
* resolved through the regular call path. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
n->lhs->type == ty_err &&
|
|
strcmp(n->lhs->str, "abort") == 0) {
|
|
if (n->list) {
|
|
cgexpr(c, n->list, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
|
|
} else {
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
|
|
}
|
|
ins1(c, A_CALL, asym("rt_abort"));
|
|
break;
|
|
}
|
|
/* assert(cond[, msg]) — if !cond, call rt_abort. Compiles to:
|
|
* CMPQ $0, AX
|
|
* JNE skip
|
|
* <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);
|
|
if (off == 0 && let_islet(a->str)) {
|
|
/* #231: str/slice GLOBAL — the .len word
|
|
* lives at the global's address+8, not a
|
|
* BP-relative slot (off==0 → MOVQ 8(BP)
|
|
* read a bogus stack slot). Route through
|
|
* the post-#1 value mangle so a private
|
|
* same-module same-leaf global isn't
|
|
* mis-resolved. */
|
|
ins2(c, A_LEAQ,
|
|
mahint(c, a->str, c->cur_mod),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 8),
|
|
areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8),
|
|
areg(D_AX));
|
|
}
|
|
} else if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
|
|
&& a->kind == N_DOT && a->lhs
|
|
&& a->lhs->kind == N_IDENT && a->str) {
|
|
/* #235: len() of a tuple-element slice/str
|
|
* (`len(t.N)`). The tuple-element read leaves only
|
|
* AX=.ptr — it has no slice-header sibling (that
|
|
* gap is #238) — so the bare cgexpr fallback below
|
|
* returned .ptr AS the length. Load the element's
|
|
* .len word directly at BP + element_off + 8,
|
|
* mirroring the N_IDENT slice arm above and the
|
|
* tuple-field-offset walk (cgen.c N_DOT TY_TUPLE). */
|
|
Type *bt = a->lhs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
if (bu && bu->kind == TY_TUPLE) {
|
|
int idx = 0;
|
|
for (const char *q = a->str; *q; q++)
|
|
idx = idx * 10 + (*q - '0');
|
|
Tparam *tp = bu->params;
|
|
int foff = 0;
|
|
while (idx > 0 && tp) {
|
|
if (tp->type)
|
|
foff += (int)tp->type->size;
|
|
tp = tp->next;
|
|
idx--;
|
|
}
|
|
int off = localfind(locals, a->lhs->str);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off + foff + 8),
|
|
areg(D_AX));
|
|
} else {
|
|
cgexpr(c, a, locals);
|
|
}
|
|
} else if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
|
|
&& a->kind == N_INDEX) {
|
|
/* #19: len() of an INDEXED str/slice element
|
|
* (`len(xs[i])`). The N_INDEX str/slice load leaves
|
|
* AX=.ptr, BX=.len, CX=.cap (cgslicehdr) — the bare
|
|
* cgexpr fallback below then returned AX (the ptr) AS
|
|
* the length. Shuffle BX (the len word) into AX, the
|
|
* same MOVQ BX,AX shape as the #14 .len pseudo-field
|
|
* fix. Same family as #18 (shared cstage==wwstage gap,
|
|
* not a rule-10 divergence). */
|
|
cgexpr(c, a, locals);
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
} 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) {
|
|
/* free(x) is a no-op: ww has no free by design
|
|
* (rt/alloc.s:30 — the bump allocator cannot reclaim
|
|
* a mid-chunk pointer; process exit does). The old
|
|
* CALL ffi_resolve("free") was an undefined reference
|
|
* unless an @symbol decl happened to be in scope (#27).
|
|
* The operand is still evaluated — Hare's free(expr)
|
|
* evaluates expr — so Hare code ports verbatim with
|
|
* its side effects intact. */
|
|
cgexpr(c, n->list, locals);
|
|
break;
|
|
}
|
|
if (n->lhs && n->lhs->kind == N_IDENT &&
|
|
n->lhs->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);
|
|
/* #34 element-kind store dispatch: the scalar 1-word
|
|
* store below silently gutted every wide element
|
|
* (str/slice 24B header, tagged box, struct body).
|
|
* Mirrors the #270/#12/#20 array-literal element
|
|
* dispatch (cg_arrlit_fill_bp). */
|
|
Type *esubu = type_chase_named(esub);
|
|
int el_str = type_isstr(esub);
|
|
int el_slice = type_isslice(esub);
|
|
int el_tagged = esubu && esubu->kind == TY_TAGGED;
|
|
int el_struct = esubu && esubu->kind == TY_STRUCT;
|
|
int el_wide = el_str || el_slice || el_tagged ||
|
|
el_struct;
|
|
if (!el_wide && esz > 8)
|
|
fatal("#34: append() element kind "
|
|
"unsupported (rule-7)");
|
|
for (Node *vn = sn->next; vn; vn = vn->next) {
|
|
/* #34 review: a spread whose source is not a
|
|
* local ident used to fall PAST the spread arm
|
|
* into the single-value stores with the
|
|
* N_SPREAD node (cstage garbage store; wwstage
|
|
* silently SKIPPED it — divergent). Deferred
|
|
* source shape, task #37. */
|
|
if (vn->kind == N_SPREAD) {
|
|
if (!vn->lhs || vn->lhs->kind != N_IDENT)
|
|
fatal("#34: append() spread source "
|
|
"shape unsupported (rule-7)");
|
|
if (localfind(locals, vn->lhs->str) == 0)
|
|
fatal("#34: append() spread source "
|
|
"ident is not a local (rule-7)");
|
|
}
|
|
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));
|
|
if (el_wide) {
|
|
/* #34: a spread element is already a
|
|
* fully-formed T in the source slice
|
|
* (tag included), so a whole-width
|
|
* word-copy is the store — no boxing.
|
|
* Grow FIRST: rt_ensure may realloc,
|
|
* so both addresses are recomputed
|
|
* from the slice headers after the
|
|
* call (i reloads from the counter
|
|
* slot; CX was clobbered). */
|
|
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_SP, 0), areg(D_CX));
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
|
|
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_DX));
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_DX));
|
|
int k = 0;
|
|
for (; k + 8 <= esz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BX, k), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, k));
|
|
}
|
|
if (k + 4 <= esz) {
|
|
ins2(c, A_MOVL, amem(D_BX, k), areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX), amem(D_DX, k));
|
|
k += 4;
|
|
}
|
|
if (k + 2 <= esz) {
|
|
ins2(c, A_MOVW, amem(D_BX, k), areg(D_AX));
|
|
ins2(c, A_MOVW, areg(D_AX), amem(D_DX, k));
|
|
k += 2;
|
|
}
|
|
if (k + 1 <= esz) {
|
|
ins2(c, A_MOVB, amem(D_BX, k), areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX), amem(D_DX, k));
|
|
k += 1;
|
|
}
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_SP, 0));
|
|
ins1(c, A_JMP, abranch(ll));
|
|
label(c, le);
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
continue;
|
|
}
|
|
/* AX = items.ptr[i] */
|
|
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;
|
|
}
|
|
if (el_str || el_slice) {
|
|
/* #34: 24B {ptr,len,cap} header. cgexpr
|
|
* leaves AX/BX/CX; all three must survive
|
|
* rt_ensure. dst lands in DX, NOT BX — the
|
|
* pops put the element .len back in BX
|
|
* (the #24 register discipline). */
|
|
cgexpr(c, vn, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_CX));
|
|
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));
|
|
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_DX));
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_DX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, 16));
|
|
continue;
|
|
}
|
|
if (el_tagged || el_struct) {
|
|
/* #34: no register form survives rt_ensure
|
|
* for these — grow FIRST, then fill through
|
|
* the dst pointer (tagged: the #12 widen
|
|
* choke-point cgexprs the value internally;
|
|
* struct: literal fill / ident word-copy). */
|
|
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));
|
|
if (el_tagged) {
|
|
cg_widen_tagged_store(c, &locals,
|
|
esub, vn, D_BX, 0, esz);
|
|
continue;
|
|
}
|
|
if (vn->kind == N_STRUCTLIT) {
|
|
if (cg_appendscr == 0)
|
|
cg_appendscr = local_alloc(c,
|
|
&locals, "@appendscr", 8,
|
|
cg_frame);
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, cg_appendscr));
|
|
cg_structlit_fill(c, &locals, esubu,
|
|
vn, DST_PTR_LOCAL, cg_appendscr,
|
|
NULL, 0);
|
|
continue;
|
|
}
|
|
if (vn->kind == N_IDENT) {
|
|
int soff = localfind(locals, vn->str);
|
|
if (soff == 0)
|
|
fatal("#34: append() struct "
|
|
"element source ident is "
|
|
"not a local (rule-7)");
|
|
int k = 0;
|
|
for (; k + 8 <= esz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, k));
|
|
}
|
|
if (k + 4 <= esz) {
|
|
ins2(c, A_MOVL, amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX), amem(D_BX, k));
|
|
k += 4;
|
|
}
|
|
if (k + 2 <= esz) {
|
|
ins2(c, A_MOVW, amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, A_MOVW, areg(D_AX), amem(D_BX, k));
|
|
k += 2;
|
|
}
|
|
if (k + 1 <= esz) {
|
|
ins2(c, A_MOVB, amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX), amem(D_BX, k));
|
|
k += 1;
|
|
}
|
|
continue;
|
|
}
|
|
fatal("#34: append() struct element source "
|
|
"shape unsupported (rule-7)");
|
|
}
|
|
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;
|
|
/* esz from the type table for an N_IDENT base
|
|
* (#76) or an N_DOT array/slice-field base
|
|
* (#257: scale by the field's element width via
|
|
* the checker-stamped base->type, not esz=1 --
|
|
* silently wrong for non-u8). Other non-ident
|
|
* bases stay esz=1 (unscaled). */
|
|
int esz = (base && (base->kind == N_IDENT
|
|
|| base->kind == N_DOT
|
|
|| base->kind == N_ARRLIT)
|
|
&& bu && bu->sub)
|
|
? (int)bu->sub->size : 1;
|
|
/* base addr → push */
|
|
if (base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal && bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
|
|
}
|
|
} else if (cg_dotbase_addr(c, base, D_AX, locals)) {
|
|
/* #257: N_DOT `[N]T`-field base as a call
|
|
* arg → field ADDRESS (LEAQ), not the
|
|
* auto-deref VALUE load cgexpr emits. Same
|
|
* choke-point as the cgslice #252 site;
|
|
* `[]T`/str/`*T` fields fall through to
|
|
* cgexpr (correct header/ptr load). */
|
|
} else {
|
|
cgexpr(c, base, locals);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
/* hi (default base length) → push */
|
|
if (hi) cgexpr(c, hi, locals);
|
|
else if (bu && bu->kind == TY_ARRAY)
|
|
cgexpr_int(c, (long long)bu->alen);
|
|
else if (base->kind == N_IDENT && bu &&
|
|
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_CX, 8), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
/* lo (default 0) → AX */
|
|
if (lo) cgexpr(c, lo, locals);
|
|
else cgexpr_int(c, 0);
|
|
ins1(c, A_POPQ, areg(D_BX)); /* hi */
|
|
ins1(c, A_POPQ, areg(D_CX)); /* base */
|
|
/* len = hi - lo (DX) */
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_DX));
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_DX));
|
|
/* ptr = base + lo*esz (#76; ensure.ha:30
|
|
* membsz-unit). BX=lo*esz; AX=lo PRESERVED
|
|
* for cap. BX (dead hi) reloaded by cap below. */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_BX), areg(D_CX));
|
|
} else {
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
/* push cap, len, ptr (top). cap = base_cap - lo
|
|
* (#20); AX=lo, BX free. */
|
|
if (cg_base_cap(c, base, bu, locals, D_BX)) {
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* cap */
|
|
} else {
|
|
ins1(c, A_PUSHQ, areg(D_DX)); /* cap = len */
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_DX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* ptr */
|
|
continue;
|
|
}
|
|
if (!widen[i] && node_isstructarg(args[i]) && args[i]->kind == N_IDENT) {
|
|
/* load qword(s) directly from the struct's slot */
|
|
int off = localfind(locals, args[i]->str);
|
|
int sz = struct_arg_size(args[i]->type);
|
|
if (sz > 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
continue;
|
|
}
|
|
if (!widen[i] && node_istaggedarg(args[i]) && args[i]->kind == N_IDENT) {
|
|
/* Tagged-union: push each 8B word from the slot.
|
|
* High word goes first so the popper drains them
|
|
* in low→high order into the arg-register class. */
|
|
int off = localfind(locals, args[i]->str);
|
|
int sz = tagged_arg_size(args[i]->type);
|
|
int nwords = sz / 8;
|
|
for (int k = nwords - 1; k >= 0; k--) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + k*8),
|
|
areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
continue;
|
|
}
|
|
/* #271: aggregate (struct/array) arg from any source the
|
|
* ≤16B-struct-IDENT fast path above doesn't cover — a
|
|
* 16B struct from a non-ident source, OR any array, OR a
|
|
* struct > 16B. The arg twin of the #265/#268 let-init
|
|
* copy: materialise the source's ADDRESS in SI and push
|
|
* its ceil(sz/8) words high→low (the pop drains word0
|
|
* into the first arg reg). A CALL source receives first —
|
|
* ≤24B in AX/DX/CX pushed straight, >24B sret'd into
|
|
* @aggargscr then pushed from there. Pre-fix every such
|
|
* source fell to the scalar default (one PUSHQ for a
|
|
* multi-word aggregate) and stack-imbalanced against the
|
|
* type-based multi-word drain. */
|
|
if (!widen[i] && node_isaggarg(args[i])
|
|
&& !(node_isstructarg(args[i])
|
|
&& args[i]->kind == N_IDENT)) {
|
|
int aggsz = aggarg_size(args[i]->type);
|
|
int nwords = (aggsz + 7) / 8;
|
|
/* A float-bearing ≤16B struct from a non-ident
|
|
* source would need the #165 SSE eightbyte
|
|
* transport the GP push/drain here can't model —
|
|
* loud-stop rather than silently GP-pass it (a
|
|
* ≤16B struct with any float field; the wwstage
|
|
* tinfo mirror uses the same predicate). */
|
|
{
|
|
Type *st = args[i]->type;
|
|
if (st && st->kind == TY_NAMED)
|
|
st = st->under;
|
|
if (st && st->kind == TY_STRUCT
|
|
&& st->size <= 16) {
|
|
int f32;
|
|
for (Tfield *f = st->fields; f;
|
|
f = f->next)
|
|
if (fld_isfloat(f->type,
|
|
&f32))
|
|
fatal("#271/#165: "
|
|
"float-bearing "
|
|
"struct arg from a "
|
|
"non-ident source "
|
|
"needs SSE eightbyte "
|
|
"transport (out of "
|
|
"scope)");
|
|
}
|
|
}
|
|
if (args[i]->kind == N_CALL) {
|
|
if (cg_sret_retsize(args[i]->type) > 0) {
|
|
if (cg_aggargscr == 0) {
|
|
cg_aggargscr =
|
|
local_alloc(c, &locals,
|
|
"@aggargscr", aggsz,
|
|
cg_frame);
|
|
cg_aggargscr_sz = aggsz;
|
|
} else if (aggsz >
|
|
cg_aggargscr_sz) {
|
|
fatal("cgcall: @aggargscr "
|
|
"cached sz %d, need %d "
|
|
"(#271 pinned offset "
|
|
"can't grow)",
|
|
cg_aggargscr_sz,
|
|
aggsz);
|
|
}
|
|
cg_sret_dest_off = cg_aggargscr;
|
|
cgexpr(c, args[i], locals);
|
|
cg_sret_dest_off = 0;
|
|
for (int k = nwords - 1; k >= 0;
|
|
k--) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP,
|
|
cg_aggargscr + k*8),
|
|
areg(D_AX));
|
|
ins1(c, A_PUSHQ,
|
|
areg(D_AX));
|
|
}
|
|
} else {
|
|
/* ≤24B: producer left AX=word0,
|
|
* DX=word1, CX=word2. Push
|
|
* high→low so the pop drains
|
|
* word0 first. */
|
|
int rr[3] = { D_AX, D_DX, D_CX };
|
|
cgexpr(c, args[i], locals);
|
|
for (int k = nwords - 1; k >= 0;
|
|
k--)
|
|
ins1(c, A_PUSHQ,
|
|
areg(rr[k]));
|
|
}
|
|
continue;
|
|
}
|
|
if (!aggarg_srcaddr(c, args[i], D_SI, locals))
|
|
fatal("#271: aggregate arg from "
|
|
"unsupported source kind %d",
|
|
args[i]->kind);
|
|
for (int k = nwords - 1; k >= 0; k--) {
|
|
ins2(c, A_MOVQ, amem(D_SI, k*8),
|
|
areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
continue;
|
|
}
|
|
if (widen[i]) {
|
|
/* Concrete → tagged-union widening at the call
|
|
* site. Mirrors the let/assign/return widening:
|
|
* lay out the value in the parameter's slot
|
|
* shape, then push high→low so pop drains tag
|
|
* first.
|
|
*
|
|
* Branches by source shape:
|
|
* - nullable (sz==8): pointer IS the disc.
|
|
* - str: tag@+0, ptr@+8, len@+16.
|
|
* - struct ident: copy struct words then
|
|
* prepend tag, zero-pad to slot size.
|
|
* - struct literal: materialise via a stack
|
|
* scratch slot — store each field at its
|
|
* struct-relative offset (with the +8 tag
|
|
* shift), zero-fill, then push from slot.
|
|
* - tagged source: load src slot words, remap
|
|
* the tag word via cg_widen_tag_remap, pad
|
|
* to wider dst slot, push.
|
|
* - scalar: tag@+0, value@+8, optional pad. */
|
|
cg_widen_tagged_push(c, &locals, widen_param[i],
|
|
args[i], widen_sz[i]);
|
|
continue;
|
|
}
|
|
/* #38b residual (rule 7): a tagged arg slot past the
|
|
* 6-reg arg capacity has no push shape —
|
|
* tagged_arg_size returns 0 ("too large") and the
|
|
* scalar default silently pushed ONE word. Loud-stop;
|
|
* symmetric ww gate in pushargsrev. */
|
|
{
|
|
Type *au = type_chase_named(args[i]->type);
|
|
if (au && au->kind == TY_TAGGED
|
|
&& !au->nullable
|
|
&& tagged_arg_size(args[i]->type) == 0)
|
|
fatal("#38b: tagged arg exceeds the "
|
|
"register arg capacity (>48B "
|
|
"slot) — unwired");
|
|
}
|
|
cgexpr(c, args[i], locals);
|
|
Type *tuparg_push = node_tuplearg(args[i]);
|
|
if (node_isfloat(args[i])) {
|
|
/* f32 spills 4B (MOVSS), f64 8B (MOVSD): the SysV
|
|
* float class drives the width per ref/qbe
|
|
* amd64/emit.c:524 (slot-copy single→movss). The
|
|
* slot is 8B either way; the pop reads the same
|
|
* width back. #143. */
|
|
int fmov = op_for(args[i], A_MOVSD, A_MOVSS);
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, fmov, areg(D_X0), amem(D_SP, 0));
|
|
} else if (node_isstr(args[i])) {
|
|
/* str IS []u8: cgexpr left (AX=ptr, BX=len,
|
|
* CX=cap). Push the triple, same as slice
|
|
* (#1/Phase 3). */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
|
|
} else if (node_isslice(args[i])) {
|
|
/* Slice-typed arg without a fast path above
|
|
* (e.g. `s: []u8` cast): cgexpr left
|
|
* (AX=ptr, BX=len, CX=cap). Push the triple. */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
|
|
} else if (node_istaggedarg(args[i])) {
|
|
/* Tagged-return ABI: AX=tag, DX=val0,
|
|
* CX=val1, R8=val2. Push high-to-low so pop
|
|
* drains tag first (into arg-reg[0]), then
|
|
* values into arg-reg[1..]. Nullable (sz=8):
|
|
* AX holds the pointer, no value-word
|
|
* registers — push just AX. */
|
|
/* #38b residual (rule 7): an sret-class call
|
|
* result is in memory, not the cursor — the
|
|
* @aggargscr-style receive-then-push is the
|
|
* #40-family follow-up. */
|
|
if (args[i]->kind == N_CALL
|
|
&& cg_sret_retsize(args[i]->type) > 0)
|
|
fatal("#38b: >32B tagged call result "
|
|
"as a call argument unwired "
|
|
"(#40-family follow-up)");
|
|
int sz = tagged_arg_size(args[i]->type);
|
|
if (sz > 24)
|
|
ins1(c, A_PUSHQ, areg(D_R8));
|
|
if (sz > 16)
|
|
ins1(c, A_PUSHQ, areg(D_CX));
|
|
if (sz > 8)
|
|
ins1(c, A_PUSHQ, areg(D_DX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
} else if (tuparg_push) {
|
|
/* #163: tuple ARG (param twin of #164's return).
|
|
* cgexpr above left the tuple in the return-ABI
|
|
* cursor; restage it into @tupargscr by SysV class
|
|
* (tuple_store, the #164 helper), then push the slot
|
|
* words high→low so the pop drains slot+0 first into
|
|
* the ARG cursor. The frame slot decouples the
|
|
* return-class regs (AX/DX/CX/R8 + X0/X1) from the
|
|
* overlapping arg-class regs (DI/SI/.. + X0..X7). */
|
|
int gpcur = 0, ssecur = 0, eoff = 0, ef32;
|
|
int gptot = 0, sstot = 0, tsz = 0;
|
|
for (Tparam *p = tuparg_push->params; p; p = p->next) {
|
|
Type *pu = (p->type
|
|
&& p->type->kind == TY_NAMED)
|
|
? p->type->under : p->type;
|
|
int wide = pu && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR);
|
|
if (fld_isfloat(p->type, &ef32))
|
|
sstot++;
|
|
else
|
|
gptot += tuple_ebytes(wide);
|
|
/* slot stride per element (sum == tuple slot
|
|
* size); matches the wwstage slotsize() walk so
|
|
* the @tupargscr width + reverse-push count agree
|
|
* byte-for-byte. */
|
|
tsz += wide ? (int)pu->size : 8;
|
|
}
|
|
/* The producing call already satisfied #164's
|
|
* return caps; guard anyway (tuple_store indexes
|
|
* tuple_rseq[4] / tuple_sse_seq[2]). */
|
|
if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP)
|
|
fatal("tuple arg exceeds return-cursor ABI "
|
|
"capacity; see #163/#164");
|
|
if (cg_tupargscr == 0) {
|
|
cg_tupargscr = local_alloc(c, &locals,
|
|
"@tupargscr", tsz, cg_frame);
|
|
cg_tupargscr_sz = tsz;
|
|
} else if (tsz > cg_tupargscr_sz) {
|
|
fatal("cgcall: @tupargscr cached sz %d, "
|
|
"need %d (pinned offset can't grow; "
|
|
"#163)", cg_tupargscr_sz, tsz);
|
|
}
|
|
for (Tparam *p = tuparg_push->params; p; p = p->next) {
|
|
Type *pu = (p->type
|
|
&& p->type->kind == TY_NAMED)
|
|
? p->type->under : p->type;
|
|
int wide = pu && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR);
|
|
int isflt = fld_isfloat(p->type, &ef32);
|
|
tuple_store(c, p->type, wide, gpcur, ssecur,
|
|
cg_tupargscr + eoff);
|
|
if (isflt)
|
|
ssecur++;
|
|
else
|
|
gpcur += tuple_ebytes(wide);
|
|
eoff += wide ? (int)pu->size : 8;
|
|
}
|
|
for (int w = tsz - 8; w >= 0; w -= 8) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_tupargscr + w),
|
|
areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
} else {
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
}
|
|
/* sret discipline (#23): callee returns plain TY_STRUCT
|
|
* > 24B. Reserve RDI for the hidden dest-pointer arg by
|
|
* starting the int-arg cursor at 1 and emit the LEAQ AFTER
|
|
* the pop loop (so the pops don't clobber RDI). The dest
|
|
* slot is either the receiver's own slot (cg_sret_dest_off,
|
|
* propagated from N_LET / N_ASSIGN ident receive) or a
|
|
* per-fn @sretscr discard slot. Sized at the receive site
|
|
* or here for discards.
|
|
*
|
|
* Stack alignment is unaffected because pushargsrev/pops
|
|
* left RDI free — we never popped a user arg into it. */
|
|
int sret_call_sz = 0;
|
|
int sret_call_off = 0;
|
|
const char *sret_dest_sym = NULL; /* #220 */
|
|
{
|
|
Type *ret = (cu && cu->kind == TY_FN)
|
|
? cu->ret : NULL;
|
|
sret_call_sz = cg_sret_retsize(ret);
|
|
}
|
|
if (sret_call_sz > 0 && cg_sret_dest_sym != NULL) {
|
|
/* #220: GLOBAL dest — RDI gets LEAQ name(SB) below; no
|
|
* @sretscr slot needed (the callee writes the struct
|
|
* straight into g's storage). */
|
|
sret_dest_sym = cg_sret_dest_sym;
|
|
cg_sret_dest_sym = NULL;
|
|
} else if (sret_call_sz > 0) {
|
|
/* @sretscr is only needed when the result is dropped
|
|
* (no `let x = f();` receiver wired the call's dest into
|
|
* cg_sret_dest_off). Allocate first-use per #15/#26c
|
|
* size-strategy convergence — wwstage's scanlocals pre-
|
|
* pass that used to reserve this slot unconditionally is
|
|
* gone; cstage matches by skipping the allocation when a
|
|
* dest is already wired. fatal() on a later sret CALL
|
|
* needing a bigger slot (rule 7 — pinned offset can't
|
|
* grow in place). */
|
|
if (cg_sret_dest_off != 0) {
|
|
sret_call_off = cg_sret_dest_off;
|
|
cg_sret_dest_off = 0;
|
|
} else {
|
|
if (cg_sretscr_off == 0) {
|
|
cg_sretscr_off = local_alloc(c,
|
|
&locals, "@sretscr",
|
|
sret_call_sz, cg_frame);
|
|
cg_sretscr_sz = sret_call_sz;
|
|
} else if (sret_call_sz > cg_sretscr_sz) {
|
|
fatal("cgcall: @sretscr cached sz "
|
|
"%d, need %d (per-fn slot growth "
|
|
"post-#15 — pinned offset can't "
|
|
"grow in place)",
|
|
cg_sretscr_sz, sret_call_sz);
|
|
}
|
|
sret_call_off = cg_sretscr_off;
|
|
}
|
|
}
|
|
/* pop forward into the right register class. Args that
|
|
* don't fit in regs stay on the stack and are reached by
|
|
* the callee via positive offsets from BP. The caller is
|
|
* responsible for cleaning them up after CALL. */
|
|
int ii = (sret_call_sz > 0) ? 1 : 0, fi = 0, stackslots = 0;
|
|
Type *tu;
|
|
for (int i = 0; i < argcount; i++) {
|
|
if (widen[i]) {
|
|
/* Pop widened tagged slot into arg-register
|
|
* class — sized by the parameter's tagged slot,
|
|
* not the arg's static type. */
|
|
int eb = widen_sz[i] / 8;
|
|
for (int k = 0; k < eb; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ,
|
|
areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
continue;
|
|
}
|
|
if (node_isfloat(args[i])) {
|
|
if (fi < 8) {
|
|
/* Reload the spilled f32/f64 at its class
|
|
* width — MOVSS for f32, MOVSD for f64 —
|
|
* matching the push above (#143). */
|
|
int fmov = op_for(args[i], A_MOVSD,
|
|
A_MOVSS);
|
|
ins2(c, fmov, amem(D_SP, 0),
|
|
areg(sysv_fargregs[fi]));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
fi++;
|
|
} else {
|
|
stackslots++; /* leave on stack */
|
|
}
|
|
} else if (node_isstr(args[i])) {
|
|
/* str IS []u8: 3-word arg, same as slice (#1/Phase 3). */
|
|
for (int k = 0; k < 3; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if (node_isslice(args[i])) {
|
|
for (int k = 0; k < 3; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if (node_isstructarg(args[i])) {
|
|
int sclass[2], snb;
|
|
/* SSE-drain only for an ident arg: the struct push
|
|
* stages raw slot words for an N_IDENT only (non-
|
|
* ident struct args are a pre-existing >8B-push gap,
|
|
* out of scope). Gating here keeps cstage byte-id
|
|
* with wwstage, whose type lookup is ident-keyed. */
|
|
if (args[i]->kind == N_IDENT
|
|
&& (snb = struct_float_class(args[i]->type,
|
|
sclass)) > 0) {
|
|
/* #165: float-bearing struct arg — drain by
|
|
* SysV eightbyte class: a lone-f64 eightbyte
|
|
* MOVSD off (SP) into the next XMM (X0..X7), a
|
|
* pure-INT eightbyte POPQ into the next INTEGER
|
|
* arg reg (DI/SI/..). The struct-ident push
|
|
* staged raw words (class-independent); only the
|
|
* drain differs. Gated to qualifying floats;
|
|
* all-int + f32-packed keep the all-GP pop
|
|
* below. Reg overflow loud-stops (rule 7), the
|
|
* partial-spill stitch out of scope (#163 twin). */
|
|
for (int e = 0; e < snb; e++) {
|
|
if (sclass[e]) {
|
|
if (fi >= 8)
|
|
fatal("float struct arg "
|
|
"eightbyte overflows SSE "
|
|
"arg regs (X0..X7); stitch "
|
|
"out of scope, see #165");
|
|
ins2(c, A_MOVSD,
|
|
amem(D_SP, 0),
|
|
areg(sysv_fargregs[fi]));
|
|
ins2(c, A_ADDQ, aimm(8),
|
|
areg(D_SP));
|
|
fi++;
|
|
} else {
|
|
if (ii >= 6)
|
|
fatal("float struct arg "
|
|
"eightbyte overflows "
|
|
"integer arg regs (DI/SI/"
|
|
"DX/CX/R8/R9); stitch out "
|
|
"of scope, see #165");
|
|
ins1(c, A_POPQ,
|
|
areg(sysv_argregs[ii++]));
|
|
}
|
|
}
|
|
} else {
|
|
int sz = struct_arg_size(args[i]->type);
|
|
int eb = (sz > 8) ? 2 : 1;
|
|
for (int k = 0; k < eb; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ,
|
|
areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
}
|
|
} else if (node_isaggarg(args[i])
|
|
&& !node_isstructarg(args[i])) {
|
|
/* #271: array / >16B-struct aggregate arg —
|
|
* drain its ceil(sz/8) staged words into the
|
|
* INTEGER arg cursor (overflow spills to the
|
|
* stack, reached by the callee via positive BP
|
|
* offsets). The ≤16B struct case stays in
|
|
* node_isstructarg above (SSE class path
|
|
* intact). */
|
|
int aggsz = aggarg_size(args[i]->type);
|
|
int nw = (aggsz + 7) / 8;
|
|
for (int k = 0; k < nw; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ,
|
|
areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if (node_istaggedarg(args[i])) {
|
|
int sz = tagged_arg_size(args[i]->type);
|
|
int eb = sz / 8;
|
|
for (int k = 0; k < eb; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if ((tu = node_tuplearg(args[i])) != NULL) {
|
|
/* #163: drain the tuple's staged words (pushed
|
|
* slot+0 first) into the SysV arg cursor by SysV
|
|
* class — a float MOVSD/MOVSS off (SP) into the
|
|
* next XMM (X0..X7), everything else POPQ into the
|
|
* next INTEGER arg reg (DI/SI/..); a slice/str its
|
|
* 3-word {ptr,len,cap}. Reg overflow loud-stops
|
|
* (rule 7): the partial-spill stitch is out of
|
|
* scope (twin of #164's cap). */
|
|
int ef32;
|
|
for (Tparam *p = tu->params; p; p = p->next) {
|
|
Type *pu = (p->type
|
|
&& p->type->kind == TY_NAMED)
|
|
? p->type->under : p->type;
|
|
int wide = pu && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR);
|
|
if (fld_isfloat(p->type, &ef32)) {
|
|
if (fi >= 8)
|
|
fatal("tuple arg float "
|
|
"element overflows SSE "
|
|
"arg regs (X0..X7); "
|
|
"stitch out of scope, "
|
|
"see #163");
|
|
ins2(c, ef32 ? A_MOVSS : A_MOVSD,
|
|
amem(D_SP, 0),
|
|
areg(sysv_fargregs[fi]));
|
|
ins2(c, A_ADDQ, aimm(8),
|
|
areg(D_SP));
|
|
fi++;
|
|
continue;
|
|
}
|
|
int eb = tuple_ebytes(wide);
|
|
if (ii + eb > 6)
|
|
fatal("tuple arg element "
|
|
"overflows integer arg regs "
|
|
"(DI/SI/DX/CX/R8/R9); stitch "
|
|
"out of scope, see #163");
|
|
for (int k = 0; k < eb; k++)
|
|
ins1(c, A_POPQ,
|
|
areg(sysv_argregs[ii++]));
|
|
}
|
|
} else {
|
|
if (ii < 6) {
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii]));
|
|
ii++;
|
|
} else {
|
|
stackslots++;
|
|
}
|
|
}
|
|
}
|
|
/* sret hidden first-arg (#23): load &dest into RDI AFTER
|
|
* all user-arg pops have finished — the pop loop started
|
|
* its int-arg cursor at 1, so RDI was never written.
|
|
*
|
|
* Forwarding (task #9 follow-up): when outer's `return f();`
|
|
* forwards through an sret callee, source RDI from outer's
|
|
* saved @sretarg — inner writes directly into outer's
|
|
* caller-prealloc dest. No temporary in outer's frame.
|
|
* Post-#15 @sretscr is skipped entirely on the forwarding
|
|
* branch (no allocation, no frame growth) — earlier scan-
|
|
* lockstep reservation is gone. */
|
|
if (sret_call_sz > 0) {
|
|
if (cg_sret_forward) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_DI));
|
|
cg_sret_forward = 0;
|
|
} else if (sret_dest_sym != NULL) {
|
|
/* #220: sret into a GLOBAL — RDI = &g(SB). */
|
|
ins2(c, A_LEAQ, masym(c, sret_dest_sym),
|
|
areg(D_DI));
|
|
} else {
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, sret_call_off),
|
|
areg(D_DI));
|
|
}
|
|
}
|
|
/* SysV: variadic callees require AL to hold the count of
|
|
* XMM regs used in the variable portion. We don't pass
|
|
* floats yet, so AL=0 covers every case we emit. */
|
|
if (cu && cu->kind == TY_FN && cu->variadic)
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
if (n->lhs->kind == N_IDENT) {
|
|
/* If the callee names a local variable holding a
|
|
* function pointer, load it and call indirect. Without
|
|
* this check `CALL fp(SB)` is emitted as if `fp` were
|
|
* a global symbol — the linker rightly fails. Hare /
|
|
* QBE handles this by treating any non-`$symbol` value
|
|
* as an indirect target; we get the same effect by
|
|
* reusing the cgexpr path. */
|
|
int loff = localfind(locals, n->lhs->str);
|
|
if (loff != 0) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, loff), areg(D_AX));
|
|
ins1(c, A_CALL, areg(D_AX));
|
|
} else {
|
|
/* Bare `f()` — same-module by ww's resolver
|
|
* rules. Hint with c->cur_mod so the right
|
|
* fn wins when the leaf collides with another
|
|
* module's exported same-leaf fn. */
|
|
ins1(c, A_CALL,
|
|
mafn(c, n->lhs->str, c->cur_mod));
|
|
}
|
|
} else if (n->lhs->kind == N_DOT && n->lhs->lhs &&
|
|
n->lhs->lhs->kind == N_IDENT) {
|
|
/* `m.fn()` is module-qualified iff the ident has no
|
|
* concrete type (SK_USE leaves it ty_err). For a real
|
|
* type — typically a struct or *struct holding a
|
|
* function pointer — we load the field and indirect. */
|
|
Type *bt = n->lhs->lhs->type;
|
|
if (bt == NULL || bt == ty_err) {
|
|
/* `m.fn()` — explicit module qualifier. Pass
|
|
* the bareword as the hint so cross-module
|
|
* same-leaf exports resolve correctly. */
|
|
ins1(c, A_CALL,
|
|
mafn(c, n->lhs->str, n->lhs->lhs->str));
|
|
} else {
|
|
cgexpr(c, n->lhs, locals); /* AX = fn ptr */
|
|
ins1(c, A_CALL, areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_CALL, areg(D_AX));
|
|
}
|
|
/* SysV: caller cleans stack args. */
|
|
if (stackslots > 0)
|
|
ins2(c, A_ADDQ, aimm(stackslots * 8), areg(D_SP));
|
|
/* str IS []u8: callee returns AX=ptr, BX=len, CX=cap —
|
|
* same as a slice, no receive-side shuffle (#1/Phase 3). */
|
|
break;
|
|
}
|
|
case N_MATCH: {
|
|
/* match on a tagged-union scrutinee. Read tag and value from
|
|
* the slot. Dispatch by the resolved variant index of each
|
|
* case's type pattern — case order is independent of variant
|
|
* declaration order. A case with no pattern (`case =>`) is a
|
|
* default arm; its body always runs.
|
|
*
|
|
* Slot layout: [+0]=tag, [+8]=value0, [+16]=value1. The third
|
|
* word is only meaningful for variants whose payload is >8B
|
|
* (e.g. str). Bindings sized 16B (str) copy two words.
|
|
*
|
|
* Nullable folded `(*T | void)`: slot is one 8B word holding
|
|
* the pointer; null IS the void variant. Discriminator =
|
|
* value, not a separate tag. */
|
|
Node *s = n->lhs;
|
|
Type *st = s ? s->type : NULL;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int is_nullable = type_isnullable(st);
|
|
int slot_size = (su && su->kind == TY_TAGGED) ? (int)su->size : 16;
|
|
int sl_off;
|
|
if (s->kind == N_IDENT) {
|
|
sl_off = localfind(locals, s->str);
|
|
} else if (s->kind == N_DOT && s->lhs && s->lhs->kind == N_IDENT
|
|
&& s->lhs->type) {
|
|
/* `match (p.field)` — point sl_off at the field's slot
|
|
* inside the parent struct. The slot layout (tag at +0,
|
|
* value words at +8/+16) is contiguous within the struct,
|
|
* so no spill is needed. */
|
|
Type *bt = s->lhs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
Tfield *f = NULL;
|
|
if (bu && bu->kind == TY_STRUCT) {
|
|
for (Tfield *fl = bu->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, s->str) == 0) {
|
|
f = fl; break;
|
|
}
|
|
}
|
|
}
|
|
if (f) {
|
|
int boff = localfind(locals, s->lhs->str);
|
|
sl_off = boff + (int)f->offset;
|
|
} else {
|
|
/* fall back to spill — `match (h.e)` where
|
|
* h is *struct. cgexpr → cgdot now leaves the
|
|
* AX=tag, DX=val0, CX=val1[, R8=val2] shape
|
|
* (task #28), so spill all words the variant
|
|
* may carry. Pre-#28 only AX landed and the
|
|
* dispatch fired on a stale slot. */
|
|
sl_off = localoff(c, &locals, "@match_spill",
|
|
slot_size, cg_frame);
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sl_off + 0));
|
|
if (!is_nullable) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
if (slot_size > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, sl_off + 24));
|
|
}
|
|
}
|
|
} else {
|
|
/* Spill non-ident scrutinees (e.g. `match (foo()?)`) into
|
|
* a scratch slot so we can index out the tag/value. The
|
|
* call ABI for tagged returns is AX=tag, DX=value0,
|
|
* CX=value1, R8=value2 — copy each word into the slot.
|
|
* Nullable returns are single-word: AX is the pointer;
|
|
* spill only that. */
|
|
sl_off = localoff(c, &locals, "@match_spill", slot_size,
|
|
cg_frame);
|
|
if (s->kind == N_CALL && cg_sret_retsize(st) > 0) {
|
|
/* #38b: sret-classified tagged call — pass the
|
|
* scrut slot itself as the sret dest and skip
|
|
* the cursor spill; downstream tag dispatch /
|
|
* case-let binds already read the slot from
|
|
* memory. */
|
|
cg_sret_dest_off = sl_off;
|
|
cgexpr(c, s, locals);
|
|
cg_sret_dest_off = 0;
|
|
} else {
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
|
if (!is_nullable) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
if (slot_size > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, sl_off + 24));
|
|
}
|
|
}
|
|
}
|
|
char *end = mklabel(c, "match_end");
|
|
/* Push the end label as the yield target for arm bodies. */
|
|
if (nyields < YIELD_MAX) {
|
|
yield_target[nyields++] = end;
|
|
}
|
|
for (Node *cs = n->list; cs; cs = cs->next) {
|
|
char *next = mklabel(c, "match_next");
|
|
/* Per-arm scope: save the locals head, restore it
|
|
* after the body runs. Mirrors check.c's saved/restore
|
|
* around cstmt — the case bind (and any lets inside
|
|
* the arm) shouldn't leak past the arm, where a
|
|
* matching outer name would otherwise resolve to the
|
|
* shadow instead of the original. */
|
|
Local *arm_locals_saved = locals;
|
|
if (cs->type != NULL) {
|
|
int tag = cg_tag_for_variant(su, cs->type);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
if (is_nullable) {
|
|
/* discriminator = pointer-vs-null.
|
|
* *T variant: skip if ptr == 0.
|
|
* void variant: skip if ptr != 0. */
|
|
int ptr_tag = nullable_ptr_tag(su);
|
|
int want_ptr = (tag == ptr_tag);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
if (want_ptr)
|
|
ins1(c, A_JE, abranch(next));
|
|
else
|
|
ins1(c, A_JNE, abranch(next));
|
|
} else if (cs->list != NULL) {
|
|
/* Multi-pattern `case T1 | T2 | ... =>`:
|
|
* if the tag matches any of the alts,
|
|
* jump to body; otherwise to the next
|
|
* case. */
|
|
char *body = mklabel(c, "match_body");
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag),
|
|
areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
for (Node *alt = cs->list; alt;
|
|
alt = alt->next) {
|
|
int atag = cg_tag_for_variant(
|
|
su, alt->type);
|
|
ins2(c, A_CMPQ,
|
|
aimm(atag < 0 ? 0 : atag),
|
|
areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
}
|
|
ins1(c, A_JMP, abranch(next));
|
|
label(c, body);
|
|
} else {
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag),
|
|
areg(D_AX));
|
|
ins1(c, A_JNE, abranch(next));
|
|
}
|
|
}
|
|
if (cs->str && cs->str[0] && cs->type) {
|
|
Type *bt = cs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
if (is_nullable) {
|
|
/* Bind *T or void to a local. The
|
|
* value IS the slot's pointer word; no
|
|
* payload to copy. void binding is
|
|
* unusable (size 0), so only emit for
|
|
* the *T variant. local_alloc (not
|
|
* localoff): the bind must NEVER reuse
|
|
* an outer same-named slot. */
|
|
if (bu && bu->kind == TY_PTR) {
|
|
int voff = local_alloc(c, &locals,
|
|
cs->str, 8, cg_frame);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, sl_off + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, voff));
|
|
}
|
|
} else {
|
|
/* #43: route through Type.size SSoT rather
|
|
* than re-asserting 16/24 for str/slice. */
|
|
int bsz = 8;
|
|
if (bu) bsz = (int)bu->size;
|
|
if (bsz <= 0) bsz = 8;
|
|
/* local_alloc to dodge name-collision
|
|
* dedup — a 16B str bind shadowing an
|
|
* 8B outer would otherwise overflow
|
|
* into the saved BP. */
|
|
int voff = local_alloc(c, &locals, cs->str,
|
|
bsz, cg_frame);
|
|
int nwords = (bsz + 7) / 8;
|
|
for (int w = 0; w < nwords; w++) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, sl_off + 8 + 8*w),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, voff + 8*w));
|
|
}
|
|
}
|
|
}
|
|
cgstmt(c, cs->body, &locals, cg_frame);
|
|
locals = arm_locals_saved;
|
|
ins1(c, A_JMP, abranch(end));
|
|
label(c, next);
|
|
}
|
|
label(c, end);
|
|
if (nyields > 0) nyields--;
|
|
break;
|
|
}
|
|
case N_TRYPROP: {
|
|
/* Evaluate tagged value: AX=tag, DX=value0[, CX=value1].
|
|
* If the tag matches an error variant, propagate as the
|
|
* current function's return (with a tag remap to the
|
|
* enclosing fn's variant order). On success, unwrap to the
|
|
* success-variant ABI: ≤8B values in AX; str values in
|
|
* (AX=ptr, BX=len).
|
|
*
|
|
* Nullable: AX is the pointer; *T variant is the success
|
|
* (any non-null), void variant is the error (null). The
|
|
* enclosing fn's null encoding is the same — RET with AX=0
|
|
* if propagating; otherwise leave AX as-is on success. */
|
|
/* #38b residuals (rule 7): the cursor read below cannot see
|
|
* an sret-classified call result (AX = dest pointer), and the
|
|
* propagate-RET below cannot speak an sret-classified
|
|
* enclosing return (the caller reads memory, not the
|
|
* cursor). Both are unwired follow-ups of #40's family. */
|
|
if (n->lhs && n->lhs->kind == N_CALL
|
|
&& cg_sret_retsize(n->lhs->type) > 0)
|
|
fatal("#38b: `?` on an sret-class call result "
|
|
"unwired (mem-based unwrap is a #40-family "
|
|
"follow-up)");
|
|
if (cg_sret_retsize(cg_ret_type) > 0)
|
|
fatal("#38b: `?` propagation into a >32B tagged "
|
|
"return unwired (sret error-propagate is a "
|
|
"#40-family follow-up)");
|
|
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);
|
|
{
|
|
/* #241: a tuple success payload is an rvalue tuple — fill
|
|
* the cursor (shift past the tag) so the destructure /
|
|
* let consumer reads every element, not just word0. */
|
|
Type *stu = type_chase_named(succ_t);
|
|
if (stu && stu->kind == TY_TUPLE) {
|
|
cg_tagged_tuple_payload_shift(c, stu);
|
|
break;
|
|
}
|
|
}
|
|
if (success_is_str) {
|
|
/* str IS []u8: success value arrives in the tagged
|
|
* ABI as DX=ptr, CX=len, R8=cap (slot 32B). Move len
|
|
* out before cap overwrites CX (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_R8), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case N_TRYUNW: {
|
|
/* On error variant: exit(1) directly via the syscall.
|
|
* Nullable: null = error; non-null = success (AX is the
|
|
* pointer, ready to use). */
|
|
/* #38b residual (rule 7): see the N_TRYPROP twin. */
|
|
if (n->lhs && n->lhs->kind == N_CALL
|
|
&& cg_sret_retsize(n->lhs->type) > 0)
|
|
fatal("#38b: `!` on an sret-class call result "
|
|
"unwired (mem-based unwrap is a #40-family "
|
|
"follow-up)");
|
|
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);
|
|
{
|
|
/* #241: tuple success payload fills the cursor (shift past
|
|
* the tag) — same rvalue-tuple-into-cursor story. */
|
|
Type *stu = type_chase_named(succ_t);
|
|
if (stu && stu->kind == TY_TUPLE) {
|
|
cg_tagged_tuple_payload_shift(c, stu);
|
|
break;
|
|
}
|
|
}
|
|
if (success_is_str) {
|
|
/* str IS []u8: success arrives DX=ptr, CX=len, R8=cap
|
|
* (slot 32B). Move len out before cap clobbers CX
|
|
* (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_R8), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case N_TYPETEST: {
|
|
/* `e is T` — Compare scrutinee tag against T's variant index.
|
|
* Result is bool (0/1) in AX. Nullable: discriminator is
|
|
* pointer-vs-null, not a tag. */
|
|
/* #38b residual (rule 7): an sret-class call result leaves
|
|
* AX = dest pointer, not the tag — mem-based test is a
|
|
* #40-family follow-up. */
|
|
if (n->lhs && n->lhs->kind == N_CALL
|
|
&& cg_sret_retsize(n->lhs->type) > 0)
|
|
fatal("#38b: `is` on an sret-class call result "
|
|
"unwired (#40-family follow-up)");
|
|
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;
|
|
/* #38b residual (rule 7): the @asrt_spill below reads the
|
|
* cursor, which an sret-class call result never fills. */
|
|
if (s && s->kind == N_CALL && cg_sret_retsize(st) > 0)
|
|
fatal("#38b: `as` on an sret-class call result "
|
|
"unwired (#40-family follow-up)");
|
|
int sl_off = 0;
|
|
if (s && s->kind == N_IDENT && s->str) {
|
|
sl_off = localfind(locals, s->str);
|
|
}
|
|
if (sl_off == 0) {
|
|
sl_off = localoff(c, &locals, "@asrt_spill",
|
|
slot_size, cg_frame);
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
|
if (!(u && u->kind == TY_TAGGED && u->nullable)) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
}
|
|
}
|
|
char *ok = mklabel(c, "asrt_ok");
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
int ptr_tag = nullable_ptr_tag(u);
|
|
int want_ptr = (tag == ptr_tag);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
if (want_ptr)
|
|
ins1(c, A_JNE, abranch(ok));
|
|
else
|
|
ins1(c, A_JE, abranch(ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, ok);
|
|
/* AX already holds the pointer (or 0 for void
|
|
* variant, where the result type has size 0 and
|
|
* no consumer reads it). */
|
|
break;
|
|
}
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
|
|
ins1(c, A_JE, abranch(ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, ok);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 8), areg(D_AX));
|
|
if (type_isstr(vt))
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 16), areg(D_BX));
|
|
break;
|
|
}
|
|
case N_CAST: {
|
|
int from_f = node_isfloat(n->lhs);
|
|
int to_f = cg_isfloat(n->type);
|
|
int from_f32 = node_isf32(n->lhs);
|
|
int to_f32 = type_isf32(n->type);
|
|
cgexpr(c, n->lhs, locals); /* AX or X0 depending */
|
|
if (from_f && !to_f) {
|
|
int op = from_f32 ? A_CVTTSS2SI : A_CVTTSD2SI;
|
|
ins2(c, op, areg(D_X0), areg(D_AX));
|
|
} else if (!from_f && to_f) {
|
|
int op = to_f32 ? A_CVTSI2SS : A_CVTSI2SD;
|
|
ins2(c, op, areg(D_AX), areg(D_X0));
|
|
} else if (from_f && to_f && from_f32 != to_f32) {
|
|
int op = to_f32 ? A_CVTSD2SS : A_CVTSS2SD;
|
|
ins2(c, op, areg(D_X0), areg(D_X0));
|
|
}
|
|
/* str → []u8 (or any []T): cgexpr left (AX=ptr, BX=len).
|
|
* Slice register convention is (AX=ptr, BX=len, CX=cap);
|
|
* synthesise cap = len so downstream arg-push / let-init
|
|
* paths see the canonical triple. Without this, the cap
|
|
* register stays whatever cgexpr happened to leave there
|
|
* and the receiver reads a stale value. */
|
|
{
|
|
Type *tt = n->type;
|
|
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
|
|
Type *ft = n->lhs ? n->lhs->type : NULL;
|
|
Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft;
|
|
if (tu && tu->kind == TY_SLICE
|
|
&& fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
}
|
|
}
|
|
/* Narrowing integer cast: clamp AX to the target width so
|
|
* downstream 64-bit ops see a value within the declared
|
|
* range. Hare semantics: `expr: T` truncates to T's bit
|
|
* width (mod 2^n). Without this, `(big_u64): u32` left the
|
|
* upper 32 bits intact and CMPQ/DIVQ misread the value.
|
|
*
|
|
* Unsigned targets use MOVL/ANDQ to clear the high bits.
|
|
* Signed-narrow targets (i8/i16/i32) sign-extend via
|
|
* MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates;
|
|
* this is what lets `(0xFF80i64): i8` compare equal to
|
|
* -128i64 after a widening read-back. Symmetric on signed
|
|
* vs unsigned: both branches gate on `type_isint(tu) &&
|
|
* size<8`, then dispatch on type_isunsigned(tu). The
|
|
* recursion through TY_ENUM in type_isunsigned (task #5)
|
|
* is what lets an enum-aliased narrow (`type myflag = i8`)
|
|
* pick up the right MOVS*Q. Wwstage's cgcast keys off the
|
|
* resolved type-name through the same shape. TY_RUNE is
|
|
* unsigned (Unicode scalar) and lands on the MOVL path. */
|
|
if (!from_f && !to_f && n->type) {
|
|
Type *tt = n->type;
|
|
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
|
|
/* Identity-width identity-sign cast is a no-op at the
|
|
* machine-int level: src and dst share both width and
|
|
* signedness, so the natural slot/load already carries
|
|
* the right canonical 64-bit shape and the narrow-clamp
|
|
* is dead. Replaces b5632b1's single-site `!dst_is_enum`
|
|
* gate (task #25) which mirrored wwstage's N_TENUM
|
|
* lacuna; the lacuna is fixed there too, so this gate
|
|
* stays symmetric across both stages (#33). Source side
|
|
* uses `castsrcprim` (a structural walk matching
|
|
* wwstage's exprprimresolved exactly), NOT n->lhs->type
|
|
* — cstage's checker has richer type info than wwstage
|
|
* can derive without a checker, and the asymmetric
|
|
* coverage broke 995_self_rebuild's byte-id. The cost
|
|
* is that some casts (`.len: i32`, N_BIN result, call
|
|
* return, match-bound payload) still emit a redundant
|
|
* clamp on both stages; closing those gaps is a
|
|
* sibling task that extends wwstage's type inference.
|
|
* Incidentally fixes a silent miscompile #25's
|
|
* dst-kind-only skip left in place: u32→enum-u8 (and
|
|
* similar narrow-to-enum casts) was suppressing the
|
|
* clamp, so the upper bits of the source value leaked
|
|
* through register-chained downstream uses. Caveat:
|
|
* removing the defensive MOVL exposes any upstream
|
|
* cgen path that leaves garbage in upper RAX when
|
|
* producing a sub-word value — the contract is
|
|
* producers leave the value in canonical width-
|
|
* extended form. */
|
|
int src_w = 0, src_unsignd = 0;
|
|
castsrcprim(n->lhs, &src_w, &src_unsignd);
|
|
int dst_w = (tu && type_isint(tu)) ? (int)tu->size : 0;
|
|
int identity = dst_w > 0 && src_w == dst_w
|
|
&& src_unsignd == type_isunsigned(tu);
|
|
if (tu && type_isint(tu) && tu->size > 0
|
|
&& tu->size < 8 && !identity) {
|
|
if (type_isunsigned(tu)) {
|
|
if (tu->size == 4) {
|
|
ins2(c, A_MOVL,
|
|
areg(D_AX), areg(D_AX));
|
|
} else {
|
|
u64 mask = ((u64)1 << (tu->size * 8)) - 1;
|
|
ins2(c, A_ANDQ,
|
|
aimm((i64)mask),
|
|
areg(D_AX));
|
|
}
|
|
} else {
|
|
int op = A_MOVSXD;
|
|
if (tu->size == 1) op = A_MOVSBQ;
|
|
else if (tu->size == 2) op = A_MOVSWQ;
|
|
ins2(c, op, areg(D_AX), areg(D_AX));
|
|
}
|
|
}
|
|
/* TY_BOOL is size 1 too; clamp to a single byte so
|
|
* `(u32_val): bool` produces 0 or a low-byte value
|
|
* instead of leaking the upper bits. type_isint(bool)
|
|
* is false, so the symmetric narrow above misses it
|
|
* — this dedicated branch covers the bool case. */
|
|
if (tu && tu->kind == TY_BOOL) {
|
|
ins2(c, A_ANDQ, aimm(0xFF), areg(D_AX));
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case N_DOT: {
|
|
/* slice/str pseudo-fields: .ptr (offset 0), .len (8), .cap (16).
|
|
* Arrays don't carry a header; .len uses the static size and
|
|
* .ptr is the address of the first element. */
|
|
/* `(*p).f` read retarget: parser produces n->lhs = N_UN(STAR,
|
|
* IDENT(p)) with type T (post-deref struct). Pull the inner
|
|
* IDENT in as dot_lhs so bt resolves to *T and the pointer-
|
|
* auto-deref branch below fires (mirror of the N_ASSIGN
|
|
* N_DOT lhs retarget). v1 scope: N_IDENT inner only;
|
|
* (*expr).f follow-up task pending. Branches that gate on
|
|
* `n->lhs->kind == N_DOT/N_INDEX/...` keep checking the raw
|
|
* n->lhs since (*p) isn't either of those shapes. */
|
|
Node *dot_lhs = n->lhs;
|
|
if (dot_lhs && dot_lhs->kind == N_UN && dot_lhs->op == TK_STAR
|
|
&& dot_lhs->lhs && dot_lhs->lhs->kind == N_IDENT)
|
|
dot_lhs = dot_lhs->lhs;
|
|
Type *bt = dot_lhs ? dot_lhs->type : NULL;
|
|
/* type_chase_named (#22): `type b = a; type a = struct;` stacks
|
|
* two TY_NAMED layers; single peel left `u` still TY_NAMED,
|
|
* missing the TY_STRUCT field-walk gate below and collapsing
|
|
* `s.field` to a base-only MOVQ read (offset 0 instead of
|
|
* the field's declared offset). */
|
|
Type *u = type_chase_named(bt);
|
|
/* Module-qualified value reference: `mod.name`. The checker
|
|
* leaves SK_USE idents untyped (NULL/ty_err); detect that and
|
|
* look up the leaf in the flat (driver-concatenated) sym/def
|
|
* maps the same way a bare N_IDENT would. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT
|
|
&& (bt == NULL || bt == ty_err)) {
|
|
Type *t = n->type;
|
|
Type *tu = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (tu && tu->kind == TY_FN) {
|
|
/* `mod.fn` address-of via N_DOT — pass the
|
|
* module bareword as the disambiguation hint. */
|
|
ins2(c, A_LEAQ,
|
|
mafn(c, n->str, n->lhs->str), areg(D_AX));
|
|
break;
|
|
}
|
|
{
|
|
/* Same-module-first walk using n->lhs->str as
|
|
* the explicit module hint (sister of wwstage
|
|
* deflookuprhsmod). The TY_FN branch above
|
|
* already uses n->lhs->str via mafn for the
|
|
* cross-module qualifier disambiguation; this
|
|
* walk mirrors that polarity so `alpha.MSG`
|
|
* from a third module beats a head-of-sdefs
|
|
* beta.MSG collision (#11, sister of #4c). */
|
|
Sdef *s;
|
|
for (s = sdefs; s; s = s->next) {
|
|
if (strcmp(s->name, n->str) != 0)
|
|
continue;
|
|
if (sdef_mod_match_hint(s, n->lhs->str))
|
|
break;
|
|
}
|
|
if (s == NULL) {
|
|
for (s = sdefs; s; s = s->next)
|
|
if (strcmp(s->name, n->str) == 0)
|
|
break;
|
|
}
|
|
if (s != NULL) {
|
|
const char *lab = intern_strlit(c,
|
|
s->bytes, s->len);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
aimm((long long)s->len),
|
|
areg(D_BX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
/* Same gating as the bare-ident catch-all: lets route
|
|
* through localloadop (their slot can be the target of
|
|
* a narrow deref-store via `&letname: *iN`); defs and
|
|
* unresolved symbols stay on MOVQ so wwstage's defent-
|
|
* registry-without-tnode shape agrees byte-for-byte. */
|
|
int mqop = let_islet(n->str)
|
|
? localloadop(n->type) : A_MOVQ;
|
|
/* #229: thread the DOTTED module (the `m` in `m.x`) into
|
|
* the value mangle, not cur_mod — masym's non-preferring
|
|
* leaf lookup mis-mangled `aa.v` onto another module's
|
|
* same-leaf global (read the WRONG global). The TY_FN
|
|
* branch above already uses n->lhs->str via mafn. */
|
|
if (mqop == A_MOVQ) {
|
|
ins2(c, A_MOVQ,
|
|
mahint(c, n->str, n->lhs->str), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_LEAQ,
|
|
mahint(c, n->str, n->lhs->str), areg(D_CX));
|
|
ins2(c, mqop, amem(D_CX, 0), areg(D_AX));
|
|
}
|
|
goto dot_done;
|
|
}
|
|
/* Chained N_DOT spine through value-struct fields. Handles any
|
|
* depth `root.f0.f1.…leaf` where every intermediate field is a
|
|
* value struct, plus the slice/str pseudo-field tail (`s.buf.len`)
|
|
* where the innermost field is a slice/str header. Walks inward
|
|
* collecting (parent_struct, field_name); reverses to sum field
|
|
* offsets; emits one load at (base + total_off). Placed BEFORE
|
|
* the slice/str pseudo-field branch so its else-arm (cgexpr lhs
|
|
* + shuffle BX→AX) doesn't mis-handle `b.buf.len` — cgexpr on a
|
|
* value-struct→slice chain only loads .ptr into AX, leaving BX
|
|
* stale. Sibling of the pointer-chain branch further down. */
|
|
if (n->lhs && n->lhs->kind == N_DOT) {
|
|
Type *lt0 = n->lhs->type;
|
|
Type *lu0 = (lt0 && lt0->kind == TY_NAMED) ? lt0->under : lt0;
|
|
int leaf_is_pseudo = lu0 && n->str
|
|
&& (lu0->kind == TY_SLICE || lu0->kind == TY_STR)
|
|
&& (strcmp(n->str, "ptr") == 0
|
|
|| strcmp(n->str, "len") == 0
|
|
|| strcmp(n->str, "cap") == 0);
|
|
int leaf_in_struct = lu0 && lu0->kind == TY_STRUCT;
|
|
if (leaf_is_pseudo || leaf_in_struct) {
|
|
struct { Type *pu; const char *name; } steps[16];
|
|
int nsteps = 0;
|
|
int ptr_root = 0;
|
|
Node *cur = n;
|
|
int abort = 0;
|
|
while (cur && cur->kind == N_DOT && cur->lhs) {
|
|
Type *pt = cur->lhs->type;
|
|
Type *pu = (pt && pt->kind == TY_NAMED)
|
|
? pt->under : pt;
|
|
if (!pu) { abort = 1; break; }
|
|
if (cur == n && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR)) {
|
|
/* leaf pseudo on slice/str header */
|
|
} else if (pu->kind == TY_STRUCT) {
|
|
/* value-struct hop */
|
|
} else if (pu->kind == TY_PTR && pu->sub
|
|
&& cur->lhs->kind == N_IDENT) {
|
|
/* `*T` root: dereference once at emit
|
|
* time, then walk offsets through the
|
|
* pointee. Only at the last hop (root
|
|
* is a bare ident) — `*T`-field mid-
|
|
* chain keeps its cgexpr-based pointer-
|
|
* field branch further down. */
|
|
Type *sub = (pu->sub->kind == TY_NAMED)
|
|
? pu->sub->under : pu->sub;
|
|
if (sub && sub->kind == TY_STRUCT) {
|
|
pu = sub;
|
|
ptr_root = 1;
|
|
} else {
|
|
abort = 1;
|
|
break;
|
|
}
|
|
} else {
|
|
abort = 1;
|
|
break;
|
|
}
|
|
if (nsteps >= 16) { abort = 1; break; }
|
|
steps[nsteps].pu = pu;
|
|
steps[nsteps].name = cur->str;
|
|
nsteps++;
|
|
cur = cur->lhs;
|
|
}
|
|
if (!abort && cur && cur->kind == N_IDENT
|
|
&& nsteps > 0) {
|
|
int total_off = 0;
|
|
Type *leaf_type = NULL;
|
|
int slice_delta = -1;
|
|
int ok = 1;
|
|
for (int i = nsteps - 1; i >= 0; i--) {
|
|
Type *pu = steps[i].pu;
|
|
if (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR) {
|
|
if (strcmp(steps[i].name, "ptr") == 0)
|
|
slice_delta = 0;
|
|
else if (strcmp(steps[i].name, "len") == 0)
|
|
slice_delta = 8;
|
|
else if (strcmp(steps[i].name, "cap") == 0)
|
|
slice_delta = 16;
|
|
else { ok = 0; break; }
|
|
} else {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = pu->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, steps[i].name) == 0)
|
|
{ f = fl; break; }
|
|
if (!f) { ok = 0; break; }
|
|
total_off += (int)f->offset;
|
|
leaf_type = f->type;
|
|
}
|
|
}
|
|
if (ok) {
|
|
int root_off = localfind(locals, cur->str);
|
|
int base_reg = D_BP;
|
|
int base_disp = root_off;
|
|
int root_resolved = (root_off != 0);
|
|
/* #129 A.2: struct-typed defs (def_isstructdef)
|
|
* now have DATA storage and need the same
|
|
* LEAQ-and-offset shape as struct lets. */
|
|
if (!root_resolved && (let_islet(cur->str)
|
|
|| def_isstructdef(cur->str))) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str), areg(D_CX));
|
|
base_reg = D_CX;
|
|
base_disp = 0;
|
|
root_resolved = 1;
|
|
}
|
|
if (root_resolved && ptr_root) {
|
|
/* `*T` root: load the pointer value
|
|
* once; field accesses then index at
|
|
* total_off off the pointer. */
|
|
if (base_reg == D_BP) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_CX, 0), areg(D_CX));
|
|
}
|
|
base_reg = D_CX;
|
|
base_disp = 0;
|
|
}
|
|
if (root_resolved) {
|
|
if (slice_delta >= 0) {
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + slice_delta),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
Type *fu = (leaf_type
|
|
&& leaf_type->kind == TY_NAMED)
|
|
? leaf_type->under : leaf_type;
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 8),
|
|
areg(D_BX));
|
|
goto dot_done;
|
|
}
|
|
if (fu && fu->kind == TY_SLICE) {
|
|
/* Slice leaf: load all three header
|
|
* words into (AX=ptr, BX=len, CX=cap)
|
|
* so the value follows the canonical
|
|
* slice-rhs convention. base_reg may
|
|
* be CX (global / `*T` root); load
|
|
* .cap LAST so the base survives the
|
|
* earlier reads. */
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 16),
|
|
areg(D_CX));
|
|
goto dot_done;
|
|
}
|
|
int g_isf32 = 0;
|
|
if (fld_isfloat(leaf_type, &g_isf32)) {
|
|
int mov = g_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(base_reg,
|
|
base_disp + total_off),
|
|
areg(D_X0));
|
|
goto dot_done;
|
|
}
|
|
int fsz = (int)(leaf_type
|
|
? leaf_type->size : 8);
|
|
int op = fldloadop(leaf_type, fsz);
|
|
ins2(c, op,
|
|
amem(base_reg,
|
|
base_disp + total_off),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
int lenfld = (n->str && strcmp(n->str, "len") == 0);
|
|
int capfld = (n->str && strcmp(n->str, "cap") == 0);
|
|
int ptrfld = (n->str && strcmp(n->str, "ptr") == 0);
|
|
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
|
|
&& (lenfld || capfld || ptrfld)) {
|
|
if (n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off == 0) {
|
|
/* Not a local — could be `def NAME: str
|
|
* = "lit"`. Sdef-backed strs aren't laid
|
|
* out in memory; emit .ptr/.len from the
|
|
* literal directly, mirroring the bare
|
|
* N_IDENT branch above. Without this we'd
|
|
* load BP+8 (return-address slot) as the
|
|
* "len". */
|
|
{
|
|
/* Same-module-first walk: two
|
|
* same-leaf `def MSG: str = ...`
|
|
* across modules would otherwise
|
|
* fold the wrong strlit's length /
|
|
* label into `MSG.len` / `MSG.ptr`
|
|
* (sister of wwstage deflookuprhs
|
|
* #4c). */
|
|
Sdef *s;
|
|
for (s = sdefs; s; s = s->next) {
|
|
if (strcmp(s->name,
|
|
n->lhs->str) != 0)
|
|
continue;
|
|
if (sdef_mod_match(c, s))
|
|
break;
|
|
}
|
|
if (s == NULL) {
|
|
for (s = sdefs; s;
|
|
s = s->next)
|
|
if (strcmp(s->name,
|
|
n->lhs->str)
|
|
== 0)
|
|
break;
|
|
}
|
|
if (s != NULL) {
|
|
if (ptrfld) {
|
|
const char *lab =
|
|
intern_strlit(c,
|
|
s->bytes,
|
|
s->len);
|
|
ins2(c, A_LEAQ,
|
|
asym(lab),
|
|
areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
aimm((long long)
|
|
s->len),
|
|
areg(D_AX));
|
|
}
|
|
goto dot_done;
|
|
}
|
|
}
|
|
/* Top-level str/slice `let` — load
|
|
* the field through &name(SB). Same
|
|
* pattern as the bare N_IDENT load. */
|
|
if (let_islet(n->lhs->str)) {
|
|
int delta = ptrfld ? 0
|
|
: (lenfld ? 8 : 16);
|
|
ins2(c, A_LEAQ,
|
|
masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_CX, delta),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
|
|
ins2(c, A_MOVQ, amem(D_BP, off + delta),
|
|
areg(D_AX));
|
|
} else {
|
|
/* Evaluate the str/slice expression — leaves
|
|
* the full (AX=ptr, BX=len, CX=cap) header
|
|
* (cgslicehdr) for an indexed element / non-ident
|
|
* base. .ptr returns AX, .len shuffles BX→AX,
|
|
* .cap shuffles CX→AX. The .cap shuffle is the
|
|
* #13 read-fix (sibling of the #20 store): pre-fix
|
|
* the else-arm handled only .len, so `t[i].cap`
|
|
* fell through returning AX=.ptr. */
|
|
cgexpr(c, n->lhs, locals);
|
|
if (lenfld)
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_AX));
|
|
else if (capfld)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
if (u && u->kind == TY_ARRAY && n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (lenfld) {
|
|
ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX));
|
|
break;
|
|
}
|
|
if (ptrfld) {
|
|
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
|
|
break;
|
|
}
|
|
}
|
|
/* tuple positional field access: t.0, t.1, ... */
|
|
if (u && u->kind == TY_TUPLE && n->lhs->kind == N_IDENT && n->str) {
|
|
int idx = 0;
|
|
for (const char *q = n->str; *q; q++) idx = idx * 10 + (*q - '0');
|
|
Tparam *tp = u->params;
|
|
int foff = 0;
|
|
while (idx > 0 && tp) {
|
|
if (tp->type) foff += (int)tp->type->size;
|
|
tp = tp->next;
|
|
idx--;
|
|
}
|
|
if (tp != NULL) {
|
|
int fsz = (int)(tp->type ? tp->type->size : 8);
|
|
Type *fu = (tp->type && tp->type->kind == TY_NAMED)
|
|
? tp->type->under : tp->type;
|
|
int op = fldloadop(tp->type, fsz);
|
|
int off = localfind(locals, n->lhs->str);
|
|
/* f64/f32 tuple field must ride X0 via MOVSD/MOVSS;
|
|
* the integer fldloadop left it in AX (#103 FACE Z).
|
|
* Mirrors the struct-field float load at cgen.c:1462,
|
|
* 1838 (the #96 pattern). */
|
|
int tup_isf32 = 0;
|
|
if (fld_isfloat(tp->type, &tup_isf32)) {
|
|
int mov = tup_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov, amem(D_BP, off + foff),
|
|
areg(D_X0));
|
|
break;
|
|
}
|
|
/* str IS []u8 — load (ptr, len, cap) into
|
|
* (AX, BX, CX), the canonical slice-header ABI,
|
|
* so chains like `t.1.len` propagate through the
|
|
* slice-rhs convention (#1/Phase 3 collapse).
|
|
* UNLIKE the field arms there is no slice-element
|
|
* sibling here, so the triple is hand-authored;
|
|
* base is BP (frame, not a target reg) so the
|
|
* canonical ptr/len/cap order has no clobber risk. */
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + foff + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + foff + 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + foff + 16), areg(D_CX));
|
|
break;
|
|
}
|
|
ins2(c, op, amem(D_BP, off + foff), areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
/* real struct field: load at struct_base + field_off.
|
|
* Base is either a local frame slot (off(BP)) or a top-
|
|
* level let global (&name(SB) into CX); we resolve which
|
|
* once and then share the field-walk code. */
|
|
if (u && u->kind == TY_STRUCT && n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
int is_global = 0;
|
|
int base_reg = D_BP;
|
|
int base_disp = off;
|
|
/* #129 A.2: struct-typed defs now also resolve via
|
|
* LEAQ name(SB) (paralleling lets). Pre-A.2 the
|
|
* `def_isstructdef` arm fell through to the default
|
|
* BP-relative path with off=0, emitting `MOV (BP),`
|
|
* which reads the stack frame's first slot instead
|
|
* of the def's data section. */
|
|
if (off == 0 && (let_islet(n->lhs->str)
|
|
|| def_isstructdef(n->lhs->str))) {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX));
|
|
is_global = 1;
|
|
base_reg = D_CX;
|
|
base_disp = 0;
|
|
}
|
|
for (Tfield *f = u->fields; f; f = f->next) {
|
|
if (strcmp(f->name, n->str) != 0) continue;
|
|
/* tagged-union field: load AX=tag, DX=val0,
|
|
* CX=val1, R8=val2 (CX last, since for globals
|
|
* CX is also the base addr; load R8 before CX
|
|
* so the base address survives the +24 read).
|
|
* Mirrors the tagged-return ABI so the let-init
|
|
* / match dispatch shapes just work. The val2
|
|
* word fires for slice-variant tagged-unions
|
|
* (slot = 8 tag + 24 slice header = 32B). */
|
|
Type *tag_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (tag_fu && tag_fu->kind == TY_TAGGED) {
|
|
int fo = base_disp + (int)f->offset;
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, fo + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, fo + 8), areg(D_DX));
|
|
if (tag_fu->size > 24)
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, fo + 24),
|
|
areg(D_R8));
|
|
if (tag_fu->size > 16)
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, fo + 16),
|
|
areg(D_CX));
|
|
(void)is_global;
|
|
break;
|
|
}
|
|
/* str IS []u8 — same 3-word {ptr,len,cap} as a slice
|
|
* field: load (ptr, len, cap) into (AX, BX, CX) so the
|
|
* value flows through the slice-rhs convention. str
|
|
* folds onto the slice arm (#1/Phase 3 collapse).
|
|
* base_reg may be CX for globals; load .cap LAST so
|
|
* the base survives the earlier reads. */
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if ((str_fu && str_fu->kind == TY_SLICE) ||
|
|
type_isstr(f->type)) {
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 16),
|
|
areg(D_CX));
|
|
break;
|
|
}
|
|
/* f64/f32 field: route through X0 (MOVSD/MOVSS).
|
|
* Loading via MOVQ AX would put the bits in the
|
|
* integer reg, and any downstream consumer that
|
|
* reads X0 (arg pass, return, arithmetic) would see
|
|
* stale data. */
|
|
int e_isf32 = 0;
|
|
if (fld_isfloat(f->type, &e_isf32)) {
|
|
int mov = e_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(base_reg, base_disp + (int)f->offset),
|
|
areg(D_X0));
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int op = fldloadop(f->type, fsz);
|
|
ins2(c, op,
|
|
amem(base_reg, base_disp + (int)f->offset),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
/* pointer-to-slice/str field: deref and read pseudo-field.
|
|
* Used by helpers like rt_appendu8(s: *[]u8, v: u8). dot_lhs
|
|
* gates the N_IDENT check so `(*p).len` (parser N_UN(STAR,
|
|
* IDENT)) emits the same load as `p.len` after the case-top
|
|
* retarget. */
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *inner = type_chase_named(u->sub);
|
|
if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR)
|
|
&& (lenfld || capfld || ptrfld)
|
|
&& dot_lhs && dot_lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, dot_lhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
|
|
int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
|
|
ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_AX));
|
|
break;
|
|
}
|
|
}
|
|
/* pointer-to-struct field: deref and load. Common pattern:
|
|
* fn move(p: *point) ... { p.x += dx; ... }
|
|
* dot_lhs gates this branch so both `p.f` (n->lhs is IDENT)
|
|
* and `(*p).f` (n->lhs is N_UN(STAR, IDENT), retargeted to
|
|
* inner IDENT at case-top) emit the same load sequence.
|
|
*
|
|
* type_chase_named (#22): `type b = a;` inside the pointer
|
|
* (`*b`) leaves a single peel still at TY_NAMED. Bites the
|
|
* strings.tokenize wrapper shape — caller signature
|
|
* `next_token(s: *strings.tokenizer)` where strings.tokenizer
|
|
* aliases bytes.tokenizer. */
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *inner = type_chase_named(u->sub);
|
|
if (inner && inner->kind == TY_STRUCT
|
|
&& dot_lhs && dot_lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, dot_lhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
|
|
for (Tfield *f = inner->fields; f; f = f->next) {
|
|
if (strcmp(f->name, n->str) != 0) continue;
|
|
/* tagged-union field through *struct: BX
|
|
* already holds the *struct pointer. Load
|
|
* the four payload regs from (BX, f->offset)
|
|
* — BX is not a target (AX/DX/CX/R8), so
|
|
* load order is harmless. Mirrors the direct-
|
|
* struct branch above so consumers see the
|
|
* same tagged-return register shape
|
|
* regardless of pointer rooting. Pre-#28 fell
|
|
* through to fldloadop and dropped the
|
|
* payload words. */
|
|
Type *ptag_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (ptag_fu && ptag_fu->kind == TY_TAGGED) {
|
|
int fo = (int)f->offset;
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 8),
|
|
areg(D_DX));
|
|
if (ptag_fu->size > 16)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 16),
|
|
areg(D_CX));
|
|
if (ptag_fu->size > 24)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 24),
|
|
areg(D_R8));
|
|
break;
|
|
}
|
|
/* str IS []u8 — same 3-word {ptr,len,cap} as a
|
|
* slice field through *struct: load (ptr, len,
|
|
* cap) into (AX, BX, CX). BX holds the *struct
|
|
* pointer, so load .len LAST — the earlier loads
|
|
* still index off the original base. str folds
|
|
* onto the slice arm (#1/Phase 3 collapse). */
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if ((str_fu && str_fu->kind == TY_SLICE) ||
|
|
type_isstr(f->type)) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 8),
|
|
areg(D_BX));
|
|
break;
|
|
}
|
|
/* f64/f32 field via *struct: load into X0.
|
|
* BX already holds the struct pointer from
|
|
* the MOVQ amem(D_BP,off) above. */
|
|
int f_isf32 = 0;
|
|
if (fld_isfloat(f->type, &f_isf32)) {
|
|
int mov = f_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_BX, (int)f->offset),
|
|
areg(D_X0));
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int op = fldloadop(f->type, fsz);
|
|
ins2(c, op,
|
|
amem(D_BX, (int)f->offset),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* Chained N_DOT through a *struct field. cgexpr lhs leaves
|
|
* AX = the inner *struct pointer; load the requested field
|
|
* with a single MOVQ. Without this, returning `o.p.val`
|
|
* silently leaves AX = o.p (the pointer) and the outer
|
|
* cast/use sees the pointer instead of the dereferenced
|
|
* field. (Surfaced building ww-w6l.) */
|
|
if (n->lhs->kind == N_DOT) {
|
|
Type *lt = n->lhs->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
if (lu && lu->kind == TY_PTR && lu->sub) {
|
|
Type *inner = lu->sub;
|
|
if (inner->kind == TY_NAMED) inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
for (Tfield *f = inner->fields; f; f = f->next) {
|
|
if (strcmp(f->name, n->str) != 0) continue;
|
|
cgexpr(c, n->lhs, locals); /* AX = inner ptr */
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
/* str IS []u8 — same 3-word {ptr,len,cap} as a
|
|
* slice field: load (ptr, len, cap) into
|
|
* (AX, BX, CX). AX is the *struct base, so
|
|
* load .ptr (which targets AX) LAST. str folds
|
|
* onto the slice arm (#1/Phase 3 collapse). */
|
|
if ((fu && fu->kind == TY_SLICE) ||
|
|
type_isstr(ft)) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 0),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
/* f64/f32 chained field: read into X0. */
|
|
int g_isf32 = 0;
|
|
if (fld_isfloat(ft, &g_isf32)) {
|
|
int mov = g_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_AX, (int)f->offset),
|
|
areg(D_X0));
|
|
goto dot_done;
|
|
}
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int op = fldloadop(ft, fsz);
|
|
ins2(c, op, amem(D_AX, (int)f->offset),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* `arr[i].field` — element-then-field through a `[N]*S` /
|
|
* `[N]S` (and slice/`*[N]S`) base. One branch covers both
|
|
* shapes: compute `&arr[i]` into BX, then either deref
|
|
* (`*Struct` element) or move-to-AX (value `Struct` element),
|
|
* so the leaf load is `(field.offset)(AX)` either way.
|
|
* Bypasses cgindex deliberately — cgindex's final MOVQ
|
|
* would truncate a value-struct element to 8 bytes. Mirrors
|
|
* selfhost/cmd/wcc/cgenexpr.ww's cgdot N_INDEX-lhs branch. */
|
|
if (n->lhs && n->lhs->kind == N_INDEX && n->lhs->lhs
|
|
&& n->lhs->lhs->kind == N_IDENT) {
|
|
Node *idxbase = n->lhs->lhs;
|
|
Type *elemt = n->lhs->type;
|
|
Type *elemu = (elemt && elemt->kind == TY_NAMED)
|
|
? elemt->under : elemt;
|
|
Type *struct_t = NULL;
|
|
int viaptr = 0;
|
|
if (elemu && elemu->kind == TY_PTR) {
|
|
Type *inner = elemu->sub;
|
|
if (inner && inner->kind == TY_NAMED)
|
|
inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
struct_t = inner;
|
|
viaptr = 1;
|
|
}
|
|
} else if (elemu && elemu->kind == TY_STRUCT) {
|
|
struct_t = elemu;
|
|
}
|
|
if (struct_t) {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = struct_t->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, n->str) == 0)
|
|
{ f = fl; break; }
|
|
Type *bt = idxbase->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
int is_arr = bu && bu->kind == TY_ARRAY;
|
|
int is_sl = bu && bu->kind == TY_SLICE;
|
|
int is_ptr = bu && bu->kind == TY_PTR;
|
|
int off = localfind(locals, idxbase->str);
|
|
/* #21 (READ twin of #11): a module-GLOBAL base
|
|
* makes localfind return 0, so the field-offset-
|
|
* aware branch was skipped and `g[i].field` fell to
|
|
* a generic index-load that drops f->offset (reads
|
|
* element[i] at offset 0). Resolve the global the
|
|
* same way the N_INDEX arm does (let_islet ||
|
|
* def_isarraydef) and dispatch the base load by
|
|
* shape: array -> LEAQ name(SB) (the symbol IS the
|
|
* storage), slice/ptr -> MOVQ name(SB) (the symbol's
|
|
* first word IS the .ptr). */
|
|
int isglobal = (off == 0)
|
|
&& (let_islet(idxbase->str)
|
|
|| def_isarraydef(idxbase->str));
|
|
if (f != NULL && (is_arr || is_sl || is_ptr)
|
|
&& (off != 0 || isglobal)) {
|
|
int esz = (int)elemt->size;
|
|
cgexpr(c, n->lhs->rhs, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (isglobal && is_arr)
|
|
ins2(c, A_LEAQ,
|
|
masym(c, idxbase->str),
|
|
areg(D_BX));
|
|
else if (isglobal)
|
|
ins2(c, A_MOVQ,
|
|
masym(c, idxbase->str),
|
|
areg(D_BX));
|
|
else if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off), areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0), areg(D_AX));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_AX));
|
|
int foff = (int)f->offset;
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
/* #270-1a: an `[N]T`-typed field of an
|
|
* array element (`a[i].m[j]`) — leave the
|
|
* field's ADDRESS, a base for the outer
|
|
* index, NEVER deref. AX holds &a[i]; the
|
|
* field address is &a[i]+foff. The #135
|
|
* read-side for `d.m[i]`, applied to an
|
|
* array-element base. Without this an array
|
|
* field fell to fldloadop below and loaded
|
|
* its first 8 bytes as a value → garbage
|
|
* base → SEGFAULT in the outer index. */
|
|
if (fu && fu->kind == TY_ARRAY) {
|
|
if (foff != 0)
|
|
ins2(c, A_ADDQ,
|
|
aimm(foff),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
if (fu && (fu->kind == TY_STR
|
|
|| fu->kind == TY_SLICE)) {
|
|
/* str/slice: the 3-word {ptr,len,cap}
|
|
* slice header (#1). AX holds the
|
|
* element base, so load .ptr (which
|
|
* targets AX) LAST. Matches the
|
|
* caseB *struct slice arm and
|
|
* cgslicehdr(D_AX). */
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, foff + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, foff + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, foff + 0),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
int g_isf32 = 0;
|
|
if (fld_isfloat(ft, &g_isf32)) {
|
|
int mov = g_isf32
|
|
? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_AX, foff),
|
|
areg(D_X0));
|
|
goto dot_done;
|
|
}
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int op = fldloadop(ft, fsz);
|
|
ins2(c, op, amem(D_AX, foff),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
}
|
|
/* Nested module-qualified field where the chain didn't fold to
|
|
* a known shape (typical when w6c runs on a single file with
|
|
* `use mod;` but no driver concatenation — the body's enum /
|
|
* struct hasn't been seen). Emit `MOVQ <leaf>(SB), AX` so the
|
|
* linker surfaces a clean undefined-symbol error on the leaf
|
|
* — mirrors the bare-N_IDENT unresolved fallback used by
|
|
* single-segment N_DOTs. Keeps cstage / wwstage byte-aligned
|
|
* on the cgen-match isolation probes. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->str) {
|
|
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
|
|
break;
|
|
}
|
|
/* Non-ident / untyped-str base pseudo-field: e.g. `"abc".len`
|
|
* / `"abc".ptr`. A string literal is TY_UNTYPED_STR, not
|
|
* TY_STR, so it misses the typed slice/str gate above and
|
|
* lands here. cgexpr leaves (AX=ptr, BX=len); `.ptr` keeps AX,
|
|
* `.len` shuffles BX→AX. Mirrors wwstage cgdot's catch-all
|
|
* (selfhost/cmd/wcc/cgenexpr.ww). #14. */
|
|
cgexpr(c, n->lhs, locals);
|
|
if (lenfld)
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
dot_done:
|
|
break;
|
|
}
|
|
case N_INDEX: {
|
|
/* Scaled indexing for slice/array/str/ptr-to-T.
|
|
* Element size is 1 for u8/str, otherwise type's natural size.
|
|
* For `*[N]T` drill through to the array so esz/esub reflect
|
|
* T, not sizeof(array). */
|
|
Type *bt = n->lhs ? n->lhs->type : NULL;
|
|
/* #128b: module-qualified `mod.arr[i]` — n->lhs is N_DOT and
|
|
* its type is NULL (SK_USE-bound module ident). Look up the
|
|
* imported let's type via let_var_type so esz/esub reflect
|
|
* the imported array's element width instead of falling to
|
|
* the esz=1 default (→ MOVZBQ wrong-width load). Sister of
|
|
* the dst-side cg_dotbase_addr branch that emits LEAQ for
|
|
* the base address. */
|
|
if ((bt == NULL || bt == ty_err)
|
|
&& n->lhs && n->lhs->kind == N_DOT
|
|
&& n->lhs->str
|
|
&& let_islet(n->lhs->str)) {
|
|
bt = let_var_type(n->lhs->str);
|
|
}
|
|
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
Type *eff = idx_eff(bt);
|
|
int esz = 1;
|
|
if (eff && eff->sub) esz = (int)eff->sub->size;
|
|
Type *esub = eff ? eff->sub : NULL;
|
|
Type *esubu = (esub && esub->kind == TY_NAMED)
|
|
? esub->under : esub;
|
|
int elem_tagged = esubu && esubu->kind == TY_TAGGED;
|
|
|
|
if (n->lhs->kind == N_IDENT && u) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
/* #129 A.3: array-typed defs now have DATA storage; the
|
|
* LEAQ name(SB) base-load must fire for them too, not
|
|
* just let_islet. Parallel to A.2's def_isstructdef
|
|
* gate at the N_DOT direct-struct-ident arm. */
|
|
int isglobal = (off == 0) && (let_islet(n->lhs->str)
|
|
|| def_isarraydef(n->lhs->str));
|
|
cgexpr(c, n->rhs, locals); /* idx → AX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
/* base address into BX. Top-level array → LEAQ
|
|
* name(SB); top-level ptr → MOVQ name(SB) (the symbol
|
|
* holds the pointer); locals route off BP. */
|
|
if (isglobal && u->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_BX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ, masym(c, n->lhs->str),
|
|
areg(D_BX));
|
|
} else if (u->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX));
|
|
} else {
|
|
/* slice/str/ptr: ptr field is at off+0 */
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
/* #156 (PREREQ-1 read-half): element is itself an array
|
|
* ([N][M]T → element [M]T). This index yields the sub-
|
|
* array's ADDRESS, not a loaded value — the outer index
|
|
* adds its own offset and only the final scalar element
|
|
* dereferences. Sister of #135 (N_DOT-base-on-[N]T-field
|
|
* needs ADDRESS). BX holds base+idx*esz; move it to AX (the
|
|
* value-result reg). Gated on TY_ARRAY element so 1D arrays
|
|
* are byte-identical (no 2D consumer pre-#156). */
|
|
if (esubu && esubu->kind == TY_ARRAY) {
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
}
|
|
/* str/slice element: load the full (ptr, len, cap) header
|
|
* into (AX, BX, CX) — both are 24B since #1, so the cap
|
|
* word must survive. Kind-gate on type_isstr||type_isslice,
|
|
* never size==24: a >16B struct is 24B+ too but takes the
|
|
* struct-copy path, not this 3-word header load (#10).
|
|
* Base is BX. */
|
|
if (u->sub && (type_isstr(u->sub) || type_isslice(u->sub))) {
|
|
cgslicehdr(c, D_BX);
|
|
break;
|
|
}
|
|
/* tagged element: load slot words into (AX=tag,
|
|
* DX=val0, CX=val1, R8=val2) — matches the
|
|
* tagged-return ABI so let-init / match / call-arg
|
|
* paths consume it without spilling. Nullable folded
|
|
* element is one word in AX (caller treats it as a
|
|
* pointer). */
|
|
if (elem_tagged) {
|
|
int ssz = (int)esubu->size;
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, amem(D_BX, 24),
|
|
areg(D_R8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, amem(D_BX, 16),
|
|
areg(D_CX));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, amem(D_BX, 8),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
/* float element → MOVSS/MOVSD into X0: the consumer's
|
|
* ADDSD/MOVSD spill machinery already expects X0, but the
|
|
* integer fldloadop below would leave it in AX and the SSE
|
|
* side reads stale (#119). Float-ness from esub — the same
|
|
* type the esz above reads. Twin of the scalar-float global
|
|
* load at cgen.c:2014. */
|
|
if (type_isfloat(esub)) {
|
|
int op = type_isf32(esub) ? A_MOVSS : A_MOVSD;
|
|
ins2(c, op, amem(D_BX, 0), areg(D_X0));
|
|
break;
|
|
}
|
|
int load_op = fldloadop(esub, esz);
|
|
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
/* Fallback: evaluate base (treat as plain pointer) and
|
|
* dereference at base+idx. Pick the load opcode by element
|
|
* size — `b.data[i]` on a *u8 must read 1 byte, not 8.
|
|
*
|
|
* Scale the index in a register before pushing, because
|
|
* IMULQ on a memory operand isn't currently encoded by w6a
|
|
* (modrm bits use mod=3 register form).
|
|
*
|
|
* #135: N_DOT base on a `[N]T`-typed field needs the field's
|
|
* ADDRESS, not its value. cgexpr on N_DOT would auto-deref and
|
|
* load the field's 8-byte value as if it were a pointer — the
|
|
* symmetric READ-side of the LHS bug at the cgassign sites.
|
|
* cg_dotbase_addr emits the address inline. */
|
|
cgexpr(c, n->rhs, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (!cg_dotbase_addr(c, n->lhs, D_AX, locals))
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
|
|
/* #156 (PREREQ-1 read-half): array element → AX already holds
|
|
* &elem (base+idx*esz); a nested index adds its offset and
|
|
* dereferences. See the N_IDENT arm above. */
|
|
if (esubu && esubu->kind == TY_ARRAY)
|
|
break;
|
|
/* str/slice element via fallback base: load the full (ptr, len,
|
|
* cap) header into (AX, BX, CX). Kind-gate on type_isstr||
|
|
* type_isslice, never size==24 (see Site A). Base is AX. */
|
|
if (u && u->sub && (type_isstr(u->sub) || type_isslice(u->sub))) {
|
|
cgslicehdr(c, D_AX);
|
|
break;
|
|
}
|
|
/* tagged element via fallback base: AX holds the element
|
|
* address — copy to BX (the load into AX clobbers it), then
|
|
* load slot words. */
|
|
if (elem_tagged) {
|
|
int ssz = (int)esubu->size;
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
/* float element via fallback base → X0 (see Site A, #119). The
|
|
* base address is in AX; MOVSS/MOVSD reads the element into X0. */
|
|
if (type_isfloat(esub)) {
|
|
int op = type_isf32(esub) ? A_MOVSS : A_MOVSD;
|
|
ins2(c, op, amem(D_AX, 0), areg(D_X0));
|
|
break;
|
|
}
|
|
{
|
|
int load_op = fldloadop(esub, esz);
|
|
ins2(c, load_op, amem(D_AX, 0), areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
case N_SLICE: {
|
|
/* base[lo:hi] as a slice value. Leaves the triple in
|
|
* (AX=base+lo*esz, BX=hi-lo, CX=base_cap-lo) so callers can
|
|
* route to a slice slot, return, or arg with the same ABI.
|
|
* cap is the storage remaining to the base's end (#20,
|
|
* Go/Hare-identical), via cg_base_cap. ptr advances by BYTES
|
|
* (lo*esz, #76; ref/hare/rt/ensure.ha:30 membsz-unit); esz
|
|
* from the type table, mirroring the N_INDEX idiom. */
|
|
Node *base = n->lhs;
|
|
Node *lo = n->rhs;
|
|
Node *hi = n->cond;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
/* esz from the type table for an N_IDENT base (#76) or an
|
|
* N_DOT array/slice-field base (#252: a struct-field slice
|
|
* `s.obuf[lo:hi]` must scale by the field's element width, not
|
|
* stay esz=1 — silently wrong for non-u8 elements). Other
|
|
* non-ident bases stay esz=1 (unscaled) -- #76 residual,
|
|
* non-ident cluster #74. */
|
|
int esz = (base && (base->kind == N_IDENT
|
|
|| base->kind == N_DOT || base->kind == N_ARRLIT)
|
|
&& bu && bu->sub)
|
|
? (int)bu->sub->size : 1;
|
|
if (base && base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) && let_islet(base->str);
|
|
if (isglobal && bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ, masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
|
|
}
|
|
} else if (base && base->kind == N_ARRLIT && bu
|
|
&& bu->kind == TY_ARRAY) {
|
|
/* #31: an array LITERAL base — the desugared one-step
|
|
* `let xs: []T = [..]` borrow (the ONLY context that
|
|
* reaches here; call-arg/return/assign loud-reject at the
|
|
* checker, reject_arrlit_borrow, deferred to #33). The
|
|
* literal has no storage address — cgexpr would leave
|
|
* AX=garbage and the borrow's .ptr would dangle.
|
|
* Materialise it into a FRESH per-borrow @slicescr stack
|
|
* slot (distinct slot per borrow: a borrow's backing must
|
|
* stay live for the slice's lifetime, so it can't share a
|
|
* cached SSoT slot the way @aggargscr/@tagscr — drained/
|
|
* consumed in place — do; two live borrows would otherwise
|
|
* alias one backing). Reuses local_alloc + the shared
|
|
* array-init fill; the checker re-stamped base->type to
|
|
* [count]T (#25) so the fill stores at the declared
|
|
* element width.
|
|
*
|
|
* Escape (WHY, rob): a `let xs: []T = [..]; return xs;`
|
|
* returns a slice pointing at this frame slot, freed on
|
|
* return = dangling. This is IDENTICAL to the pre-existing
|
|
* named-array borrow (`let a: [N]T = [..]; return a;`) and
|
|
* is Hare-consistent: ww has no escape analysis, no GC, no
|
|
* heap promotion — borrowing a local past its frame is a
|
|
* programmer footgun, not promoted. Don't "fix" this
|
|
* expecting heap promotion; ww deliberately doesn't, same
|
|
* as Hare. */
|
|
int cnt = (int)bu->alen;
|
|
int bsz = (bu->sub ? (int)bu->sub->size : 1) * cnt;
|
|
if (bsz < 1) bsz = 1;
|
|
int scr = local_alloc(c, &locals, "@slicescr", bsz,
|
|
cg_frame);
|
|
cg_arrlit_fill_bp(c, &locals, bu, base, scr);
|
|
ins2(c, A_LEAQ, amem(D_BP, scr), areg(D_AX));
|
|
} else if (base) {
|
|
/* #252: N_DOT `[N]T`-field base → field ADDRESS via
|
|
* cg_dotbase_addr (LEAQ), not the auto-deref VALUE load
|
|
* cgexpr would emit. Sibling of the #135 read-side. */
|
|
if (!cg_dotbase_addr(c, base, D_AX, locals))
|
|
cgexpr(c, base, locals);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (lo) cgexpr(c, lo, locals);
|
|
else cgexpr_int(c, 0);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (hi) {
|
|
cgexpr(c, hi, locals);
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
cgexpr_int(c, (long long)bu->alen);
|
|
} else if (base && base->kind == N_IDENT && bu &&
|
|
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) && let_islet(base->str);
|
|
if (isglobal) {
|
|
ins2(c, A_LEAQ, masym(c, base->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8),
|
|
areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
/* ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit).
|
|
* DX=lo*esz; CX=lo PRESERVED for len + cap (#20). */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_DX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_DX));
|
|
ins2(c, A_ADDQ, areg(D_DX), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_BX));
|
|
/* cap = base_cap - lo (#20); CX=lo, BX=len here. */
|
|
if (cg_base_cap(c, base, bu, locals, D_DX)) {
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_CX));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
}
|
|
break;
|
|
}
|
|
case N_TUPLE:
|
|
/* #241: a literal tuple rvalue `(a, b)` is a value — pack its
|
|
* elements into the register cursor (mirror cgreturn's N_TUPLE
|
|
* arm) so a let-bind / destructure consumer reads every element,
|
|
* not just AX = 0 from the default arm below. */
|
|
cg_tuple_lit_to_cursor(c, &locals, n);
|
|
break;
|
|
default:
|
|
cgexpr_int(c, 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
|
{
|
|
if (n == NULL) return;
|
|
switch (n->kind) {
|
|
case N_BLOCK: {
|
|
/* Save/restore the locals head across the block (post-#27).
|
|
* Inner-scope `let` bindings prepend to *locals via localoff;
|
|
* without this restore, the prepended stubs leak into sibling
|
|
* and ancestor scopes, and localfind (head-first) returns the
|
|
* inner binding's offset for an identifier that semantically
|
|
* belongs to the outer scope. The frame is left grown — slot
|
|
* lifetimes don't overlap with later siblings observably (the
|
|
* popped stubs' offsets are no longer reachable by name), but
|
|
* we don't reclaim the frame bytes; that's the conservative
|
|
* choice C compilers make for simple lowering.
|
|
*
|
|
* cgfn iterates fn->body->list directly to bypass this
|
|
* save/restore at the function's outermost block — defers
|
|
* (and the implicit-return epilogue) need locals intact. */
|
|
Local *saved = *locals;
|
|
for (Node *s = n->list; s; s = s->next)
|
|
cgstmt(c, s, locals, frame);
|
|
*locals = saved;
|
|
break;
|
|
}
|
|
case N_EXPRSTMT:
|
|
cgexpr(c, n->lhs, *locals);
|
|
break;
|
|
case N_LET: {
|
|
Type *lt = n->type;
|
|
/* type_chase_named (#22): a chain `type a = struct{...};
|
|
* type b = a;` stacks two TY_NAMED layers. A single peel
|
|
* left `lu` pointing at the inner alias (still TY_NAMED),
|
|
* collapsed the struct/slice/tagged sizing arms to the 8B
|
|
* fallback, and the slot under-allocated the local. */
|
|
Type *lu = type_chase_named(lt);
|
|
/* #43: every composite kind already has its byte size cached in
|
|
* lu->size; route through it instead of re-asserting 16/24 for
|
|
* str/slice and re-reading for the others. */
|
|
int sz = 8;
|
|
if (lu && (lu->kind == TY_ARRAY || lu->kind == TY_SLICE
|
|
|| lu->kind == TY_STR || lu->kind == TY_STRUCT
|
|
|| lu->kind == TY_TUPLE || lu->kind == TY_TAGGED))
|
|
sz = (int)lu->size;
|
|
int off = localoff(c, locals, n->str, sz, frame);
|
|
int isf = cg_isfloat(lt);
|
|
int isf32 = type_isf32(lt);
|
|
/* alloc([], n) initialiser for a slice local: allocate
|
|
* n*esize bytes, build the {ptr, 0, n} header in the slot.
|
|
* Element size comes from the declared slice type.
|
|
*
|
|
* Task #30 graduated the builtin to `([]T | nomem)`. The let
|
|
* declares a bare `[]T`, so the canonical idiom wraps in `!`
|
|
* (abort on OOM) or `?` (propagate nomem to the enclosing
|
|
* fn's tagged return). Task #45 extends the shortcut to also
|
|
* match N_TRYPROP and emit the propret pattern. */
|
|
{
|
|
Node *call = NULL;
|
|
int via_tryunw = 0;
|
|
int via_tryprop = 0;
|
|
if (n->rhs && n->rhs->kind == N_TRYUNW && n->rhs->lhs
|
|
&& n->rhs->lhs->kind == N_CALL) {
|
|
call = n->rhs->lhs;
|
|
via_tryunw = 1;
|
|
} else if (n->rhs && n->rhs->kind == N_TRYPROP
|
|
&& n->rhs->lhs
|
|
&& n->rhs->lhs->kind == N_CALL) {
|
|
call = n->rhs->lhs;
|
|
via_tryprop = 1;
|
|
}
|
|
if (call && lu && lu->kind == TY_SLICE && sz == 24
|
|
&& call->lhs && call->lhs->kind == N_IDENT
|
|
&& strcmp(call->lhs->str, "alloc") == 0
|
|
&& call->list && call->list->kind == N_ARRLIT
|
|
&& call->list->list == NULL
|
|
&& call->list->next
|
|
&& call->list->next->next == NULL) {
|
|
Node *count = call->list->next;
|
|
int esz = (lu->sub) ? (int)lu->sub->size : 1;
|
|
cgexpr(c, count, *locals); /* AX = n */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* save count */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
|
|
ins2(c, A_IMULQ, areg(D_BX), areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
|
ins1(c, A_CALL, asym(ffi_resolve("malloc")));
|
|
if (via_tryunw) {
|
|
char *ok = mklabel(c, "tryunw_ok");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, ok);
|
|
} else if (via_tryprop) {
|
|
/* #45: null = nomem; propagate to the
|
|
* enclosing fn's tagged return. AX = tag
|
|
* of nomem variant in cg_ret_type; epilogue
|
|
* RETs to caller. */
|
|
char *ok = mklabel(c, "tryprop_ok");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(ok));
|
|
Type *r = cg_ret_type;
|
|
if (r && r->kind == TY_NAMED) r = r->under;
|
|
int nidx = cg_tag_for_variant(r, ty_nomem);
|
|
if (nidx < 0) nidx = 1;
|
|
ins2(c, A_MOVQ, aimm(nidx), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
label(c, ok);
|
|
}
|
|
ins1(c, A_POPQ, areg(D_BX)); /* count */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
}
|
|
/* str IS []u8: cgexpr produces (AX=ptr, BX=len, CX=cap);
|
|
* store all three, same as the slice initialiser below.
|
|
* #43 gate via ty_str->size already tracks the 24B bump
|
|
* (#1/Phase 3). */
|
|
if (n->rhs && type_isstr(lt) && sz == (int)ty_str->size) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
/* Tuple initialiser from a function call (#105 / #164/#107),
|
|
* 16B (two eightbytes) or 32B (scalar/float + slice/str header).
|
|
* Each element rides its SysV class: a float its SSE cursor reg
|
|
* (X0,X1 = tuple_sse_seq), an integer/ptr word its INTEGER cursor
|
|
* reg (tuple_rseq), a slice/str its 3-word {ptr,len,cap} header
|
|
* over consecutive INTEGER cursor regs — INDEPENDENT counters,
|
|
* so the RETURN leaves floats in X0/X1 and integer words in
|
|
* AX/DX/CX/R8. A blanket MOVQ spill would store garbage where a
|
|
* float rode and the #103-FACE-Z field read (MOVSD-from-slot)
|
|
* would see it. tuple_store routes each element from its real
|
|
* class into its positional slot (eoff steps by the element's
|
|
* slot size: a slice/str takes its 24B header); the same split
|
|
* drives the destructure / reassign sites. */
|
|
if (n->rhs && lu && lu->kind == TY_TUPLE
|
|
&& (sz == 16 || sz == 32)) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
int gpcur = 0, ssecur = 0, eoff = 0, ef32;
|
|
for (Tparam *p = lu->params; p; p = p->next) {
|
|
Type *pu = (p->type && p->type->kind == TY_NAMED)
|
|
? p->type->under : p->type;
|
|
int wide = pu && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR);
|
|
int isflt = fld_isfloat(p->type, &ef32);
|
|
tuple_store(c, p->type, wide, gpcur, ssecur,
|
|
off + eoff);
|
|
if (isflt)
|
|
ssecur++;
|
|
else
|
|
gpcur += tuple_ebytes(wide);
|
|
eoff += wide ? (int)pu->size : 8;
|
|
}
|
|
break;
|
|
}
|
|
/* #38b residual (rule 7): `let w: T = f()?;` / `f()!` where f
|
|
* returns an sret-classified tagged union — the unwrap would
|
|
* need a mem-based read of the sret slot. The N_LET arms
|
|
* below have no TRYUNW/TRYPROP shape for a >8B lt, so the
|
|
* rhs was SILENTLY dropped (no CALL emitted; wwstage's cglet
|
|
* default does cgexpr and hits the cgtryunw/cgtryprop gates —
|
|
* this keeps acceptance symmetric, rule 10). */
|
|
if (n->rhs
|
|
&& (n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP)
|
|
&& n->rhs->lhs && n->rhs->lhs->kind == N_CALL
|
|
&& cg_sret_retsize(n->rhs->lhs->type) > 0)
|
|
fatal("#38b: `?`/`!` on an sret-class call result "
|
|
"unwired (mem-based unwrap is a #40-family "
|
|
"follow-up)");
|
|
/* Tagged-union initialiser. Delegates to cg_widen_tagged_store,
|
|
* which handles nullable fold, tagged→tagged (with tag remap
|
|
* when variant indices differ), struct payload (ident or
|
|
* literal — field-by-field at slot+8+field_off), str payload,
|
|
* and scalar payload (with zero-pad to the slot size).
|
|
*
|
|
* #38b: an sret-classified tagged CALL result is in memory,
|
|
* not the cursor — an exact-type receive falls through to the
|
|
* generic sret receive below (the let's slot IS the dest); a
|
|
* widening receive needs mem-to-mem tag-remap (#40, unwired). */
|
|
if (n->rhs && lu && lu->kind == TY_TAGGED) {
|
|
int rhs_sret_call = n->rhs->kind == N_CALL
|
|
&& cg_sret_retsize(n->rhs->type) > 0;
|
|
if (!rhs_sret_call) {
|
|
cg_widen_tagged_store(c, locals, lu, n->rhs,
|
|
D_BP, off, sz);
|
|
break;
|
|
}
|
|
Type *ru = type_chase_named(n->rhs->type);
|
|
if (!(ru == lu || type_eq(n->rhs->type, lt)))
|
|
fatal("#40: sret-class call result cannot be "
|
|
"widened into a tagged slot (mem-to-mem "
|
|
"widen unwired)");
|
|
}
|
|
/* Every slice initialiser routes here — fn-return, slice
|
|
* ident, slice param, and sub-slice `buf[lo:hi]`. cgexpr
|
|
* leaves (AX=ptr, BX=len, CX=cap); store all three. The
|
|
* sub-slice case once had a vestigial inline builder that
|
|
* duplicated cgexpr's N_SLICE path and mishandled global
|
|
* bases; dropping it aligns cstage onto wwstage's shared
|
|
* store path (find-4). Runs after the alloc specialisation
|
|
* above so that keeps its direct {ptr,0,n} shape. */
|
|
if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
/* struct literal initialiser: field-by-field store via the
|
|
* shared cg_structlit_fill_bp helper. The literal carries
|
|
* op == TK_ELLIPSIS when the source ends in `..., ...` —
|
|
* helper zero-fills the slot first so unmentioned fields
|
|
* read as 0. Nested struct-typed structlit field values
|
|
* recurse into the helper at the correct offset instead of
|
|
* landing AX = first-qword via cgexpr (#17 silent zero). */
|
|
if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu
|
|
&& lu->kind == TY_STRUCT) {
|
|
cg_structlit_fill_bp(c, locals, lu, n->rhs, off);
|
|
break;
|
|
}
|
|
/* sret receive (#23 / #10 Fold B): the let's own slot IS the
|
|
* caller-prealloc dest; the call writes through hidden RDI
|
|
* directly into our slot, no AX/DX/CX shuffle. Set
|
|
* cg_sret_dest_off so the nested cgexpr → N_CALL path emits
|
|
* `LEAQ off(BP), RDI` before CALL. Keys on cg_sret_retsize
|
|
* (the shared sret SSoT), NOT a kind — so an over-cap tuple
|
|
* return (Fold A made the callee sret it) materialises its
|
|
* WHOLE slot here exactly like a >24B struct, and t.0/t.1
|
|
* read by offset afterward. */
|
|
if (n->rhs && n->rhs->kind == N_CALL
|
|
&& cg_sret_retsize(lt) > 0) {
|
|
cg_sret_dest_off = off;
|
|
cgexpr(c, n->rhs, *locals);
|
|
cg_sret_dest_off = 0;
|
|
break;
|
|
}
|
|
/* Whole-struct receive for sizes <=24B (call-result rhs).
|
|
* Counterpart of #4's cgreturn ABI: cgexpr leaves
|
|
* AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23], zero-
|
|
* padded to 24B by the producer.
|
|
*
|
|
* ASYMMETRY (do NOT mirror the sender): producer emits three
|
|
* uniform MOVQs into a zero-padded 24B scratch slot; the
|
|
* receiver must write only `sz` bytes — MOVQ for full 8B
|
|
* chunks plus a sized tail (MOVL/MOVW/MOVB) by the *declared*
|
|
* struct size. Otherwise a trailing 1..7-byte chunk would
|
|
* overrun into the next local slot.
|
|
*
|
|
* Tail chunks in {3,5,6,7} (would need shift-and-store from
|
|
* the register) are unreachable under WW struct alignment
|
|
* rules (field aligns force size%align==0); the guard
|
|
* excludes them so they fall through to the existing scalar
|
|
* path rather than emit a stomping MOVQ tail. Sizes >24B also
|
|
* fall through (sret deferred, same constraint as #4). */
|
|
/* #171a: float-bearing struct RECEIVE (the return twin of
|
|
* #165's param recv). cgexpr leaves each float eightbyte in
|
|
* its SSE return reg (X0,X1 = tuple_sse_seq) and each INT
|
|
* eightbyte in its INTEGER return reg (AX,DX = tuple_rseq),
|
|
* on INDEPENDENT cursors per SysV (ref/qbe/amd64/sysv.c retr)
|
|
* — so a float is read from the next XMM regardless of its
|
|
* positional eightbyte (struct{f64,i32}: e0←X0, e1←AX). A
|
|
* qualifying struct's size is maxalign-rounded to a multiple
|
|
* of 8 (an f64 forces align 8), so every eightbyte is a full
|
|
* word — the #169 sized tail (MOVL/MOVB) is unreachable here.
|
|
* struct_float_class gates to qualifying structs; all-int +
|
|
* f32 fall through to the GP recv below (byte-id / #171b). */
|
|
if (n->rhs && n->rhs->kind == N_CALL && lu
|
|
&& lu->kind == TY_STRUCT) {
|
|
int sclass[2], snb;
|
|
if ((snb = struct_float_class(lu, sclass)) > 0) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
int gpcur = 0, ssecur = 0;
|
|
for (int e = 0; e < snb; e++) {
|
|
if (sclass[e]) {
|
|
ins2(c, A_MOVSD,
|
|
areg(tuple_sse_seq[ssecur]),
|
|
amem(D_BP, off + e * 8));
|
|
ssecur++;
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
areg(tuple_rseq[gpcur]),
|
|
amem(D_BP, off + e * 8));
|
|
gpcur++;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (n->rhs && n->rhs->kind == N_CALL && lu
|
|
&& (lu->kind == TY_STRUCT || lu->kind == TY_ARRAY)
|
|
&& sz <= 24
|
|
&& (sz % 8 == 0 || sz % 8 == 1
|
|
|| sz % 8 == 2 || sz % 8 == 4)) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = sz / 8;
|
|
int tail = sz % 8;
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(D_BP, off + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW : A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(D_BP, off + full * 8));
|
|
}
|
|
break;
|
|
}
|
|
/* array literal initialiser: `let xs: [N]T = [a, b, c];`.
|
|
* Walk elements in declaration order, store each at off + i*esz
|
|
* using the right width for the element type. The trailing
|
|
* `...` repeat marker (an N_FIELD with str=="...") fills the
|
|
* remaining slots with the last value.
|
|
*
|
|
* str/slice element (24B = ptr+len+cap, post-#1) needs all
|
|
* three words stored: cgexpr leaves it as (AX=ptr, BX=len,
|
|
* CX=cap), and a single MOVQ from AX would leave .len/.cap as
|
|
* whatever the stack held — silent miscompile (#20/#270 str-
|
|
* slice arm). The per-element store branches on TY_STR/TY_SLICE
|
|
* before falling through to the scalar MOVB/MOVL/MOVQ path.
|
|
* [N]tagged element arrays still land in the multi-word gap
|
|
* (is_agg excludes TY_TAGGED) — tracked as task #12. */
|
|
if (n->rhs && n->rhs->kind == N_ARRLIT && lu
|
|
&& lu->kind == TY_ARRAY) {
|
|
cg_arrlit_fill_bp(c, locals, lu, n->rhs, off);
|
|
break;
|
|
}
|
|
/* Struct ident copy: `let p2: T = p1;` where T is a struct
|
|
* >8B and rhs is a local ident. Pre-fix the path fell
|
|
* through to the `sz == 8` test (false) and emitted
|
|
* nothing — the dst slot read whatever the stack held,
|
|
* presenting as a silent zero copy on a fresh frame.
|
|
* Per-qword MOVQ from src slot to dst slot, with a sized
|
|
* tail (MOVL/MOVB) for natural sizes that aren't
|
|
* 8-aligned (e.g. `struct { i32, i32, i32 }` is 12B).
|
|
* Mirrors the slot-to-slot copy in cg_widen_tagged_store
|
|
* for a TY_STRUCT payload (Task #32). */
|
|
if (n->rhs && n->rhs->kind == N_IDENT && lu
|
|
&& lu->kind == TY_STRUCT && sz > 8) {
|
|
Local *src_l = NULL;
|
|
for (Local *l = *locals; l; l = l->next)
|
|
if (strcmp(l->name, n->rhs->str) == 0) {
|
|
src_l = l; break;
|
|
}
|
|
if (src_l) {
|
|
int soff = src_l->off;
|
|
int k = 0;
|
|
while (k + 8 <= sz) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
k += 8;
|
|
}
|
|
if (k < sz) {
|
|
int tail = sz - k;
|
|
int lop = (tail == 4) ? A_MOVL :
|
|
(tail == 1) ? A_MOVB : A_MOVQ;
|
|
ins2(c, lop,
|
|
amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, lop, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* #265 fold-1/1b (#268): aggregate let-init copy from an
|
|
* ADDRESSABLE rhs. The whole family converges on ONE memcpy
|
|
* loop fed by a per-rhs source-address setup: `*p` (deref,
|
|
* fold-1), an array ident `= s` (struct-ident is the #32 arm
|
|
* above), an N_DOT field `= o.i`, an N_INDEX element `= a[i]`
|
|
* — T a struct or array >8B. Each shape lands the SOURCE
|
|
* ADDRESS in SI; the loop copies sz bytes (lu->size, the #254
|
|
* non-slot-padded ABI extent) slot→slot — a MOVQ run plus a
|
|
* sized MOVL/MOVW/MOVB tail. Pre-fix every non-deref shape was
|
|
* wrong: array-ident/N_DOT truncated to the 8B scalar tail
|
|
* below; N_INDEX scalar-loaded the element address as a value
|
|
* (segfault). Both stages emit the identical sequence
|
|
* (rule-10); the by-value RETURN ABI is fold-2 (#267). The
|
|
* source-addr setups reuse closed machinery: LEAQ-slot (ident),
|
|
* the deref operand (cgexpr), cg_dotchain_addr (#253, N_DOT),
|
|
* the &base[i] spine (#252, N_INDEX). */
|
|
if (n->rhs && lu
|
|
&& (lu->kind == TY_STRUCT || lu->kind == TY_ARRAY)
|
|
&& sz > 8) {
|
|
int havesrc = 0;
|
|
if (n->rhs->kind == N_UN && n->rhs->op == TK_STAR) {
|
|
cgexpr(c, n->rhs->lhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_SI));
|
|
havesrc = 1;
|
|
} else if (n->rhs->kind == N_IDENT) {
|
|
int soff = localfind(*locals, n->rhs->str);
|
|
if (soff != 0) {
|
|
ins2(c, A_LEAQ, amem(D_BP, soff),
|
|
areg(D_SI));
|
|
havesrc = 1;
|
|
/* the laid-out-aggregate globals (#129
|
|
* A.2/A.3): a let, an array def, or a struct
|
|
* def. Struct defs copy here exactly as
|
|
* struct-let globals do; omitting def_is-
|
|
* structdef truncated the def case alone and
|
|
* diverged from wwstage (rule-10). */
|
|
} else if (let_islet(n->rhs->str)
|
|
|| def_isarraydef(n->rhs->str)
|
|
|| def_isstructdef(n->rhs->str)) {
|
|
ins2(c, A_LEAQ, masym(c, n->rhs->str),
|
|
areg(D_SI));
|
|
havesrc = 1;
|
|
}
|
|
} else if (n->rhs->kind == N_DOT) {
|
|
if (cg_dotchain_addr(c, n->rhs, D_SI, *locals))
|
|
havesrc = 1;
|
|
} else if (n->rhs->kind == N_INDEX) {
|
|
Node *base = n->rhs->lhs;
|
|
Node *idx = n->rhs->rhs;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
if (base && base->kind == N_IDENT && bu
|
|
&& bu->kind == TY_ARRAY) {
|
|
int esz = (bu->sub)
|
|
? (int)bu->sub->size : 1;
|
|
cgexpr(c, idx, *locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
int boff = localfind(*locals,
|
|
base->str);
|
|
if (boff != 0)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_BX),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_SI));
|
|
havesrc = 1;
|
|
} else if (base && (base->kind == N_DOT
|
|
|| base->kind == N_INDEX)) {
|
|
/* #270-3a: the index BASE is an N_DOT
|
|
* array-field (`x.arr[i]`) or a nested
|
|
* N_INDEX (`a[i][j]`); the N_IDENT-base arm
|
|
* above missed both, so the copy fell to the
|
|
* 8B truncation below. Compute &base[idx]:
|
|
* scaled idx on the stack, then &base via
|
|
* cg_dotbase_addr (N_DOT field address) or
|
|
* the &abase[bidx] spine (nested N_IDENT-
|
|
* array base), then add. */
|
|
int esz = (bu && bu->sub)
|
|
? (int)bu->sub->size : 1;
|
|
cgexpr(c, idx, *locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
int baseok = 0;
|
|
if (base->kind == N_DOT) {
|
|
baseok = cg_dotbase_addr(c, base,
|
|
D_AX, *locals);
|
|
} else {
|
|
Node *ab = base->lhs;
|
|
Node *bidx = base->rhs;
|
|
Type *abt = ab ? ab->type : NULL;
|
|
Type *abu = type_chase_named(abt);
|
|
if (ab && ab->kind == N_IDENT
|
|
&& abu
|
|
&& abu->kind == TY_ARRAY) {
|
|
int aesz = (abu->sub)
|
|
? (int)abu->sub->size
|
|
: 1;
|
|
cgexpr(c, bidx, *locals);
|
|
if (aesz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(aesz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
int aoff = localfind(
|
|
*locals, ab->str);
|
|
if (aoff != 0)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP,
|
|
aoff),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c,
|
|
ab->str),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ,
|
|
areg(D_BX),
|
|
areg(D_AX));
|
|
baseok = 1;
|
|
}
|
|
}
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
if (baseok) {
|
|
ins2(c, A_ADDQ, areg(D_BX),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_SI));
|
|
havesrc = 1;
|
|
}
|
|
}
|
|
}
|
|
if (havesrc) {
|
|
int k = 0;
|
|
for (; k + 8 <= sz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
}
|
|
if (k + 4 <= sz) {
|
|
ins2(c, A_MOVL, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
k += 4;
|
|
}
|
|
if (k + 2 <= sz) {
|
|
ins2(c, A_MOVW, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVW, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
k += 2;
|
|
}
|
|
if (k + 1 <= sz) {
|
|
ins2(c, A_MOVB, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
k += 1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
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) {
|
|
/* #38b: an sret-classified tagged return (slot
|
|
* > the AX/DX/CX/R8 cursor) writes the void-
|
|
* variant tag through *(@sretarg) and returns
|
|
* the dest pointer — the cursor can't carry the
|
|
* slot and the caller reads memory. */
|
|
if (cg_sret_retsize(rt) > 0) {
|
|
int tag = cg_tag_for_variant(rt, ty_void);
|
|
if (tag < 0) tag = 0;
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ, aimm(tag),
|
|
amem(D_BX, 0));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
if (rt->nullable) {
|
|
/* bare `return;` is the void/null
|
|
* variant: emit AX = 0. */
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
} else {
|
|
int tag = cg_tag_for_variant(rt, ty_void);
|
|
if (tag < 0) tag = 0;
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
if (n->lhs && cg_ret_type) {
|
|
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;
|
|
/* #263: passthrough forwards the source's AX/DX/CX
|
|
* unchanged — correct ONLY when the source already
|
|
* materialised the FULL tagged slot into registers:
|
|
* N_CALL / N_INDEX / N_DOT (the #261-broadened set).
|
|
* A tagged LOCAL ident leaves only word0 (the tag)
|
|
* in AX (cgexpr of an ident loads a single word), so
|
|
* DX (the payload) is garbage and the passthrough
|
|
* drops it. Route a tagged-ident return through the
|
|
* scratch-widen path below instead. Mirrors wwstage's
|
|
* forwardtagged kind filter, which already excludes
|
|
* N_IDENT (selfhost cgenstmt). */
|
|
int srcreg = n->lhs->kind == N_CALL ||
|
|
n->lhs->kind == N_INDEX ||
|
|
n->lhs->kind == N_DOT;
|
|
int passthrough = istagged && srcreg && (vu == rt ||
|
|
type_eq(vt, cg_ret_type));
|
|
int isstruct = vu && vu->kind == TY_STRUCT;
|
|
/* #242: a tuple variant must be PACKED into the union
|
|
* payload (tag + per-element words), not shuffled like a
|
|
* bare scalar — route it through the scratch-slot widen
|
|
* path (cg_widen_tagged_store TY_TUPLE arm). The scalar
|
|
* arm below zeroed the whole value (never packed the
|
|
* operands). */
|
|
int istuple = vu && vu->kind == TY_TUPLE;
|
|
/* #38b: sret-classified tagged return (slot >
|
|
* the AX/DX/CX/R8 cursor). Three shapes:
|
|
* - exact-type N_CALL forward: inner sret's
|
|
* straight into outer's caller dest (#9
|
|
* shape, cg_sret_forward).
|
|
* - widening from an sret-class tagged source
|
|
* ((A|B)→(A|B|C) mem-to-mem tag-remap):
|
|
* unwired, loud-stop — #40.
|
|
* - everything else: cg_widen_tagged_store
|
|
* through *(@sretarg) (the widener already
|
|
* speaks non-BP bases, the #34 precedent),
|
|
* then return the dest pointer. */
|
|
if (cg_sret_retsize(rt) > 0) {
|
|
int sz = (int)rt->size;
|
|
if (passthrough) {
|
|
/* exact type but a cursor source
|
|
* (N_INDEX/N_DOT) can't carry
|
|
* >32B — loud-stop (rule 7,
|
|
* #38b residual). */
|
|
if (n->lhs->kind != N_CALL)
|
|
fatal("#38b: >32B tagged "
|
|
"return from a cursor "
|
|
"source (kind %d) "
|
|
"unsupported",
|
|
n->lhs->kind);
|
|
cg_sret_forward = 1;
|
|
cgexpr(c, n->lhs, *locals);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_AX));
|
|
} else if (istagged
|
|
&& n->lhs->kind != N_IDENT
|
|
&& (int)vu->size > TUPLE_GPCAP * 8) {
|
|
fatal("#40: widening tagged "
|
|
"return-forward of a >32B "
|
|
"source needs mem-to-mem "
|
|
"tag-remap (unwired)");
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_BX));
|
|
cg_widen_tagged_store(c, locals,
|
|
rt, n->lhs, D_BX, 0, sz);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
if (rt->nullable) {
|
|
cgexpr(c, n->lhs, *locals);
|
|
} else if (passthrough) {
|
|
/* same tagged type: forward AX/DX/CX. */
|
|
cgexpr(c, n->lhs, *locals);
|
|
} else if (!istagged && !isstruct && !istuple) {
|
|
/* str / slice / scalar variant: synthesise
|
|
* the tag in AX and shuffle the value into
|
|
* DX[/CX[/R8]]. Direct register path keeps
|
|
* the asm short — no scratch slot.
|
|
* Tagged-return ABI: AX=tag, DX=word0,
|
|
* CX=word1, R8=word2. Slice payload uses
|
|
* all four; str uses three; scalar uses
|
|
* two. Unused ABI words must still be
|
|
* zeroed because the receiver
|
|
* (cg_widen_tagged_store call-source arm)
|
|
* writes AX/DX/CX/R8 unconditionally sized
|
|
* by the dst slot; stale CX/R8 from the
|
|
* caller (e.g. a slice-stride IMULQ) would
|
|
* land in slot+16 / slot+24. (Task #18.) */
|
|
int tag = cg_tag_for_variant(rt, vt);
|
|
int rsz = (int)rt->size;
|
|
cgexpr(c, n->lhs, *locals);
|
|
if (type_isslice(vt)) {
|
|
/* cgexpr leaves (AX=ptr, BX=len,
|
|
* CX=cap). Move into the return
|
|
* shuffle: DX=ptr, CX=len, R8=cap. */
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
areg(D_R8));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
} else if (type_isstr(vt)) {
|
|
/* str IS []u8: cgexpr leaves
|
|
* (AX=ptr, BX=len, CX=cap). Same
|
|
* shuffle as the slice arm above —
|
|
* DX=ptr, CX=len, R8=cap
|
|
* (#1/Phase 3). */
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
areg(D_R8));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
} else if (type_isfloat(vt)) {
|
|
/* #157: float variant. cgexpr left
|
|
* the value in X0, not AX; there is
|
|
* no MOVQ-xmm->gp encoding, so bridge
|
|
* X0->DX through a stack slot (same
|
|
* SUBQ/MOVSD/ADDQ idiom as the arg-
|
|
* push at cgen.c:5367). Zero the slot
|
|
* first so the f32 case (MOVSS writes
|
|
* only the low 4 bytes) leaves a
|
|
* deterministic high-4 — cs==ww byte-
|
|
* id, matching f64's MOVSD which fills
|
|
* all 8. The AX-independent spill also
|
|
* removes the stale-AX cs!=ww on
|
|
* multi-variant returns. */
|
|
int isf32 = type_isf32(vt);
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
amem(D_SP, 0));
|
|
ins2(c, isf32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_SP, 0));
|
|
ins2(c, A_MOVQ, amem(D_SP, 0),
|
|
areg(D_DX));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
if (rsz > 16)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_CX));
|
|
if (rsz > 24)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_R8));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
/* scalar fills DX only. Zero
|
|
* CX / R8 if dst slot covers
|
|
* slot+16 / slot+24. */
|
|
if (rsz > 16)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_CX));
|
|
if (rsz > 24)
|
|
ins2(c, A_MOVQ, aimm(0),
|
|
areg(D_R8));
|
|
}
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
areg(D_AX));
|
|
} else {
|
|
/* Struct variant or tagged-subset:
|
|
* materialise the widened value in a
|
|
* scratch slot, then load AX/DX/CX/R8
|
|
* from the slot. Struct literal: field
|
|
* stores; struct ident: word copy;
|
|
* tagged subset: copy + tag remap.
|
|
* 4th word in R8 covers slice payload
|
|
* variants (slot >= 32B).
|
|
*
|
|
* Single-slot @retscr (#14): returns are
|
|
* terminal, so all retscr uses in this fn
|
|
* share one slot. Pre-fix per-site fresh
|
|
* allocation over-grew the frame by sz
|
|
* bytes per extra return. */
|
|
int sz = (int)rt->size;
|
|
int scr;
|
|
if (cg_retscr != 0) {
|
|
scr = cg_retscr;
|
|
} else {
|
|
/* Fixed "@retscr" SSoT name —
|
|
* mirrors wwstage's localadd
|
|
* @-prefix dedup. Pre-fix
|
|
* mklabel(c, "retscr") consumed
|
|
* one labelseq counter slot per
|
|
* function with a tagged return,
|
|
* pushing every subsequent ct/ce/
|
|
* else/end label 1 ahead of
|
|
* wwstage. Site 1 sentinel
|
|
* masked by latent struct-widen
|
|
* offset divergence (#20/#21);
|
|
* fix is preventive symmetry per
|
|
* rule 10. */
|
|
scr = local_alloc(c, locals,
|
|
"@retscr", sz, cg_frame);
|
|
cg_retscr = scr;
|
|
}
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
cg_widen_tagged_store(c, locals, rt,
|
|
n->lhs, D_BP, scr, sz);
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
|
|
areg(D_AX));
|
|
if (sz > 8)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + 8),
|
|
areg(D_DX));
|
|
if (sz > 16)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + 16),
|
|
areg(D_CX));
|
|
if (sz > 24)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + 24),
|
|
areg(D_R8));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
/* sret return (#23): plain TY_STRUCT >24B. Callee writes
|
|
* the value through `*(@sretarg)` (the caller-prealloc
|
|
* dest passed in RDI at entry; saved to @sretarg in the
|
|
* prologue), then loads @sretarg into RAX and rets — the
|
|
* SysV sret discipline of "return the pointer". No
|
|
* AX/DX/CX shuffle, no scratch slot beyond @sretarg. */
|
|
if (n->lhs && cg_ret_type && cg_sret_arg_off != 0) {
|
|
/* type_chase_named (#22). A single peel left `rt` still
|
|
* TY_NAMED when the declared return type is `type b
|
|
* = a;` where a is itself a NAMED alias of a struct,
|
|
* so the TY_STRUCT gate below missed and the sret
|
|
* return arm fell through to the scalar-AX default —
|
|
* corrupting the caller's receive slot even though
|
|
* the prologue wired @sretarg. */
|
|
Type *rt = type_chase_named(cg_ret_type);
|
|
/* sret return-forwarding (task #9 follow-up to #23,
|
|
* generalised for #10 Fold B): `return f();` where outer
|
|
* + inner both return the same sret shape (>24B struct OR
|
|
* over-cap tuple — gate keys cg_sret_retsize, not a kind).
|
|
* Outer's @sretarg already
|
|
* holds its caller's prealloc dest; pass it to inner
|
|
* in RDI (set by cgcall via cg_sret_forward), inner
|
|
* writes directly there, inner's RAX (dest pointer)
|
|
* is already outer's return value. The trailing
|
|
* MOVQ @sretarg(BP), AX is redundant after inner's
|
|
* RET but kept for byte-id symmetry with the
|
|
* N_IDENT / N_STRUCTLIT arms below. */
|
|
if (cg_sret_retsize(rt) > 0
|
|
&& n->lhs->kind == N_CALL) {
|
|
cg_sret_forward = 1;
|
|
cgexpr(c, n->lhs, *locals);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
if (rt && (rt->kind == TY_STRUCT || rt->kind == TY_ARRAY)
|
|
&& (int)rt->size > 24
|
|
&& (n->lhs->kind == N_IDENT
|
|
|| n->lhs->kind == N_STRUCTLIT
|
|
|| n->lhs->kind == N_ARRLIT
|
|
|| n->lhs->kind == N_DOT
|
|
|| n->lhs->kind == N_INDEX
|
|
|| (n->lhs->kind == N_UN
|
|
&& n->lhs->op == TK_STAR))) {
|
|
/* Natural size = max(foff + fsz) over declared
|
|
* fields; mirrors selfhost cgenutil.ww
|
|
* structnaturalsize / sretretsize. Pre-fix this
|
|
* used the slot-padded rt->size, so a trailing
|
|
* narrow field (e.g. bool@32 in a 33B struct
|
|
* padded to 40B) widened to an 8B MOVQ at the
|
|
* loop tail — diverged from wwstage's MOVB
|
|
* tail. Task #33, Class A. An array (#267) has no
|
|
* fields; its natural size IS rt->size. */
|
|
int sz = 0;
|
|
if (rt->kind == TY_ARRAY) {
|
|
sz = (int)rt->size;
|
|
} else for (Tfield *fl = rt->fields; fl; fl = fl->next) {
|
|
int end = (int)fl->offset + (int)(fl->type
|
|
? fl->type->size : 8);
|
|
if (end > sz) sz = end;
|
|
}
|
|
if (n->lhs->kind == N_STRUCTLIT) {
|
|
/* Delegate to the shared *-relative
|
|
* fill helper. Same store sequence the
|
|
* ≤24B path emits, but the base reg is
|
|
* reloaded from @sretarg(BP) before each
|
|
* field store. Mirrors DST_PTR_LOCAL
|
|
* usage at N_ASSIGN N_DOT via_ptr. */
|
|
cg_structlit_fill(c, locals, rt,
|
|
n->lhs, DST_PTR_LOCAL,
|
|
cg_sret_arg_off, NULL, 0);
|
|
} else if (n->lhs->kind == N_IDENT) {
|
|
/* N_IDENT: word-copy from rhs slot to
|
|
* *(@sretarg). Whole 8B words via MOVQ;
|
|
* trailing partial words via MOVL/MOVB
|
|
* so the read stays inside the source
|
|
* slot's declared size. */
|
|
int rhsoff = localfind(*locals,
|
|
n->lhs->str);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_BX));
|
|
int k = 0;
|
|
while (k + 8 <= sz) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 8;
|
|
}
|
|
while (k + 4 <= sz) {
|
|
ins2(c, A_MOVL,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 4;
|
|
}
|
|
while (k < sz) {
|
|
ins2(c, A_MOVB,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 1;
|
|
}
|
|
} else if (n->lhs->kind == N_ARRLIT) {
|
|
/* #272/#276: a >24B array-literal return has
|
|
* no consumer and the ptr-relative element fill
|
|
* is untested. Loud-stop (rule 7) rather than
|
|
* fall to the scalar default. ≤24B is wired. */
|
|
fatal("#272/#276: >24B array-literal return "
|
|
"unsupported (rule 7, no consumer)");
|
|
} else {
|
|
/* #272: N_DOT / N_INDEX / deref — land the
|
|
* source ADDRESS in SI FIRST (aggarg_srcaddr
|
|
* clobbers BX on its N_INDEX spine), THEN
|
|
* reload the dest ptr from @sretarg into BX
|
|
* and memcpy sz bytes — same #265/#268 copy
|
|
* shape as the ≤24B arm. Loud-stop any source
|
|
* the helper can't address. */
|
|
if (!aggarg_srcaddr(c, n->lhs, D_SI, *locals))
|
|
fatal("#272: aggregate return from "
|
|
"unsupported source kind %d",
|
|
n->lhs->kind);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off),
|
|
areg(D_BX));
|
|
int k = 0;
|
|
while (k + 8 <= sz) {
|
|
ins2(c, A_MOVQ, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 8;
|
|
}
|
|
while (k + 4 <= sz) {
|
|
ins2(c, A_MOVL, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 4;
|
|
}
|
|
while (k < sz) {
|
|
ins2(c, A_MOVB, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BX, k));
|
|
k += 1;
|
|
}
|
|
}
|
|
/* sret return: RAX = dest pointer. */
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
/* Whole-struct return for sizes ≤24B. ABI: AX=bytes[0..7],
|
|
* DX=bytes[8..15], CX=bytes[16..23]. Sizes >24B route
|
|
* through the sret arm above. Materialise rhs into a
|
|
* zero-padded 24B scratch slot, then emit AX/DX/CX loads
|
|
* unconditionally so the instruction shape is constant
|
|
* regardless of declared struct size. The receive side
|
|
* masks via the dst slot's declared size. Two rhs shapes
|
|
* are wired: N_IDENT (word-copy from rhs local slot) and
|
|
* N_STRUCTLIT (field-by-field store at scratch+foff). Call-
|
|
* result chain return is deferred to #5's receive side. */
|
|
if (n->lhs && cg_ret_type) {
|
|
/* type_chase_named (#22); see the >24B sret arm above
|
|
* for the same rationale. The ≤24B register-return
|
|
* ABI uses the same TY_STRUCT gate. */
|
|
Type *rt = type_chase_named(cg_ret_type);
|
|
/* #272: aggregate-return source-shape closure. Beyond the
|
|
* #267 N_IDENT/N_STRUCTLIT pair, every OTHER addressable
|
|
* aggregate rvalue (`return [..]` N_ARRLIT, `return o.f`
|
|
* N_DOT, `return a[i]` N_INDEX, `return *p` deref) fell to
|
|
* the scalar-AX default below = silent truncation. Funnel
|
|
* them through the SAME @retscr materialise the arg side
|
|
* closed in #271 (aggarg_srcaddr). N_CALL still passes
|
|
* through the tail (the callee already left AX/DX/CX). */
|
|
if (rt && (rt->kind == TY_STRUCT || rt->kind == TY_ARRAY)
|
|
&& rt->size <= 24
|
|
&& (n->lhs->kind == N_IDENT
|
|
|| n->lhs->kind == N_STRUCTLIT
|
|
|| n->lhs->kind == N_ARRLIT
|
|
|| n->lhs->kind == N_DOT
|
|
|| n->lhs->kind == N_INDEX
|
|
|| (n->lhs->kind == N_UN
|
|
&& n->lhs->op == TK_STAR))) {
|
|
int sz = (int)rt->size;
|
|
/* Single-slot @retscr (#14): see tagged arm
|
|
* above for rationale. Fixed "@retscr" name
|
|
* avoids bumping labelseq; mirrors wwstage's
|
|
* localadd @-prefix dedup. */
|
|
int scr;
|
|
if (cg_retscr != 0) {
|
|
scr = cg_retscr;
|
|
} else {
|
|
scr = local_alloc(c, locals, "@retscr",
|
|
24, cg_frame);
|
|
cg_retscr = scr;
|
|
}
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + 0));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + 8));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + 16));
|
|
if (n->lhs->kind == N_STRUCTLIT) {
|
|
/* Delegate to the shared BP-relative
|
|
* fill helper. Same store sequence the
|
|
* inline pre-#17 walk emitted, plus
|
|
* nested struct-typed structlit values
|
|
* recurse instead of dropping the
|
|
* trailing bytes. */
|
|
cg_structlit_fill_bp(c, locals, rt,
|
|
n->lhs, scr);
|
|
} else if (n->lhs->kind == N_ARRLIT) {
|
|
/* #272: materialise the array literal into
|
|
* @retscr per element, mirroring the
|
|
* let-init N_ARRLIT scalar/float fill
|
|
* (cgen.c N_LET). Non-scalar elements
|
|
* (struct/array/str/slice) loud-stop: no
|
|
* return-by-value consumer exists (rule 7),
|
|
* and the let-init path already covers them
|
|
* for the addressable forms. */
|
|
Type *esub = rt->sub;
|
|
int esz = esub ? (int)esub->size : 1;
|
|
Type *esubu = type_chase_named(esub);
|
|
if ((esubu && (esubu->kind == TY_STRUCT
|
|
|| esubu->kind == TY_ARRAY
|
|
|| esubu->kind == TY_TUPLE))
|
|
|| type_isstr(esub)
|
|
|| type_isslice(esub))
|
|
fatal("#272: array-literal return "
|
|
"with non-scalar element "
|
|
"unsupported (rule 7, no "
|
|
"consumer)");
|
|
int isfl = type_isfloat(esub);
|
|
int fmov = type_isf32(esub) ? A_MOVSS
|
|
: A_MOVSD;
|
|
int op = A_MOVQ;
|
|
if (esz == 1) op = A_MOVB;
|
|
else if (esz == 2) op = A_MOVW;
|
|
else if (esz == 4) op = A_MOVL;
|
|
int idx = 0;
|
|
Node *last = NULL;
|
|
int repeat = 0;
|
|
for (Node *e = n->lhs->list; e;
|
|
e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
cgexpr(c, e, *locals);
|
|
if (isfl)
|
|
ins2(c, fmov, areg(D_X0),
|
|
amem(D_BP,
|
|
scr + idx * esz));
|
|
else
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP,
|
|
scr + idx * esz));
|
|
last = e;
|
|
idx++;
|
|
}
|
|
if (repeat && last)
|
|
while (idx < (int)rt->alen) {
|
|
if (isfl)
|
|
ins2(c, fmov,
|
|
areg(D_X0),
|
|
amem(D_BP,
|
|
scr + idx * esz));
|
|
else
|
|
ins2(c, op,
|
|
areg(D_AX),
|
|
amem(D_BP,
|
|
scr + idx * esz));
|
|
idx++;
|
|
}
|
|
} else if (n->lhs->kind == N_IDENT) {
|
|
/* N_IDENT: word-copy rhs slot into
|
|
* scratch. Whole 8B words via MOVQ;
|
|
* trailing partial word via MOVL/MOVB
|
|
* so we read no further than the
|
|
* source slot's declared size. */
|
|
int rhsoff = localfind(*locals,
|
|
n->lhs->str);
|
|
int k = 0;
|
|
while (k + 8 <= sz) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 8;
|
|
}
|
|
while (k + 4 <= sz) {
|
|
ins2(c, A_MOVL,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 4;
|
|
}
|
|
while (k < sz) {
|
|
ins2(c, A_MOVB,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 1;
|
|
}
|
|
} else {
|
|
/* #272: N_DOT / N_INDEX / deref — land the
|
|
* source ADDRESS in SI via the #271 arg-side
|
|
* helper, then memcpy sz bytes into @retscr
|
|
* (the #265/#268 let-init copy shape). Loud-
|
|
* stop any source the helper can't address
|
|
* (rule 7); the gate above already excludes
|
|
* N_CALL (tail passthrough). */
|
|
if (!aggarg_srcaddr(c, n->lhs, D_SI,
|
|
*locals))
|
|
fatal("#272: aggregate return "
|
|
"from unsupported source "
|
|
"kind %d", n->lhs->kind);
|
|
int k = 0;
|
|
while (k + 8 <= sz) {
|
|
ins2(c, A_MOVQ, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 8;
|
|
}
|
|
if (k + 4 <= sz) {
|
|
ins2(c, A_MOVL, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 4;
|
|
}
|
|
if (k + 2 <= sz) {
|
|
ins2(c, A_MOVW, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVW, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 2;
|
|
}
|
|
if (k + 1 <= sz) {
|
|
ins2(c, A_MOVB, amem(D_SI, k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 1;
|
|
}
|
|
}
|
|
/* #171a: float-bearing struct RETURN (the return
|
|
* twin of #165's param recv). A qualifying struct's
|
|
* float eightbytes ride the SSE return row (X0,X1 =
|
|
* tuple_sse_seq), its INT eightbytes the INTEGER
|
|
* return row (AX,DX = tuple_rseq), on INDEPENDENT
|
|
* cursors per SysV (ref/qbe/amd64/sysv.c retr) — so a
|
|
* float lands in the next XMM regardless of its
|
|
* positional eightbyte (struct{f64,i32}: e0→X0, e1→AX,
|
|
* NOT DX). The scr slot is zero-padded to 24B, so a
|
|
* full MOVQ on a trailing INT eightbyte reads no
|
|
* garbage — the #169 sized tail is a RECV-only concern.
|
|
* struct_float_class gates to qualifying structs (>=1
|
|
* f64, every eightbyte lone-f64 or pure-INT); all-int +
|
|
* f32 keep the AX/DX/CX transport (byte-id / #171b). */
|
|
int sclass[2], snb;
|
|
if ((snb = struct_float_class(rt, sclass)) > 0) {
|
|
int gpcur = 0, ssecur = 0;
|
|
for (int e = 0; e < snb; e++) {
|
|
if (sclass[e]) {
|
|
ins2(c, A_MOVSD,
|
|
amem(D_BP, scr + e * 8),
|
|
areg(tuple_sse_seq[ssecur]));
|
|
ssecur++;
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + e * 8),
|
|
areg(tuple_rseq[gpcur]));
|
|
gpcur++;
|
|
}
|
|
}
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 8),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 16),
|
|
areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
/* #272 close-by-construction: the addressable aggregate-return
|
|
* sources (IDENT/STRUCTLIT/ARRLIT/DOT/INDEX/deref) all route
|
|
* through the @retscr / *(@sretarg) arms above and break; an
|
|
* aggregate N_CALL passes through the cgexpr tail (the callee
|
|
* already left AX/DX/CX). Any OTHER aggregate rvalue reaching
|
|
* here would truncate to AX silently — loud-stop (rule 7) so a
|
|
* future unhandled shape is caught, not miscompiled. */
|
|
if (n->lhs && cg_ret_type) {
|
|
Type *rtc = type_chase_named(cg_ret_type);
|
|
if (rtc && (rtc->kind == TY_STRUCT || rtc->kind == TY_ARRAY)
|
|
&& n->lhs->kind != N_CALL)
|
|
fatal("#272: aggregate return reaches scalar default "
|
|
"(source kind %d) — unclosed shape",
|
|
n->lhs->kind);
|
|
}
|
|
if (n->lhs && node_isstr(n->lhs)) {
|
|
/* str IS []u8: AX=ptr, BX=len, CX=cap from cgexpr —
|
|
* no AX:DX shuffle, same as a slice (#1/Phase 3). */
|
|
cgexpr(c, n->lhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
if (n->lhs && n->lhs->kind == N_TUPLE) {
|
|
/* #83 / #164 (#107): positional register-return over a SysV
|
|
* dual class cursor. Each element rides its SysV class
|
|
* (harec create_unpack_bindings, ref/harec/src/check.c:1354-
|
|
* 1416): a float takes one SSE eightbyte (X0,X1 = tuple_sse_
|
|
* seq), everything else INTEGER eightbytes over tuple_rseq —
|
|
* a slice/str its 3-word {ptr,len,cap} header (ref/hare/rt/
|
|
* ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a scalar 1 word
|
|
* in AX. Integer words spill L→R to the stack and pop into the
|
|
* INTEGER cursor in reverse so positional slot i lands in
|
|
* tuple_rseq[i] (byte-id with #83 when no float is present).
|
|
* Each float must spill X0 to @tupfscr as we walk, because a
|
|
* later element's cgexpr clobbers X0; after the integer pops
|
|
* the saved floats reload into X0/X1 by SSE index — INDEPENDENT
|
|
* of the integer cursor (ref/qbe/amd64/sysv.c retr L95-108).
|
|
* Both rows are loud-stopped at their cap (rule-7, never a
|
|
* silent collide): INTEGER 4, SSE 2. The SAME class split
|
|
* drives the receive sites. */
|
|
int ssecap = TUPLE_SSECAP;
|
|
int gptotal = 0, ssecount = 0, f32;
|
|
for (Node *e = n->lhs->list; e; e = e->next) {
|
|
if (fld_isfloat(e->type, &f32))
|
|
ssecount++;
|
|
else
|
|
gptotal += tuple_ebytes(node_isstr(e)
|
|
|| node_isslice(e));
|
|
}
|
|
if (gptotal > TUPLE_GPCAP || ssecount > ssecap) {
|
|
/* #10 Fold A: over-cap tuple returns via sret. The
|
|
* prologue wired @sretarg (cg_sret_retsize agrees on
|
|
* the caps — the shared SSoT), holding the caller-
|
|
* prealloc dest. Store each element through
|
|
* *(@sretarg) at its packed layout offset (running
|
|
* sum of element sizes — the t.0/t.1 positional
|
|
* layout, N_DOT TY_TUPLE arm), each at its natural
|
|
* width so a narrow tail doesn't over-MOVQ (#169);
|
|
* the dest base is reloaded into DX each step since a
|
|
* wide element's cgexpr clobbers AX/BX/CX. Then reuse
|
|
* the struct-sret epilogue. The CALL/receive side
|
|
* stays loud-stopped (#10 Fold B). */
|
|
/* #240: foff advances by the DECLARED return-type
|
|
* element size (cg_ret_type tuple params), NOT the
|
|
* literal expression's type. A bare int literal
|
|
* element is stamped TY_UNTYPED_INT (size 0), so
|
|
* `e->type->size` collapsed foff to 0 for a leading
|
|
* scalar — the next element then clobbered it at
|
|
* offset 0 and every trailing element packed 8 bytes
|
|
* low, diverging from the t.N reader (f->offset) and
|
|
* from wwstage (cgenstmt.ww walks c.fnret.list). */
|
|
Type *rtt = type_chase_named(cg_ret_type);
|
|
Tparam *pp = (rtt && rtt->kind == TY_TUPLE)
|
|
? rtt->params : NULL;
|
|
int foff = 0;
|
|
for (Node *e = n->lhs->list; e; e = e->next) {
|
|
int isflt = fld_isfloat(e->type, &f32);
|
|
int wide = node_isstr(e) || node_isslice(e);
|
|
int esz = 8;
|
|
if (pp && pp->type)
|
|
esz = (int)pp->type->size;
|
|
else if (e->type)
|
|
esz = (int)e->type->size;
|
|
cgexpr(c, e, *locals);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, cg_sret_arg_off), areg(D_DX));
|
|
if (isflt)
|
|
ins2(c, f32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_DX, foff));
|
|
else if (wide) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_DX, foff + 16));
|
|
} else
|
|
ins2(c, fldstoreop(e->type, esz),
|
|
areg(D_AX), amem(D_DX, foff));
|
|
foff += esz;
|
|
if (pp) pp = pp->next;
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, cg_sret_arg_off),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
int fscr = 0;
|
|
if (ssecount > 0) {
|
|
if (cg_tupfscr != 0)
|
|
fscr = cg_tupfscr;
|
|
else {
|
|
fscr = local_alloc(c, locals, "@tupfscr",
|
|
ssecap * 8, cg_frame);
|
|
cg_tupfscr = fscr;
|
|
}
|
|
}
|
|
int sseidx = 0;
|
|
for (Node *e = n->lhs->list; e; e = e->next) {
|
|
int isflt = fld_isfloat(e->type, &f32);
|
|
cgexpr(c, e, *locals); /* scalar=AX; slice/str=AX,BX,CX; float=X0 */
|
|
if (isflt) {
|
|
ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0),
|
|
amem(D_BP, fscr + sseidx * 8));
|
|
sseidx++;
|
|
continue;
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* scalar / .ptr */
|
|
if (node_isstr(e) || node_isslice(e)) {
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* .len */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* .cap */
|
|
}
|
|
}
|
|
for (int i = gptotal - 1; i >= 0; i--)
|
|
ins1(c, A_POPQ, areg(tuple_rseq[i]));
|
|
int j = 0;
|
|
for (Node *e = n->lhs->list; e; e = e->next) {
|
|
if (!fld_isfloat(e->type, &f32))
|
|
continue;
|
|
ins2(c, f32 ? A_MOVSS : A_MOVSD,
|
|
amem(D_BP, fscr + j * 8),
|
|
areg(tuple_sse_seq[j]));
|
|
j++;
|
|
}
|
|
} else if (n->lhs) {
|
|
cgexpr(c, n->lhs, *locals);
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
case N_IF: {
|
|
char *els = mklabel(c, "else");
|
|
char *end = mklabel(c, "end");
|
|
cgexpr(c, n->cond, *locals);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JE, abranch(n->els ? els : end));
|
|
cgstmt(c, n->body, locals, frame);
|
|
if (n->els) {
|
|
ins1(c, A_JMP, abranch(end));
|
|
label(c, els);
|
|
cgstmt(c, n->els, locals, frame);
|
|
}
|
|
label(c, end);
|
|
break;
|
|
}
|
|
case N_FORRANGE: {
|
|
/* Lower `for (let x .. s) body` (and its tuple-destructure
|
|
* cousin `for (let (a, b) .. s)`). The body is wrapped in a
|
|
* counted loop driven by stack-spilled `_i`/`_len`. Each
|
|
* iteration computes the element address `s.ptr + i*esz`
|
|
* and either loads the whole element into the named local
|
|
* or pulls each tuple field into its respective local. */
|
|
Node *slc = n->lhs;
|
|
Type *st = slc ? slc->type : NULL;
|
|
Type *u = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int esz = (u && u->sub) ? (int)u->sub->size : 1;
|
|
Type *etu = (u && u->sub && u->sub->kind == TY_NAMED)
|
|
? u->sub->under : (u ? u->sub : NULL);
|
|
int destruct = (n->list != NULL);
|
|
|
|
/* allocate temp slots: _i (8B), _len (8B) */
|
|
char *iname = aprintf(c->a, ".rgi_%d", c->labelseq++);
|
|
char *lname = aprintf(c->a, ".rgl_%d", c->labelseq++);
|
|
int ioff = localoff(c, locals, iname, 8, frame);
|
|
int loff = localoff(c, locals, lname, 8, frame);
|
|
|
|
/* allocate per-name slots */
|
|
struct { int off, sz, foff; Type *ftype; } binds[8] = {0};
|
|
int nbinds = 0;
|
|
if (destruct) {
|
|
Tparam *tp = (etu && etu->kind == TY_TUPLE) ?
|
|
etu->params : NULL;
|
|
int field_off = 0;
|
|
for (Node *nm = n->list; nm && nbinds < 8; nm = nm->next) {
|
|
int fsz = tp && tp->type ? (int)tp->type->size : 8;
|
|
int slot_sz = (fsz < 8) ? 8 : fsz;
|
|
binds[nbinds].sz = fsz;
|
|
binds[nbinds].foff = field_off;
|
|
binds[nbinds].ftype = tp ? tp->type : NULL;
|
|
binds[nbinds].off = localoff(c, locals,
|
|
nm->str, slot_sz, frame);
|
|
field_off += fsz;
|
|
nbinds++;
|
|
if (tp) tp = tp->next;
|
|
}
|
|
} else {
|
|
int slot_sz = (esz < 8) ? 8 : esz;
|
|
binds[0].off = localoff(c, locals, n->str, slot_sz, frame);
|
|
binds[0].sz = esz;
|
|
binds[0].foff = 0;
|
|
binds[0].ftype = u ? u->sub : NULL;
|
|
nbinds = 1;
|
|
}
|
|
|
|
ins2(c, A_MOVQ, aimm(0), amem(D_BP, ioff));
|
|
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
|
|
&& slc->kind == N_IDENT) {
|
|
int boff = localfind(*locals, slc->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff));
|
|
} else if (u && u->kind == TY_ARRAY) {
|
|
ins2(c, A_MOVQ, aimm((long long)u->alen),
|
|
amem(D_BP, loff));
|
|
} else {
|
|
cgexpr(c, slc, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff));
|
|
}
|
|
|
|
char *loop = mklabel(c, "rloop");
|
|
char *end = mklabel(c, "rend");
|
|
char *natural_exit = end;
|
|
if (n->els) natural_exit = mklabel(c, "relseloop");
|
|
/* #138 (range form): `continue` must run the implicit `i+=1`
|
|
* post-step before re-testing the loop bound. Pre-fix the
|
|
* cont-target was `loop` (top), skipping the ADDQ $1, ioff
|
|
* below the body — infinite loop on the value that triggered
|
|
* continue. Dedicated `rpost` label; bootstrap-NEUTRAL (no
|
|
* range-form continue callers in lib/ or selfhost/). */
|
|
char *rpost = mklabel(c, "rpost");
|
|
if (nloops < LOOP_MAX) {
|
|
loop_cont[nloops] = rpost;
|
|
loop_brk[nloops] = end;
|
|
nloops++;
|
|
}
|
|
label(c, loop);
|
|
ins2(c, A_MOVQ, amem(D_BP, ioff), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, loff), areg(D_BX));
|
|
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
|
|
ins1(c, A_JGE, abranch(natural_exit));
|
|
/* compute element base: s.ptr + i*esz → BX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
if (slc->kind == N_IDENT && u && u->kind == TY_ARRAY) {
|
|
int boff = localfind(*locals, slc->str);
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX));
|
|
} else if (slc->kind == N_IDENT) {
|
|
int boff = localfind(*locals, slc->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
/* load each binding from BX + foff into its slot */
|
|
for (int b = 0; b < nbinds; b++) {
|
|
int op = fldloadop(binds[b].ftype, binds[b].sz);
|
|
ins2(c, op, amem(D_BX, binds[b].foff), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, binds[b].off));
|
|
}
|
|
cgstmt(c, n->body, locals, frame);
|
|
label(c, rpost);
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_BP, ioff));
|
|
ins1(c, A_JMP, abranch(loop));
|
|
if (n->els) {
|
|
label(c, natural_exit);
|
|
cgstmt(c, n->els, locals, frame);
|
|
}
|
|
label(c, end);
|
|
if (nloops > 0) nloops--;
|
|
break;
|
|
}
|
|
case N_FOR: {
|
|
char *loop = mklabel(c, "loop");
|
|
char *end = mklabel(c, "endloop");
|
|
/* `else` runs at normal cond-false exit; break skips it.
|
|
* Separate the natural exit label from the break target so
|
|
* the else block sits between them. */
|
|
char *natural_exit = end;
|
|
if (n->els) natural_exit = mklabel(c, "elseloop");
|
|
/* #138: `continue` in a 3-clause `for (init; cond; post)` must
|
|
* run the post-step before re-testing cond. Pre-fix the
|
|
* continue-target was `loop` (top), which SKIPPED post → state
|
|
* never advanced → infinite loop. Allocate a dedicated `post`
|
|
* label only when there IS a post-step (`n->rhs`); else keep
|
|
* continue → loop-top, byte-id with 1-clause for. */
|
|
char *cont_target = loop;
|
|
if (n->rhs) cont_target = mklabel(c, "post");
|
|
if (n->lhs) cgstmt(c, n->lhs, locals, frame);
|
|
label(c, loop);
|
|
if (n->cond) {
|
|
cgexpr(c, n->cond, *locals);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JE, abranch(natural_exit));
|
|
}
|
|
if (nloops < LOOP_MAX) {
|
|
loop_cont[nloops] = cont_target;
|
|
loop_brk[nloops] = end;
|
|
nloops++;
|
|
}
|
|
cgstmt(c, n->body, locals, frame);
|
|
if (nloops > 0) nloops--;
|
|
if (n->rhs) {
|
|
label(c, cont_target);
|
|
cgexpr(c, n->rhs, *locals);
|
|
}
|
|
ins1(c, A_JMP, abranch(loop));
|
|
if (n->els) {
|
|
label(c, natural_exit);
|
|
cgstmt(c, n->els, locals, frame);
|
|
}
|
|
label(c, end);
|
|
break;
|
|
}
|
|
case N_MLET: {
|
|
/* #83 / #164 (#107): positional per-element destructure store.
|
|
* The rhs left each tuple element in its SysV-class register
|
|
* (see N_RETURN / harec create_unpack_bindings, ref/harec/src/
|
|
* check.c:1354-1416); walk the bindings over the SAME dual
|
|
* cursor and store each at its own width — a slice/str's 3-word
|
|
* {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8) into a
|
|
* header-sized slot (sized from u->size so #1 propagates), a
|
|
* float from X0/X1 (SSE cursor), a scalar's 1 word from the
|
|
* INTEGER cursor into an 8B slot. Both rows loud-stop at their
|
|
* cap. */
|
|
/* #10 Fold B: over-cap tuple destructure RECEIVE. The callee
|
|
* sret'd the whole tuple into the @sretscr discard slot (cgcall
|
|
* sees cg_sret_retsize > 0, no lvalue dest wired). Copy each
|
|
* element out to its binding slot at the SAME packed offset the
|
|
* SEND wrote (foff += element size — the t.0/t.1 layout), each
|
|
* at its NATURAL width (#169). The receive has no single lvalue
|
|
* dest, so it reuses the same per-fn @sretscr slot a discarded
|
|
* sret call would; the in-reg path below is unchanged. */
|
|
/* #242: rhs is a tuple already materialised in a local slot (a
|
|
* match-bound union payload, `let (a,b)=t`), NOT a register-
|
|
* returning call. cgexpr(tuple ident) loads only word0->AX, so
|
|
* the register-cursor path below reads DX/CX stale. Copy each
|
|
* element from the ident's slot at the register-ABI 8B stride
|
|
* (24B for a slice/str header) — the SAME layout the tagged
|
|
* construct + match payload-bind write. */
|
|
if (n->rhs && n->rhs->kind == N_IDENT) {
|
|
Type *rty = type_chase_named(n->rhs->type);
|
|
if (rty && rty->kind == TY_TUPLE) {
|
|
int srcoff = localfind(*locals, n->rhs->str);
|
|
int lf32b;
|
|
int foff = 0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *t = l->type;
|
|
Type *u = type_chase_named(t);
|
|
int wide = u && (u->kind == TY_SLICE
|
|
|| u->kind == TY_STR);
|
|
int isflt = fld_isfloat(t, &lf32b);
|
|
int esz = t ? (int)t->size : 8;
|
|
int bsz = wide ? esz : 8;
|
|
int off = localoff(c, locals, l->str,
|
|
bsz, frame);
|
|
if (isflt) {
|
|
ins2(c, lf32b ? A_MOVSS : A_MOVSD,
|
|
amem(D_BP, srcoff + foff),
|
|
areg(D_X0));
|
|
ins2(c, lf32b ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_BP, off));
|
|
} else if (wide) {
|
|
for (int k = 0; k < esz; k += 8) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, srcoff + foff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
}
|
|
} else {
|
|
ins2(c, fldloadop(t, esz),
|
|
amem(D_BP, srcoff + foff),
|
|
areg(D_AX));
|
|
ins2(c, fldstoreop(t, esz),
|
|
areg(D_AX), amem(D_BP, off));
|
|
}
|
|
foff += wide ? 24 : 8;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
int sret_recv = (n->rhs && n->rhs->kind == N_CALL)
|
|
? cg_sret_retsize(n->rhs->type) : 0;
|
|
cgexpr(c, n->rhs, *locals);
|
|
int lf32;
|
|
if (sret_recv > 0) {
|
|
int scr = cg_sretscr_off;
|
|
int foff = 0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *t = l->type;
|
|
Type *u = type_chase_named(t);
|
|
int wide = u && (u->kind == TY_SLICE
|
|
|| u->kind == TY_STR);
|
|
int isflt = fld_isfloat(t, &lf32);
|
|
int esz = t ? (int)t->size : 8;
|
|
int bsz = wide ? esz : 8;
|
|
int off = localoff(c, locals, l->str, bsz, frame);
|
|
if (isflt) {
|
|
ins2(c, lf32 ? A_MOVSS : A_MOVSD,
|
|
amem(D_BP, scr + foff), areg(D_X0));
|
|
ins2(c, lf32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_BP, off));
|
|
} else if (wide) {
|
|
for (int k = 0; k < esz; k += 8) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + foff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
}
|
|
} else {
|
|
ins2(c, fldloadop(t, esz),
|
|
amem(D_BP, scr + foff), areg(D_AX));
|
|
ins2(c, fldstoreop(t, esz),
|
|
areg(D_AX), amem(D_BP, off));
|
|
}
|
|
foff += esz;
|
|
}
|
|
break;
|
|
}
|
|
int gpcap = TUPLE_GPCAP;
|
|
int ssecap = TUPLE_SSECAP;
|
|
int gptotal = 0, ssetotal = 0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *t = l->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (fld_isfloat(t, &lf32))
|
|
ssetotal++;
|
|
else
|
|
gptotal += tuple_ebytes(u && (u->kind == TY_SLICE
|
|
|| u->kind == TY_STR));
|
|
}
|
|
if (gptotal > gpcap)
|
|
fatal("tuple destructure exceeds integer register-return "
|
|
"ABI capacity (%d eightbytes: AX,DX,CX,R8); "
|
|
"see return-ABI #10", gpcap);
|
|
if (ssetotal > ssecap)
|
|
fatal("tuple destructure exceeds SSE register-return ABI "
|
|
"capacity (%d eightbytes: X0,X1); see return-ABI #10",
|
|
ssecap);
|
|
int gpcur = 0, ssecur = 0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *t = l->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR);
|
|
int isflt = fld_isfloat(t, &lf32);
|
|
int sz = wide ? (int)u->size : 8;
|
|
int off = localoff(c, locals, l->str, sz, frame);
|
|
tuple_store(c, t, wide, gpcur, ssecur, off);
|
|
if (isflt)
|
|
ssecur++;
|
|
else
|
|
gpcur += tuple_ebytes(wide);
|
|
}
|
|
break;
|
|
}
|
|
case N_MASSIGN: {
|
|
/* #83: positional per-element destructure REASSIGN. Same cursor
|
|
* as N_MLET (and N_RETURN; harec create_unpack_bindings,
|
|
* ref/harec/src/check.c:1354-1416), but the slots already exist
|
|
* (reassignment) so localfind them. Element WIDTH comes from the
|
|
* rhs tuple's element types (n->rhs->type->params) — the SAME
|
|
* producer source the SEND site walks and wwstage reads via the
|
|
* callee return type — NOT the binding type: a `_` lvalue is an
|
|
* N_IDENT with empty str the checker never type-stamps (it skips
|
|
* cexpr on `_`, cmd/wcc/check.c N_MASSIGN), so a binding-typed
|
|
* width would mis-size a wide `_` and DESYNC the cursor for the
|
|
* next element. harec `_` skips the store but CONSUMES its tuple
|
|
* offset; the cursor advance below honours that. A wide element's
|
|
* 3-word {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8) is stored
|
|
* at its slot. This bare-comma `a, s = f()` multi-assign is a
|
|
* retained ww-EXTENSION beyond Hare (Hare tuple-unpack is binding-
|
|
* only); ww keeps the Go/rob-pike multi-assign idiom — rule-9
|
|
* carve-out. Over-capacity is a loud stop, not a silent drop. */
|
|
/* #10 Fold B: over-cap tuple destructure REASSIGN. Same sret
|
|
* copy-out as N_MLET but the slots already exist (localfind);
|
|
* a `_` / missing binding (off == 0) SKIPS its store yet still
|
|
* ADVANCES foff so the next element stays aligned (harec `_`).
|
|
* Element widths come from the rhs tuple's element types — the
|
|
* SAME producer source the SEND walks. */
|
|
int sret_recv = (n->rhs && n->rhs->kind == N_CALL)
|
|
? cg_sret_retsize(n->rhs->type) : 0;
|
|
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 mf32;
|
|
if (sret_recv > 0) {
|
|
int scr = cg_sretscr_off;
|
|
int foff = 0;
|
|
Tparam *tp = tp0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *et = tp ? tp->type : NULL;
|
|
Type *eu = type_chase_named(et);
|
|
int wide = eu && (eu->kind == TY_SLICE
|
|
|| eu->kind == TY_STR);
|
|
int isflt = fld_isfloat(et, &mf32);
|
|
int esz = et ? (int)et->size : 8;
|
|
int off = (l->kind == N_IDENT)
|
|
? localfind(*locals, l->str) : 0;
|
|
if (off != 0) {
|
|
if (isflt) {
|
|
ins2(c, mf32 ? A_MOVSS : A_MOVSD,
|
|
amem(D_BP, scr + foff),
|
|
areg(D_X0));
|
|
ins2(c, mf32 ? A_MOVSS : A_MOVSD,
|
|
areg(D_X0), amem(D_BP, off));
|
|
} else if (wide) {
|
|
for (int k = 0; k < esz; k += 8) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, scr + foff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + k));
|
|
}
|
|
} else {
|
|
ins2(c, fldloadop(et, esz),
|
|
amem(D_BP, scr + foff),
|
|
areg(D_AX));
|
|
ins2(c, fldstoreop(et, esz),
|
|
areg(D_AX), amem(D_BP, off));
|
|
}
|
|
}
|
|
foff += esz;
|
|
if (tp) tp = tp->next;
|
|
}
|
|
break;
|
|
}
|
|
int gpcap = TUPLE_GPCAP;
|
|
int ssecap = TUPLE_SSECAP;
|
|
int gptotal = 0, ssetotal = 0;
|
|
for (Tparam *tp = tp0; tp; tp = tp->next) {
|
|
Type *u = (tp->type && tp->type->kind == TY_NAMED)
|
|
? tp->type->under : tp->type;
|
|
if (fld_isfloat(tp->type, &mf32))
|
|
ssetotal++;
|
|
else
|
|
gptotal += tuple_ebytes(u && (u->kind == TY_SLICE
|
|
|| u->kind == TY_STR));
|
|
}
|
|
if (gptotal > gpcap)
|
|
fatal("tuple destructure exceeds integer register-return "
|
|
"ABI capacity (%d eightbytes: AX,DX,CX,R8); "
|
|
"see return-ABI #10", gpcap);
|
|
if (ssetotal > ssecap)
|
|
fatal("tuple destructure exceeds SSE register-return ABI "
|
|
"capacity (%d eightbytes: X0,X1); see return-ABI #10",
|
|
ssecap);
|
|
int gpcur = 0, ssecur = 0;
|
|
Tparam *tp = tp0;
|
|
for (Node *l = n->list; l; l = l->next) {
|
|
Type *et = tp ? tp->type : NULL;
|
|
Type *u = (et && et->kind == TY_NAMED) ? et->under : et;
|
|
int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR);
|
|
int isflt = fld_isfloat(et, &mf32);
|
|
int off = (l->kind == N_IDENT)
|
|
? localfind(*locals, l->str) : 0;
|
|
/* harec `_` (off==0): skip the store but CONSUME the
|
|
* cursor slot so the next element stays aligned. */
|
|
if (off != 0)
|
|
tuple_store(c, et, wide, gpcur, ssecur, off);
|
|
if (isflt)
|
|
ssecur++;
|
|
else
|
|
gpcur += tuple_ebytes(wide);
|
|
if (tp) tp = tp->next;
|
|
}
|
|
break;
|
|
}
|
|
case N_DEFER:
|
|
if (ndefers < DEFER_MAX) {
|
|
defers[ndefers++] = n->lhs;
|
|
}
|
|
break;
|
|
case N_YIELD:
|
|
/* Evaluate the value into AX, then jump to the enclosing
|
|
* match's end label. str-typed yields land in (AX, BX);
|
|
* the consumer's let-init or call-arg site reads both. */
|
|
if (n->lhs) cgexpr(c, n->lhs, *locals);
|
|
if (nyields > 0)
|
|
ins1(c, A_JMP, abranch(yield_target[nyields - 1]));
|
|
break;
|
|
case N_BREAK:
|
|
if (nloops > 0)
|
|
ins1(c, A_JMP, abranch(loop_brk[nloops - 1]));
|
|
break;
|
|
case N_CONTINUE:
|
|
if (nloops > 0)
|
|
ins1(c, A_JMP, abranch(loop_cont[nloops - 1]));
|
|
break;
|
|
case N_SWITCH: {
|
|
/* Lower to a chain of compares. Scrutinee lands in a fresh
|
|
* local slot so case bodies can spill through SP without
|
|
* losing it. Cases are tried top-to-bottom; a `case:` arm
|
|
* with no exprs is the default and runs after all named
|
|
* arms fail. */
|
|
char *swname = aprintf(c->a, ".sw_%d", c->labelseq++);
|
|
int sloff = localoff(c, locals, swname, 8, frame);
|
|
cgexpr(c, n->lhs, *locals); /* AX = scrutinee */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sloff));
|
|
char *end = mklabel(c, "swend");
|
|
Node *defcase = NULL;
|
|
for (Node *cs = n->list; cs; cs = cs->next) {
|
|
if (cs->list == NULL) {
|
|
defcase = cs; /* save for last */
|
|
continue;
|
|
}
|
|
char *body = mklabel(c, "swcase");
|
|
char *next = mklabel(c, "swnext");
|
|
for (Node *e = cs->list; e; e = e->next) {
|
|
cgexpr(c, e, *locals); /* AX = case-expr */
|
|
ins2(c, A_MOVQ, amem(D_BP, sloff), areg(D_BX));
|
|
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
}
|
|
ins1(c, A_JMP, abranch(next));
|
|
label(c, body);
|
|
cgstmt(c, cs->body, locals, frame);
|
|
ins1(c, A_JMP, abranch(end));
|
|
label(c, next);
|
|
}
|
|
if (defcase)
|
|
cgstmt(c, defcase->body, locals, frame);
|
|
label(c, end);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
cgfn(Cg *c, FILE *out, Node *fn)
|
|
{
|
|
if (fn->body == NULL) return; /* extern decl, no body */
|
|
|
|
/* fresh per-fn state */
|
|
c->head = c->tail = NULL;
|
|
c->fnname = fn->str;
|
|
c->cur_mod = (fn->module && fn->module[0]) ? fn->module : NULL;
|
|
c->labelseq = 0;
|
|
cg_stack_arg_cursor = 0;
|
|
ndefers = 0;
|
|
nloops = 0;
|
|
cg_ret_type = fn->type ? fn->type->ret : NULL;
|
|
cg_retscr = 0;
|
|
cg_tupfscr = 0;
|
|
cg_tupargscr = 0;
|
|
cg_tupargscr_sz = 0;
|
|
cg_aggargscr = 0;
|
|
cg_aggargscr_sz = 0;
|
|
cg_tagbase = 0;
|
|
cg_tagbase_sz = 0;
|
|
cg_ntagscr = 0;
|
|
cg_appendscr = 0;
|
|
cg_sret_arg_off = 0;
|
|
cg_sret_dest_off = 0;
|
|
cg_sret_dest_sym = NULL;
|
|
cg_sretscr_off = 0;
|
|
cg_sretscr_sz = 0;
|
|
cg_sret_forward = 0;
|
|
|
|
int frame = 0;
|
|
Local *locals = NULL;
|
|
cg_frame = &frame;
|
|
|
|
/* TEXT directive comes first; framesize is filled at the end. */
|
|
Prog *text = newprog(c, A_TEXT);
|
|
/* Mangle the label using the fn's own module as the hint — picks
|
|
* the right entry when multiple modules export the same leaf. */
|
|
text->to = mafn(c, fn->str, c->cur_mod);
|
|
text->from.offset = 0; /* framesize patched below */
|
|
emit(c, text);
|
|
|
|
/* prologue */
|
|
ins1(c, A_PUSHQ, areg(D_BP));
|
|
ins2(c, A_MOVQ, areg(D_SP), areg(D_BP));
|
|
Prog *subsp = newprog(c, A_SUBQ);
|
|
subsp->from = aimm(0);
|
|
subsp->to = areg(D_SP);
|
|
emit(c, subsp);
|
|
|
|
/* sret discipline (#23): plain TY_STRUCT return > 24B consumes
|
|
* RDI as a hidden first-arg dest pointer. Spill it to @sretarg
|
|
* before the user-param loop so cgreturn can write through it,
|
|
* and start the user-arg register counter at 1 to shift every
|
|
* declared arg right by one (SI/DX/CX/R8/R9/+stack). */
|
|
if (cg_sret_retsize(cg_ret_type) > 0) {
|
|
cg_sret_arg_off = local_alloc(c, &locals, "@sretarg",
|
|
8, &frame);
|
|
ins2(c, A_MOVQ, areg(D_DI),
|
|
amem(D_BP, cg_sret_arg_off));
|
|
}
|
|
|
|
/* spill incoming arg registers to local slots. Slice params
|
|
* occupy 24 bytes; float params land in XMM0..7 (counted
|
|
* separately from integer DI/SI/DX/CX/R8/R9). */
|
|
int argi = (cg_sret_arg_off != 0) ? 1 : 0;
|
|
int fargi = 0;
|
|
Tparam *tp = fn->type ? fn->type->params : NULL;
|
|
for (Node *p = fn->list; p; p = p->next) {
|
|
if (p->str == NULL || strcmp(p->str, "...") == 0) {
|
|
if (tp) tp = tp->next;
|
|
continue;
|
|
}
|
|
Type *pt = tp ? tp->type : NULL;
|
|
Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
|
|
int slice = (pu && pu->kind == TY_SLICE);
|
|
int is_str = type_isstr(pt);
|
|
int is_struct = pu && pu->kind == TY_STRUCT && pu->size <= 16;
|
|
/* #271: a by-value array param, or a struct param > 16B —
|
|
* received as ceil(sz/8) GP eightbytes, the callee twin of the
|
|
* generalised aggregate-arg push. The ≤16B struct keeps its own
|
|
* (possibly SSE-classified) path above. */
|
|
int is_bigagg = pu && ((pu->kind == TY_ARRAY)
|
|
|| (pu->kind == TY_STRUCT && pu->size > 16));
|
|
int agg_eb = is_bigagg ? (int)((pu->size + 7) / 8) : 0;
|
|
int tagged_sz = tagged_arg_size(pt);
|
|
int is_tagged = tagged_sz > 0;
|
|
int isf = cg_isfloat(pt);
|
|
|
|
/* #163: tuple PARAM receive (param twin of #164's return).
|
|
* Walk the tuple's elements over the SysV arg cursor — a float
|
|
* reads its XMM (X0..X7), everything else an INTEGER arg reg
|
|
* (DI/SI/..); a slice/str its 3-word {ptr,len,cap} header — and
|
|
* store each into the param's frame slot positionally (eoff
|
|
* steps by the element's slot width: a slice/str 24B, else 8B,
|
|
* matching the tuple-field-access offset walk + the SEND). Reg
|
|
* overflow loud-stops (rule 7), the partial-spill stitch out of
|
|
* scope (twin of #164's cap). Placed before the single-class
|
|
* eightbytes logic below, which can't model a mixed GP/SSE
|
|
* aggregate. */
|
|
if (pu && pu->kind == TY_TUPLE) {
|
|
int sz = (int)pu->size;
|
|
int off = localoff(c, &locals, p->str, sz, &frame);
|
|
int eoff = 0, ef32;
|
|
for (Tparam *te = pu->params; te; te = te->next) {
|
|
Type *teu = (te->type
|
|
&& te->type->kind == TY_NAMED)
|
|
? te->type->under : te->type;
|
|
int wide = teu && (teu->kind == TY_SLICE
|
|
|| teu->kind == TY_STR);
|
|
if (fld_isfloat(te->type, &ef32)) {
|
|
if (fargi >= 8)
|
|
fatal("tuple param float element "
|
|
"overflows SSE arg regs "
|
|
"(X0..X7); stitch out of "
|
|
"scope, see #163");
|
|
ins2(c, ef32 ? A_MOVSS : A_MOVSD,
|
|
areg(sysv_fargregs[fargi]),
|
|
amem(D_BP, off + eoff));
|
|
fargi++;
|
|
eoff += 8;
|
|
continue;
|
|
}
|
|
int eb = tuple_ebytes(wide);
|
|
if (argi + eb > 6)
|
|
fatal("tuple param element overflows "
|
|
"integer arg regs (DI/SI/DX/CX/R8/"
|
|
"R9); stitch out of scope, see #163");
|
|
for (int k = 0; k < eb; k++, argi++)
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off + eoff + k * 8));
|
|
eoff += wide ? (int)teu->size : 8;
|
|
}
|
|
if (tp) tp = tp->next;
|
|
continue;
|
|
}
|
|
|
|
/* #165: float-bearing struct PARAM receive (param twin of
|
|
* #163's tuple). Classify each SysV eightbyte; a lone-f64
|
|
* eightbyte reads its XMM (X0..X7), a pure-INT eightbyte its
|
|
* INTEGER arg reg (DI/SI/..), stored into the param's frame
|
|
* slot at the 8-byte eightbyte stride. Gated to qualifying
|
|
* structs by struct_float_class — all-int + f32-packed keep
|
|
* the GP transport below (byte-id / #165b). Placed before the
|
|
* single-class eightbyte logic, which can't model a mixed
|
|
* GP/SSE aggregate. Reg overflow loud-stops (rule 7). */
|
|
if (is_struct) {
|
|
int sclass[2], snb;
|
|
if ((snb = struct_float_class(pt, sclass)) > 0) {
|
|
int sz = (int)pu->size;
|
|
int off = localoff(c, &locals, p->str, sz, &frame);
|
|
for (int e = 0; e < snb; e++) {
|
|
if (sclass[e]) {
|
|
if (fargi >= 8)
|
|
fatal("float struct param "
|
|
"eightbyte overflows SSE "
|
|
"arg regs (X0..X7); stitch "
|
|
"out of scope, see #165");
|
|
ins2(c, A_MOVSD,
|
|
areg(sysv_fargregs[fargi]),
|
|
amem(D_BP, off + e * 8));
|
|
fargi++;
|
|
} else {
|
|
if (argi >= 6)
|
|
fatal("float struct param "
|
|
"eightbyte overflows "
|
|
"integer arg regs (DI/SI/"
|
|
"DX/CX/R8/R9); stitch out "
|
|
"of scope, see #165");
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off + e * 8));
|
|
argi++;
|
|
}
|
|
}
|
|
if (tp) tp = tp->next;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* Args overflowing register classes live at positive offsets
|
|
* from BP (16 + i*8). We register them as Locals at those
|
|
* offsets, no spill needed. */
|
|
int struct_eb = is_struct ? ((pu->size > 8) ? 2 : 1) : 0;
|
|
int tagged_eb = is_tagged ? (tagged_sz / 8) : 0;
|
|
/* str IS []u8: 3 eightbytes (ptr,len,cap), same as a slice
|
|
* — the caller pushes the triple (#1/Phase 3). */
|
|
int eightbytes = (slice || is_str) ? 3 :
|
|
(is_struct ? struct_eb :
|
|
(is_bigagg ? agg_eb :
|
|
(is_tagged ? tagged_eb : 1)));
|
|
int regs_left = isf ? (8 - fargi) : (6 - argi);
|
|
if (regs_left >= eightbytes) {
|
|
/* #60: route slice/str slot widths through Type.size SSoT
|
|
* so #1's ty_str.size bump propagates without retouching
|
|
* this site (or its stack-stitch mirror below). */
|
|
int sz = (slice || is_str) ? (int)pu->size :
|
|
(is_struct ? (int)pu->size :
|
|
(is_bigagg ? (int)pu->size :
|
|
(is_tagged ? tagged_sz : 8)));
|
|
int off = localoff(c, &locals, p->str, sz, &frame);
|
|
if (slice || is_str || is_struct || is_bigagg || is_tagged) {
|
|
for (int k = 0; k < eightbytes; k++, argi++)
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off + k * 8));
|
|
} else if (isf) {
|
|
int mov = type_isf32(pt) ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
areg(sysv_fargregs[fargi]),
|
|
amem(D_BP, off));
|
|
fargi++;
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off));
|
|
argi++;
|
|
}
|
|
} else if (eightbytes > 1 && regs_left > 0 &&
|
|
(slice || is_str || is_struct || is_bigagg || is_tagged)) {
|
|
/* Multi-word arg that partially fits in regs: caller
|
|
* filled (regs_left) registers greedily, the rest spilled
|
|
* to stack at positive BP offsets. Stitch a single local
|
|
* slot from both sources so the body sees a contiguous
|
|
* value. Mirrors the SysV greedy reg fill the caller
|
|
* does. */
|
|
/* #60: same SSoT routing as the regs-fit arm above. */
|
|
int sz = (slice || is_str) ? (int)pu->size :
|
|
(is_struct ? (int)pu->size :
|
|
(is_bigagg ? (int)pu->size :
|
|
(is_tagged ? tagged_sz : 8)));
|
|
int off = localoff(c, &locals, p->str, sz, &frame);
|
|
extern int cg_stack_arg_cursor;
|
|
int k = 0;
|
|
for (; k < regs_left; k++, argi++)
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off + k * 8));
|
|
for (; k < eightbytes; k++) {
|
|
int stack_off = 16 +
|
|
cg_stack_arg_cursor * 8;
|
|
cg_stack_arg_cursor++;
|
|
ins2(c, A_MOVQ, amem(D_BP, stack_off),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + k * 8));
|
|
}
|
|
} else {
|
|
/* stack-spilled. Access in place via positive BP offset. */
|
|
static int stack_arg_off;
|
|
(void)stack_arg_off;
|
|
Local *l = amalloc(c->a, sizeof *l);
|
|
l->name = p->str;
|
|
/* spilled args layout: each takes 8B (ptr/len/etc); we
|
|
* only support the simple case of plain int/float here. */
|
|
extern int cg_stack_arg_cursor;
|
|
l->off = 16 + cg_stack_arg_cursor * 8;
|
|
cg_stack_arg_cursor += eightbytes;
|
|
l->next = locals;
|
|
locals = l;
|
|
}
|
|
if (tp) tp = tp->next;
|
|
}
|
|
|
|
/* Iterate the fn body's statements directly rather than dispatching
|
|
* the outermost N_BLOCK through cgstmt — N_BLOCK now save/restores
|
|
* the locals head to scope inner shadows, but the function body is
|
|
* not "an inner block": defers (queued during the body) and the
|
|
* implicit-return epilogue both call cgexpr after this loop and
|
|
* resolve identifiers via localfind, so the body's locals must
|
|
* still be in *locals when we get there. */
|
|
if (fn->body && fn->body->kind == N_BLOCK) {
|
|
for (Node *s = fn->body->list; s; s = s->next)
|
|
cgstmt(c, s, &locals, &frame);
|
|
} else {
|
|
cgstmt(c, fn->body, &locals, &frame);
|
|
}
|
|
|
|
/* implicit return for void functions */
|
|
if (c->tail->as != A_RET) {
|
|
for (int di = ndefers - 1; di >= 0; di--)
|
|
cgexpr(c, defers[di], locals);
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
}
|
|
|
|
/* round frame to 16; patch SUBQ */
|
|
if (frame & 15) frame = (frame + 15) & ~15;
|
|
subsp->from.offset = frame;
|
|
text->from.offset = frame;
|
|
|
|
txt_emit(out, c->head);
|
|
}
|
|
|
|
/* Escape one byte for an asm string literal — the same rules
|
|
* emit_data and emit_defs already use. */
|
|
static void
|
|
emit_data_byte(FILE *out, u8 b)
|
|
{
|
|
if (b == '"' || b == '\\')
|
|
fprintf(out, "\\%c", b);
|
|
else if (b < 0x20 || b >= 0x7f)
|
|
fprintf(out, "\\x%02x", b);
|
|
else
|
|
fputc(b, out);
|
|
}
|
|
|
|
/* Emit `DIR NAME(SB),"<8 LE bytes of v>"`. Used for scalar `def`
|
|
* constants (DATA) and scalar `let` globals (DATAW). */
|
|
static void
|
|
emit_data_row(FILE *out, const char *dir, const char *name, u64 v)
|
|
{
|
|
fprintf(out, "%s %s(SB),\"", dir, name);
|
|
for (int i = 0; i < 8; i++)
|
|
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
|
|
fputs("\"\n", out);
|
|
}
|
|
|
|
/* Emit `DIR NAME(SB),"<sz zero bytes>"`. Used for top-level str/
|
|
* slice/struct lets without a baked-in initialiser — the slot is
|
|
* pre-zeroed and the program writes the real value at runtime. */
|
|
static void
|
|
emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz)
|
|
{
|
|
fprintf(out, "%s %s(SB),\"", dir, name);
|
|
for (int i = 0; i < sz; i++)
|
|
emit_data_byte(out, 0);
|
|
fputs("\"\n", out);
|
|
}
|
|
|
|
/* Emit DATAW directives for top-level mutable `let` decls.
|
|
*
|
|
* Scalar lets (8B): emit the literal value, or 0 if no init.
|
|
* Non-literal init: skip — undefined symbol surfaces at link time.
|
|
*
|
|
* str lets (16B): three init shapes are wired:
|
|
* - no rhs / `nil` / `""` → 16 zero bytes
|
|
* - `"literal"` (non-empty) → 8 zero placeholder + 8 LE len,
|
|
* plus DATAR patching the ptr
|
|
* half with the interned strlit's
|
|
* runtime VA at link time.
|
|
*
|
|
* Slice lets (24B): no-init only — the slot is zero. There's no
|
|
* literal slice syntax to honour, so this is the natural shape.
|
|
*
|
|
* Struct lets (size from Type.size): no-init only. */
|
|
/* Emit a (DATA|DATAW) row for a float-typed top-level let/def with a
|
|
* FLOATLIT RHS (optionally wrapped in N_CAST or N_UN(±, ...)). Shared
|
|
* SSoT for emit_lets's float arm and emit_defs's float arm (#129
|
|
* Phase A.1, rule-12). The N_UN peel mirrors fold_int_literal's
|
|
* MINUS/TILDE/PLUS peel (#24) — the float arm had never been given
|
|
* the same treatment, so `let g: f64 = -1.5;` silently fell through
|
|
* to no-emit + undef-ref at link. Returns 1 on emit, 0 if the rhs
|
|
* shape doesn't reduce to a foldable float literal. */
|
|
static int
|
|
emit_floatlit_data(FILE *out, Cg *c, const char *directive,
|
|
const char *name, const char *module, Type *t, Node *rhs)
|
|
{
|
|
int isf32 = type_isf32(t);
|
|
int sz = isf32 ? 4 : 8;
|
|
u64 v = 0;
|
|
int neg = 0;
|
|
if (rhs != NULL) {
|
|
Node *r = rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r != NULL && r->kind == N_UN
|
|
&& (r->op == TK_MINUS || r->op == TK_PLUS)) {
|
|
if (r->op == TK_MINUS) neg = 1;
|
|
r = r->lhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
}
|
|
if (r == NULL || r->kind != N_FLOATLIT) return 0;
|
|
if (isf32) {
|
|
union { float f; u32 u; } x;
|
|
x.f = (float)r->fval;
|
|
v = (u64)x.u;
|
|
} else {
|
|
union { double d; u64 u; } x;
|
|
x.d = r->fval;
|
|
v = x.u;
|
|
}
|
|
}
|
|
fprintf(out, "%s %s(SB),\"", directive,
|
|
mod_mangle_value(c, name, module));
|
|
/* IEEE-754 sign-bit XOR for negation happens INSIDE the emit
|
|
* loop on the top byte only — semantically identical to a whole-
|
|
* u64 XOR with 2^63 (or 2^31 for f32) but never materialises
|
|
* that constant. Mirrors the wwstage helper's shape so the
|
|
* cgen.ww self-rebuild stays cs==ww byte-identical. */
|
|
for (int i = 0; i < sz; i++) {
|
|
u8 b = (u8)((v >> (i * 8)) & 0xff);
|
|
if (neg && i == sz - 1)
|
|
b = (u8)(b ^ 0x80);
|
|
emit_data_byte(out, b);
|
|
}
|
|
fputs("\"\n", out);
|
|
return 1;
|
|
}
|
|
|
|
/* Forward declaration: emit_struct_lit_bytes recurses into
|
|
* emit_array_lit_bytes for nested array fields (#129 A.3 closes the
|
|
* A.2 shape-15 park). Defined further down. */
|
|
static int emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs,
|
|
int emit_phase);
|
|
|
|
/* emit_struct_lit_bytes — emit the byte sequence for a struct-typed
|
|
* top-level let/def whose rhs is an N_STRUCTLIT (or NULL for bare
|
|
* no-rhs). Walks Tfield list in declaration order, zero-fills padding
|
|
* gaps via the offset table (rule 13), and dispatches per field type:
|
|
* integer/bool/nil via fold_int_literal, float via emit_floatlit_data's
|
|
* peel+bitcast core inlined, nested struct via recursion (the per-field
|
|
* inner literal lookup; nested-struct field-name-leak is a separate
|
|
* #145 bug filed against the parser/checker — the recursion is
|
|
* unblocked because emit-time field resolution goes through the type
|
|
* table, not the parser's symbol table). Array / str / slice / ptr-
|
|
* with-address fields are out of #129 A.2 scope — fatals loudly per
|
|
* rule-7 so a future consumer gets a precise stop rather than a
|
|
* silent zero-emit.
|
|
*
|
|
* Shared by emit_struct_data (#129 Phase A.2) below; broken out so the
|
|
* recursive call can recurse on the inner field bytes without re-
|
|
* opening the "DIR name(SB),\"" prefix. */
|
|
static int
|
|
emit_struct_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, u64 base)
|
|
{
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (u == NULL || u->kind != TY_STRUCT) return 0;
|
|
u64 pos = base;
|
|
for (Tfield *f = u->fields; f != NULL; f = f->next) {
|
|
u64 fstart = base + f->offset;
|
|
while (pos < fstart) {
|
|
emit_data_byte(out, 0);
|
|
pos++;
|
|
}
|
|
Node *v = NULL;
|
|
if (rhs != NULL) {
|
|
for (Node *fn = rhs->list; fn != NULL; fn = fn->next) {
|
|
if (fn->str && f->name
|
|
&& strcmp(fn->str, f->name) == 0) {
|
|
v = fn->lhs;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
int fsz = (int)f->type->size;
|
|
if (v == NULL) {
|
|
for (int i = 0; i < fsz; i++) emit_data_byte(out, 0);
|
|
pos += (u64)fsz;
|
|
continue;
|
|
}
|
|
Node *vr = v;
|
|
while (vr != NULL && vr->kind == N_CAST) vr = vr->lhs;
|
|
Type *fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (fu && fu->kind == TY_STRUCT) {
|
|
/* Recurse into nested struct lit. Pre-#145 the parser/
|
|
* checker has its own gap on inner-N_STRUCTLIT field
|
|
* name resolution; this emit recursion goes through
|
|
* the type table so it's correct in isolation. */
|
|
if (vr == NULL || vr->kind != N_STRUCTLIT)
|
|
fatal("emit_struct_lit_bytes: nested struct "
|
|
"field '%s' rhs is not N_STRUCTLIT "
|
|
"(#129 A.2)", f->name ? f->name : "?");
|
|
(void)emit_struct_lit_bytes(out, c, f->type, vr, fstart);
|
|
pos = fstart + (u64)fsz;
|
|
continue;
|
|
}
|
|
/* #129 A.3: array-typed field with N_ARRLIT rhs (the shape
|
|
* parked in A.2). Calls emit_array_lit_bytes which dispatches
|
|
* by element kind (int/float/struct). Returns 0 if the rhs
|
|
* shape can't reduce — fatal here per rule-7 since the field
|
|
* is declared array-typed and a non-reducible inner rhs is
|
|
* a real bug surface, not a fall-through. */
|
|
if (fu && fu->kind == TY_ARRAY) {
|
|
if (vr == NULL || vr->kind != N_ARRLIT)
|
|
fatal("emit_struct_lit_bytes: array field "
|
|
"'%s' rhs is not N_ARRLIT (#129 A.3)",
|
|
f->name ? f->name : "?");
|
|
if (!emit_array_lit_bytes(out, c, f->type, vr, 1))
|
|
fatal("emit_struct_lit_bytes: array field "
|
|
"'%s' rhs has non-reducible elements "
|
|
"(#129 A.3)", f->name ? f->name : "?");
|
|
pos = fstart + (u64)fsz;
|
|
continue;
|
|
}
|
|
if (type_isfloat(f->type)) {
|
|
int isf32 = type_isf32(f->type);
|
|
u64 fv = 0;
|
|
int neg = 0;
|
|
Node *fr = vr;
|
|
if (fr != NULL && fr->kind == N_UN
|
|
&& (fr->op == TK_MINUS || fr->op == TK_PLUS)) {
|
|
if (fr->op == TK_MINUS) neg = 1;
|
|
fr = fr->lhs;
|
|
while (fr != NULL && fr->kind == N_CAST)
|
|
fr = fr->lhs;
|
|
}
|
|
if (fr == NULL || fr->kind != N_FLOATLIT)
|
|
fatal("emit_struct_lit_bytes: float field "
|
|
"'%s' rhs not foldable FLOATLIT (#129 A.2)",
|
|
f->name ? f->name : "?");
|
|
if (isf32) {
|
|
union { float f; u32 u; } x;
|
|
x.f = (float)fr->fval;
|
|
fv = (u64)x.u;
|
|
} else {
|
|
union { double d; u64 u; } x;
|
|
x.d = fr->fval;
|
|
fv = x.u;
|
|
}
|
|
for (int i = 0; i < fsz; i++) {
|
|
u8 b = (u8)((fv >> (i * 8)) & 0xff);
|
|
if (neg && i == fsz - 1) b = (u8)(b ^ 0x80);
|
|
emit_data_byte(out, b);
|
|
}
|
|
pos = fstart + (u64)fsz;
|
|
continue;
|
|
}
|
|
u64 iv = 0;
|
|
if (!fold_int_literal(vr, &iv))
|
|
fatal("emit_struct_lit_bytes: field '%s' rhs not a "
|
|
"foldable literal (str/slice/ptr/array fields "
|
|
"are out of #129 A.2 scope)",
|
|
f->name ? f->name : "?");
|
|
for (int i = 0; i < fsz; i++)
|
|
emit_data_byte(out, (u8)((iv >> (i * 8)) & 0xff));
|
|
pos = fstart + (u64)fsz;
|
|
}
|
|
/* Tail padding to t->size. */
|
|
u64 end = base + t->size;
|
|
while (pos < end) {
|
|
emit_data_byte(out, 0);
|
|
pos++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* emit_struct_data — top-level wrapper that opens the DATA/DATAW
|
|
* directive and delegates the byte payload to emit_struct_lit_bytes.
|
|
* Shared SSoT between emit_lets's struct arm and emit_defs's struct
|
|
* arm (#129 Phase A.2, rule-12 sea-of-stars). Returns 1 on emit, 0 if
|
|
* the type isn't a struct. */
|
|
static int
|
|
emit_struct_data(FILE *out, Cg *c, const char *directive,
|
|
const char *name, const char *module, Type *t, Node *rhs)
|
|
{
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (u == NULL || u->kind != TY_STRUCT) return 0;
|
|
fprintf(out, "%s %s(SB),\"", directive,
|
|
mod_mangle_value(c, name, module));
|
|
emit_struct_lit_bytes(out, c, t, rhs, 0);
|
|
fputs("\"\n", out);
|
|
return 1;
|
|
}
|
|
|
|
/* emit_array_lit_bytes — emit alen * esz bytes for an [N]T top-level
|
|
* let/def with N_ARRLIT rhs. Per-element dispatch:
|
|
* - int element (covers bool/rune/typed-int/N_UN-int): fold_int_literal
|
|
* per element, emit LE bytes. Existing pre-#129-A.3 emit_lets array
|
|
* arm logic preserved byte-for-byte so the bootstrap consumers in
|
|
* lib/os, lib/bufio, lib/strings, lib/encoding/utf8, lib/strconv/
|
|
* stof_data don't shift.
|
|
* - float element (f32/f64): peel N_CAST/N_UN(±), bitcast magnitude
|
|
* via union (mirrors emit_floatlit_data), sign-XOR top byte of each
|
|
* element inline. NO 2^63 immediate.
|
|
* - struct element: per element call emit_struct_lit_bytes (#129 A.2
|
|
* helper).
|
|
* - other element kinds (str/slice/ptr-with-address/nested-array):
|
|
* return 0 — caller falls through to zero-init (str/slice accepts
|
|
* no-rhs already).
|
|
*
|
|
* Trailing `...` repeat marker fills remaining slots with the last
|
|
* value (mirrors the scalar repeat path). Returns 1 on emit, 0 if the
|
|
* rhs shape can't reduce to a foldable literal — caller MUST then
|
|
* fall back to zero-init / skip path; the caller opens the DATA/DATAW
|
|
* directive AFTER a successful validate-only call. Two-call pattern
|
|
* keeps emit-on-failure from emitting partial bytes.
|
|
*
|
|
* `emit_phase = 0` runs validate-only (returns 1 if ok); `emit_phase
|
|
* = 1` actually emits. */
|
|
static int
|
|
emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase)
|
|
{
|
|
Type *u = type_unwrap(t);
|
|
if (u == NULL || u->kind != TY_ARRAY) return 0;
|
|
Type *etype = u->sub;
|
|
Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype;
|
|
int esz = etype ? (int)etype->size : 1;
|
|
int alen = (int)u->alen;
|
|
|
|
if (eu && eu->kind == TY_STRUCT) {
|
|
/* Validate: every element must be N_STRUCTLIT (after N_CAST
|
|
* peel). */
|
|
int idx = 0;
|
|
Node *last_ev = NULL;
|
|
int repeat = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
if (ev == NULL || ev->kind != N_STRUCTLIT) return 0;
|
|
last_ev = ev;
|
|
idx++;
|
|
}
|
|
if (!emit_phase) return 1;
|
|
idx = 0;
|
|
repeat = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
emit_struct_lit_bytes(out, c, etype, ev, 0);
|
|
idx++;
|
|
}
|
|
while (idx < alen) {
|
|
if (repeat && last_ev != NULL)
|
|
emit_struct_lit_bytes(out, c, etype, last_ev, 0);
|
|
else
|
|
for (int b = 0; b < esz; b++)
|
|
emit_data_byte(out, 0);
|
|
idx++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* #129 A.3 capstone (PREREQ-1, #156): nested-array element [M]T
|
|
* inside [N][M]T. Mirror of the TY_STRUCT-element arm above and of
|
|
* the TY_ARRAY-field-in-struct arm in emit_struct_lit_bytes — recurse
|
|
* into emit_array_lit_bytes per element; recursion bottoms out at
|
|
* scalar (int/float) elements. esz = etype->size gives the per-
|
|
* element stride (rule 13, no manual stride math). The `...` repeat
|
|
* marker with nested-array elements is rejected loud (rule 7): no
|
|
* consumer needs it (powers_of_ten is fully enumerated) and the
|
|
* scalar-repeat byte-fill cannot reduce a nested N_ARRLIT. */
|
|
if (eu && eu->kind == TY_ARRAY) {
|
|
int idx = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0)
|
|
fatal("emit_array_lit_bytes: '...' repeat with "
|
|
"nested-array elements unsupported "
|
|
"(#129 A.3, rule 7)");
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
if (ev == NULL || ev->kind != N_ARRLIT) return 0;
|
|
if (!emit_array_lit_bytes(out, c, etype, ev, 0))
|
|
return 0;
|
|
idx++;
|
|
}
|
|
if (!emit_phase) return 1;
|
|
idx = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
emit_array_lit_bytes(out, c, etype, ev, 1);
|
|
idx++;
|
|
}
|
|
while (idx < alen) {
|
|
for (int b = 0; b < esz; b++)
|
|
emit_data_byte(out, 0);
|
|
idx++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
if (type_isfloat(etype)) {
|
|
int isf32 = type_isf32(etype);
|
|
/* Validate: every element must be N_FLOATLIT (after N_CAST
|
|
* + optional N_UN(±) peel). */
|
|
int idx = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) break;
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
if (ev != NULL && ev->kind == N_UN
|
|
&& (ev->op == TK_MINUS || ev->op == TK_PLUS)) {
|
|
ev = ev->lhs;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
}
|
|
if (ev == NULL || ev->kind != N_FLOATLIT) return 0;
|
|
idx++;
|
|
}
|
|
if (!emit_phase) return 1;
|
|
idx = 0;
|
|
u64 last_bits = 0;
|
|
int last_neg = 0;
|
|
int repeat = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
int neg = 0;
|
|
if (ev != NULL && ev->kind == N_UN
|
|
&& (ev->op == TK_MINUS || ev->op == TK_PLUS)) {
|
|
if (ev->op == TK_MINUS) neg = 1;
|
|
ev = ev->lhs;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
}
|
|
u64 bits = 0;
|
|
if (isf32) {
|
|
union { float f; u32 u; } x;
|
|
x.f = (float)ev->fval;
|
|
bits = (u64)x.u;
|
|
} else {
|
|
union { double d; u64 u; } x;
|
|
x.d = ev->fval;
|
|
bits = x.u;
|
|
}
|
|
for (int b = 0; b < esz; b++) {
|
|
u8 byt = (u8)((bits >> (b * 8)) & 0xff);
|
|
if (neg && b == esz - 1) byt = (u8)(byt ^ 0x80);
|
|
emit_data_byte(out, byt);
|
|
}
|
|
last_bits = bits;
|
|
last_neg = neg;
|
|
idx++;
|
|
}
|
|
while (idx < alen) {
|
|
if (repeat) {
|
|
for (int b = 0; b < esz; b++) {
|
|
u8 byt = (u8)((last_bits >> (b * 8)) & 0xff);
|
|
if (last_neg && b == esz - 1)
|
|
byt = (u8)(byt ^ 0x80);
|
|
emit_data_byte(out, byt);
|
|
}
|
|
} else {
|
|
for (int b = 0; b < esz; b++)
|
|
emit_data_byte(out, 0);
|
|
}
|
|
idx++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* Int-element path — preserved BYTE-FOR-BYTE from the pre-A.3
|
|
* emit_lets in-place array arm so the bootstrap consumers (u8 /
|
|
* i8 / u16 arrays in lib/os, lib/bufio, lib/strings, lib/
|
|
* encoding/utf8, lib/strconv/stof_data) don't shift. */
|
|
u64 *vals = amalloc(c->a, sizeof(u64) * (size_t)alen);
|
|
int idx = 0;
|
|
int ok = 1;
|
|
u64 last = 0;
|
|
int repeat = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
if (ev == NULL) { ok = 0; break; }
|
|
if (!fold_int_literal(ev, &last)) { ok = 0; break; }
|
|
vals[idx++] = last;
|
|
}
|
|
if (!ok) return 0;
|
|
if (!emit_phase) return 1;
|
|
if (repeat) {
|
|
while (idx < alen) vals[idx++] = last;
|
|
} else {
|
|
while (idx < alen) vals[idx++] = 0;
|
|
}
|
|
for (int i = 0; i < alen; i++) {
|
|
u64 v = vals[i];
|
|
for (int b = 0; b < esz; b++)
|
|
emit_data_byte(out, (u8)((v >> (b * 8)) & 0xff));
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* emit_strarray_data — module-level `let xs: [N]str = ["a","b",…];`
|
|
* static init (#18). The str-element case can't ride emit_array_lit_bytes:
|
|
* a str element carries a ptr→rodata relocation, not just bytes. So the
|
|
* scalar-str-global pattern (emit_lets str arm: DATAW header with a zero
|
|
* ptr placeholder + inline LE len, then a DATAR patching the ptr half)
|
|
* is applied per element at offset idx*esz. Each strlit was pre-interned
|
|
* by let_pre_intern so its rodata _S_ row exists before this row's DATAR
|
|
* references it.
|
|
*
|
|
* Scoped to DATAW (writable `let`): A_DATAR requires its holder be a
|
|
* DATAW slot (w6a asm.c:362), so a read-only `def [N]str` can't carry
|
|
* the relocs — that generalisation is a #18 follow-up. Returns 0 if the
|
|
* element type isn't str, leaving the generic array path / zero-init to
|
|
* the caller. */
|
|
static int
|
|
emit_strarray_data(FILE *out, Cg *c, const char *directive,
|
|
const char *name, const char *module, Type *t, Node *rhs)
|
|
{
|
|
Type *u = type_unwrap(t);
|
|
if (u == NULL || u->kind != TY_ARRAY) return 0;
|
|
Type *etype = u->sub;
|
|
Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype;
|
|
if (eu == NULL || eu->kind != TY_STR) return 0;
|
|
if (strcmp(directive, "DATAW") != 0) return 0;
|
|
int esz = (int)etype->size;
|
|
int alen = (int)u->alen;
|
|
|
|
/* Validate: each cast-peeled element is an N_STRLIT, up to an
|
|
* optional trailing `...` repeat marker. Bail (return 0) on any
|
|
* non-strlit so a non-reducible rhs still falls through to the
|
|
* generic path rather than emitting a partial row. */
|
|
Node *last_ev = NULL;
|
|
int repeat = 0;
|
|
int cnt = 0;
|
|
for (Node *e = rhs->list; e && cnt < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) { repeat = 1; break; }
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
if (ev == NULL || ev->kind != N_STRLIT) return 0;
|
|
last_ev = ev;
|
|
cnt++;
|
|
}
|
|
|
|
const char *sym = mod_mangle_value(c, name, module);
|
|
fprintf(out, "DATAW %s(SB),\"", sym);
|
|
int idx = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) break;
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
for (int i = 0; i < 8; i++) emit_data_byte(out, 0);
|
|
u64 v = ev->strlen;
|
|
for (int i = 0; i < 8; i++)
|
|
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
|
|
for (int i = 16; i < esz; i++) emit_data_byte(out, 0);
|
|
idx++;
|
|
}
|
|
while (idx < alen) {
|
|
u64 v = (repeat && last_ev != NULL) ? last_ev->strlen : 0;
|
|
for (int i = 0; i < 8; i++) emit_data_byte(out, 0);
|
|
for (int i = 0; i < 8; i++)
|
|
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
|
|
for (int i = 16; i < esz; i++) emit_data_byte(out, 0);
|
|
idx++;
|
|
}
|
|
fputs("\"\n", out);
|
|
|
|
idx = 0;
|
|
for (Node *e = rhs->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) break;
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
if (ev->strlen > 0) {
|
|
const char *lab = intern_strlit(c, ev->str, ev->strlen);
|
|
fprintf(out, "DATAR %s+%d(SB),%s(SB)\n",
|
|
sym, idx * esz, lab);
|
|
}
|
|
idx++;
|
|
}
|
|
while (idx < alen) {
|
|
if (repeat && last_ev != NULL && last_ev->strlen > 0) {
|
|
const char *lab = intern_strlit(c, last_ev->str,
|
|
last_ev->strlen);
|
|
fprintf(out, "DATAR %s+%d(SB),%s(SB)\n",
|
|
sym, idx * esz, lab);
|
|
}
|
|
idx++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* emit_array_data — opens DATA/DATAW prefix on validate success, then
|
|
* emits payload. Two-pass keeps emit-on-failure from emitting partial
|
|
* bytes (would corrupt the asm if rhs reduces partway through). */
|
|
static int
|
|
emit_array_data(FILE *out, Cg *c, const char *directive,
|
|
const char *name, const char *module, Type *t, Node *rhs)
|
|
{
|
|
Type *u = type_unwrap(t);
|
|
if (u == NULL || u->kind != TY_ARRAY) return 0;
|
|
/* str-element arrays carry per-element ptr relocations — handled
|
|
* by the dedicated DATAW+DATAR helper (#18). */
|
|
if (emit_strarray_data(out, c, directive, name, module, t, rhs))
|
|
return 1;
|
|
if (!emit_array_lit_bytes(out, c, t, rhs, 0)) return 0;
|
|
fprintf(out, "%s %s(SB),\"", directive,
|
|
mod_mangle_value(c, name, module));
|
|
emit_array_lit_bytes(out, c, t, rhs, 1);
|
|
fputs("\"\n", out);
|
|
return 1;
|
|
}
|
|
|
|
/* emit_slice_data — module-level `let g: []T = [v0, v1, …];` static
|
|
* init (#10 part a). A slice literal needs three things: a writable
|
|
* backing holding the k elements, a 24B header { ptr, len, cap }, and a
|
|
* DATAR patching the ptr word with the backing's VA. The backing rides
|
|
* the emit_array_lit_bytes choke-point via a synthesized [k]T so int /
|
|
* float / struct / nested-array elements reduce exactly as a [N]T
|
|
* global's do. The backing symbol is "<mangled g>.d": a second '.' can
|
|
* never collide with a user global, since source identifiers carry no
|
|
* '.' (one is inserted only by the module mangle).
|
|
*
|
|
* Scoped to a writable `let` — A_DATAR's holder must be a DATAW slot
|
|
* (w6a asm.c:362), so a read-only `def []T = [...]` can't carry the ptr
|
|
* reloc. That, a `...` repeat (a slice literal has no target length),
|
|
* and slice-of-{str,slice,tagged} elements (per-element relocs / #17)
|
|
* all loud-stop (rule 7) — #10 follow-ups, never silent fall-through.
|
|
* Returns 0 only on the early shape guards (not a slice / rhs not
|
|
* N_ARRLIT) so the caller's gate stays the sole entry contract. */
|
|
static int
|
|
emit_slice_data(FILE *out, Cg *c, const char *directive, const char *name,
|
|
const char *module, Type *t, Node *rhs)
|
|
{
|
|
Type *u = type_unwrap(t);
|
|
if (u == NULL || u->kind != TY_SLICE) return 0;
|
|
if (rhs == NULL || rhs->kind != N_ARRLIT) return 0;
|
|
if (strcmp(directive, "DATAW") != 0)
|
|
fatal("emit_slice_data: slice-literal static-init needs a "
|
|
"writable `let` (DATAR holder must be DATAW, w6a "
|
|
"asm.c:362); read-only `def` unsupported (#10, rule 7)");
|
|
Type *etype = u->sub;
|
|
Type *eu = (etype && etype->kind == TY_NAMED) ? etype->under : etype;
|
|
if (eu && (eu->kind == TY_STR || eu->kind == TY_SLICE
|
|
|| eu->kind == TY_TAGGED))
|
|
fatal("emit_slice_data: slice-of-{str,slice,tagged} literal "
|
|
"static-init unsupported (#10 follow-up, rule 7)");
|
|
int k = 0;
|
|
for (Node *e = rhs->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str && strcmp(e->str, "...") == 0)
|
|
fatal("emit_slice_data: '...' repeat has no target "
|
|
"length in a slice literal (#10, rule 7)");
|
|
k++;
|
|
}
|
|
int esz = etype ? (int)etype->size : 1;
|
|
/* Synthesize [k]T to ride the emit_array_lit_bytes choke-point. */
|
|
Type arr;
|
|
memset(&arr, 0, sizeof arr);
|
|
arr.kind = TY_ARRAY;
|
|
arr.sub = etype;
|
|
arr.alen = (u64)k;
|
|
arr.size = (u64)k * (u64)esz;
|
|
if (!emit_array_lit_bytes(out, c, &arr, rhs, 0))
|
|
fatal("emit_slice_data: slice-literal element not a foldable "
|
|
"constant (#10, rule 7)");
|
|
|
|
const char *sym = mod_mangle_value(c, name, module);
|
|
const char *bk = aprintf(c->a, "%s.d", sym);
|
|
/* Writable backing data. */
|
|
fprintf(out, "DATAW %s(SB),\"", bk);
|
|
emit_array_lit_bytes(out, c, &arr, rhs, 1);
|
|
fputs("\"\n", out);
|
|
/* 24B header: ptr placeholder + LE len + LE cap (both = k). Word
|
|
* sizes from the type table (rule 13). */
|
|
fprintf(out, "DATAW %s(SB),\"", sym);
|
|
for (int i = 0; i < (int)ty_uintptr->size; i++) emit_data_byte(out, 0);
|
|
u64 kv = (u64)k;
|
|
for (int i = 0; i < (int)ty_size->size; i++)
|
|
emit_data_byte(out, (u8)((kv >> (i * 8)) & 0xff));
|
|
for (int i = 0; i < (int)ty_size->size; i++)
|
|
emit_data_byte(out, (u8)((kv >> (i * 8)) & 0xff));
|
|
fputs("\"\n", out);
|
|
/* Patch the ptr word with the backing VA. */
|
|
fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, bk);
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
emit_lets(Cg *c, FILE *out, Node *file)
|
|
{
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_LET) continue;
|
|
if (d->str == NULL || d->str[0] == '\0') continue;
|
|
int sz = let_emit_size(d->type);
|
|
if (sz == 0) continue;
|
|
if (let_isfloat(d->type)) {
|
|
(void)emit_floatlit_data(out, c, "DATAW",
|
|
d->str, d->module, d->type, d->rhs);
|
|
continue;
|
|
}
|
|
/* #129 A.2: gate `!let_isstruct` so an 8B struct lit
|
|
* (`struct { i32, i32 }`, `struct { f32, f32 }`, …) does
|
|
* NOT short-circuit through the scalar 8B `fold_int_literal`
|
|
* arm — fold-fail-`continue` would otherwise drop the let
|
|
* entirely, emitting no DATA and diverging from wwstage's
|
|
* emitletdataw (which gates its 8B scalar with `!issg`).
|
|
* Symmetric ordering with the wwstage struct arm. */
|
|
if (sz == 8 && !let_isarray(d->type) && !let_isstruct(d->type)) {
|
|
u64 v = 0;
|
|
if (d->rhs != NULL) {
|
|
Node *r = d->rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r == NULL) continue;
|
|
/* Same helper as emit_defs (#24): widens
|
|
* the gate to cover N_UN(TK_MINUS/TILDE/PLUS,
|
|
* leaf) so `let x: i8 = -1i8;` and friends
|
|
* encode as sign-extended two's-complement
|
|
* bytes. emit_data_row writes 8 LE bytes
|
|
* so narrow signed types just naturally
|
|
* round-trip via the sign-extended u64. */
|
|
if (!fold_int_literal(r, &v)) continue;
|
|
}
|
|
emit_data_row(out, "DATAW",
|
|
mod_mangle_value(c, d->str, d->module), v);
|
|
continue;
|
|
}
|
|
/* Strip leading casts on the rhs so a `nil: str` etc.
|
|
* reads the same as a bare nil. */
|
|
Node *r = NULL;
|
|
if (d->rhs != NULL) {
|
|
r = d->rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r == NULL) continue;
|
|
}
|
|
/* str literal init: bake the interned label's address
|
|
* into the ptr half via a DATAR reloc, set the len half
|
|
* inline. */
|
|
/* #43: gate via ty_str->size so #1 propagates. */
|
|
if (sz == (int)ty_str->size && r != NULL && r->kind == N_STRLIT
|
|
&& r->strlen > 0) {
|
|
const char *lab = intern_strlit(c, r->str, r->strlen);
|
|
const char *sym = mod_mangle_value(c, d->str, d->module);
|
|
u64 v = r->strlen;
|
|
/* 16-byte payload: 8 zero placeholder + LE len. */
|
|
fprintf(out, "DATAW %s(SB),\"", sym);
|
|
for (int i = 0; i < 8; i++) emit_data_byte(out, 0);
|
|
for (int i = 0; i < 8; i++)
|
|
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
|
|
fputs("\"\n", out);
|
|
fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, lab);
|
|
continue;
|
|
}
|
|
/* Array literal init: `let xs: [N]T = [v0, v1, ...];`. The
|
|
* helper dispatches per element kind (int/float/struct).
|
|
* Int-element path preserved BYTE-FOR-BYTE from pre-A.3 so
|
|
* bootstrap consumers (lib/os, lib/bufio, lib/strings, lib/
|
|
* encoding/utf8, lib/strconv/stof_data) don't shift. Float
|
|
* + struct elements gain emit; ptr / nested-array fall
|
|
* through to zero-init (existing path below). */
|
|
if (r != NULL && r->kind == N_ARRLIT && let_isarray(d->type)) {
|
|
if (emit_array_data(out, c, "DATAW", d->str,
|
|
d->module, d->type, r))
|
|
continue;
|
|
/* fall through to zero-init */
|
|
}
|
|
/* #10: slice-literal static init `let g: []T = [v0, …];`.
|
|
* Header { ptr, len, cap } + a writable backing + a DATAR
|
|
* patching ptr → backing. emit_slice_data loud-stops on the
|
|
* deferred element kinds and on the read-only / `...` shapes
|
|
* (rule 7); when the gate matches it always emits or fatals,
|
|
* never silently falls through. */
|
|
if (r != NULL && r->kind == N_ARRLIT && let_isslice(d->type)) {
|
|
if (emit_slice_data(out, c, "DATAW", d->str,
|
|
d->module, d->type, r))
|
|
continue;
|
|
}
|
|
/* Otherwise: zero-init. str accepts nil / ""; struct
|
|
* accepts no rhs at all; slice accepts nil; array with no
|
|
* literal init (or a non-constant one) zero-fills. */
|
|
if (r != NULL) {
|
|
int is_struct = let_isstruct(d->type);
|
|
int is_array = let_isarray(d->type);
|
|
int empty_str = (r->kind == N_STRLIT && r->strlen == 0);
|
|
/* #129 A.2: struct-typed let with N_STRUCTLIT rhs
|
|
* routes through the emit_struct_data SSoT. Pre-#129
|
|
* this fell through to `continue` and emit-NOTHING,
|
|
* so the link surfaced an undefined ref. */
|
|
if (is_struct && r->kind == N_STRUCTLIT) {
|
|
if (emit_struct_data(out, c, "DATAW", d->str,
|
|
d->module, d->type, r))
|
|
continue;
|
|
}
|
|
if (is_struct) continue;
|
|
if (is_array) continue;
|
|
if (r->kind != N_NIL && !empty_str) continue;
|
|
}
|
|
emit_data_row_zero(out, "DATAW",
|
|
mod_mangle_value(c, d->str, d->module), sz);
|
|
}
|
|
}
|
|
|
|
/* Emit DATA directives for top-level `def` constants whose value
|
|
* folds to an integer literal. The w6a side stores the bytes inside
|
|
* .text and accesses are RIP-relative.
|
|
*
|
|
* fold_int_literal (cmd/wcc/check.c) gates: int/rune literal,
|
|
* true/false/nil, and a unary +/-/~ over the same. `def NEG: i32 =
|
|
* -100;` arrives as N_UN(TK_MINUS, N_INTLIT) — the unary peel is
|
|
* exactly what the gate is for. Anything richer (sibling refs,
|
|
* arithmetic) falls through; emit_defs has no scope to resolve
|
|
* names. */
|
|
static void
|
|
emit_defs(Cg *c, FILE *out, Node *file)
|
|
{
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_DEF || d->rhs == NULL) continue;
|
|
u64 v;
|
|
if (fold_int_literal(d->rhs, &v)) {
|
|
fprintf(out, "DATA %s(SB),\"",
|
|
mod_mangle_value(c, d->str, d->module));
|
|
for (int i = 0; i < 8; i++) {
|
|
unsigned b = (unsigned)((v >> (i * 8)) & 0xff);
|
|
if (b == '"' || b == '\\')
|
|
fprintf(out, "\\%c", b);
|
|
else if (b < 0x20 || b >= 0x7f)
|
|
fprintf(out, "\\x%02x", b);
|
|
else
|
|
fputc(b, out);
|
|
}
|
|
fputs("\"\n", out);
|
|
continue;
|
|
}
|
|
/* Float-typed def with FLOATLIT (or N_UN(±,FLOATLIT)) rhs.
|
|
* Routes through the same SSoT helper as emit_lets's float
|
|
* arm — pre-#129 this fell through to no-emit + undef-ref
|
|
* at link. */
|
|
if (let_isfloat(d->type)) {
|
|
(void)emit_floatlit_data(out, c, "DATA",
|
|
d->str, d->module, d->type, d->rhs);
|
|
continue;
|
|
}
|
|
/* #129 A.2: struct-typed def with N_STRUCTLIT rhs. Parallel
|
|
* to emit_lets's struct arm; uses DATA (read-only) directive.
|
|
* Without the LOAD-side widening below the def's address
|
|
* still wouldn't be reachable, but storage is the precondition
|
|
* for the LOAD path to find something. */
|
|
if (let_isstruct(d->type) && d->rhs->kind == N_STRUCTLIT) {
|
|
(void)emit_struct_data(out, c, "DATA",
|
|
d->str, d->module, d->type, d->rhs);
|
|
continue;
|
|
}
|
|
/* #129 A.3: array-typed def with N_ARRLIT rhs. Parallel to
|
|
* emit_lets's array arm; uses DATA (read-only). LOAD-side
|
|
* widening at cgindex/cgdot resolves the def's address via
|
|
* LEAQ name(SB). */
|
|
if (let_isarray(d->type) && d->rhs->kind == N_ARRLIT) {
|
|
(void)emit_array_data(out, c, "DATA",
|
|
d->str, d->module, d->type, d->rhs);
|
|
continue;
|
|
}
|
|
/* #10: a read-only `def g: []T = [...]` slice literal can't
|
|
* carry the ptr reloc emit_slice_data needs (DATAR holder must
|
|
* be DATAW, w6a asm.c:362). Loud-stop rather than silently
|
|
* emit nothing and surface an undefined-ref at link. */
|
|
if (let_isslice(d->type) && d->rhs->kind == N_ARRLIT)
|
|
fatal("emit_defs: module-level slice-literal init needs "
|
|
"a writable `let` (DATAR holder must be DATAW, w6a "
|
|
"asm.c:362); read-only `def` unsupported (#10, "
|
|
"rule 7)");
|
|
}
|
|
(void)c;
|
|
}
|
|
|
|
/* Collect str-typed `def`s so cgexpr N_IDENT can splice them inline.
|
|
* Walks past any leading cast on the rhs (e.g. `def x: error = "x": error;`
|
|
* shows up as N_CAST wrapping an N_STRLIT). */
|
|
static void
|
|
sdef_collect(Cg *c, Node *file)
|
|
{
|
|
(void)file;
|
|
sdefs = NULL;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_DEF || d->rhs == NULL) continue;
|
|
Node *r = d->rhs;
|
|
while (r && r->kind == N_CAST) r = r->lhs;
|
|
if (r == NULL || r->kind != N_STRLIT) continue;
|
|
Sdef *s = amalloc(c->a, sizeof *s);
|
|
s->name = d->str;
|
|
s->mod = (d->module && d->module[0]) ? d->module : NULL;
|
|
s->bytes = r->str;
|
|
s->len = r->strlen;
|
|
s->next = sdefs;
|
|
sdefs = s;
|
|
}
|
|
}
|
|
|
|
/* Pre-intern strlits referenced from top-level `let` initialisers
|
|
* (e.g. `let g: str = "hello";`). Interning has to happen before
|
|
* emit_data walks the strlit list, but we don't want to reorder
|
|
* emit_data after emit_lets (the (DATA strlits, DATAW lets) section
|
|
* order is part of the byte-identity contract with the selfhost
|
|
* cgen). So this pass populates the strlit table; emit_lets later
|
|
* just looks up the label. */
|
|
static void
|
|
let_pre_intern(Cg *c, Node *file)
|
|
{
|
|
if (file == NULL) return;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_LET) continue;
|
|
Node *r = d->rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
/* #18: `let xs: [N]str = […];` — pre-intern each element's
|
|
* strlit in element order (then repeat-fill) so emit_strarray_
|
|
* data's DATAR rows find an _S_ rodata row. Must match that
|
|
* helper's interning order exactly to keep labels stable. */
|
|
Type *u = type_unwrap(d->type);
|
|
if (u != NULL && u->kind == TY_ARRAY
|
|
&& r != NULL && r->kind == N_ARRLIT) {
|
|
Type *eu = (u->sub && u->sub->kind == TY_NAMED)
|
|
? u->sub->under : u->sub;
|
|
if (eu != NULL && eu->kind == TY_STR) {
|
|
int alen = (int)u->alen;
|
|
int cnt = 0;
|
|
Node *last_ev = NULL;
|
|
int repeat = 0;
|
|
for (Node *e = r->list; e && cnt < alen;
|
|
e = e->next) {
|
|
if (e->kind == N_FIELD && e->str
|
|
&& strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST)
|
|
ev = ev->lhs;
|
|
if (ev == NULL || ev->kind != N_STRLIT)
|
|
break;
|
|
if (ev->strlen > 0)
|
|
(void)intern_strlit(c, ev->str,
|
|
ev->strlen);
|
|
last_ev = ev;
|
|
cnt++;
|
|
}
|
|
if (repeat && last_ev != NULL
|
|
&& last_ev->strlen > 0) {
|
|
while (cnt < alen) {
|
|
(void)intern_strlit(c,
|
|
last_ev->str,
|
|
last_ev->strlen);
|
|
cnt++;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
if (let_emit_size(d->type) != (int)ty_str->size) continue;
|
|
if (r == NULL || r->kind != N_STRLIT) continue;
|
|
if (r->strlen == 0) continue;
|
|
(void)intern_strlit(c, r->str, r->strlen);
|
|
}
|
|
}
|
|
|
|
void
|
|
cg_file(Cg *c, FILE *out, Node *file)
|
|
{
|
|
if (file == NULL || file->kind != N_FILE) return;
|
|
ffi_collect(c, file);
|
|
mod_collect(c, file);
|
|
sdef_collect(c, file);
|
|
let_collect(c, file);
|
|
strlits = NULL;
|
|
strlit_seq = 0;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_FNDECL) continue;
|
|
cgfn(c, out, d);
|
|
}
|
|
let_pre_intern(c, file);
|
|
emit_data(c, out);
|
|
emit_defs(c, out, file);
|
|
emit_lets(c, out, file);
|
|
}
|
|
|
|
void peephole(Cg *c) { (void)c; }
|
|
void regalloc_init(Cg *c) { (void)c; }
|