Receive side of #4's cgreturn ABI (aee8149) for TY_STRUCT lvalues of size <=24B. Producer materialises rhs into AX=bytes[0..7], DX=[8..15], CX=[16..23], zero-padded to 24B; receive sites here read the regs and write only `declared sz` bytes — MOVQ for full 8B chunks plus a sized tail (MOVL/MOVW/MOVB) by the *declared* struct size. ASYMMETRY: do NOT mirror the sender's three uniform MOVQs, else trailing 1..7B chunks overrun the next local slot. Tail chunks in {3,5,6,7} are unreachable under WW struct align rules (size%align==0) and fall through. Five sites wired in each stage (cstage cgen.c, wwstage cgenexpr.ww + cgenstmt.ww), call-result + structlit rhs at each: - N_LET `let s: T = bar()` / `= T{...}` cgenstmt cglet - N_ASSIGN N_IDENT-lhs `s = bar()` / `= T{...}` cgenexpr cgassign - N_ASSIGN single-DOT local-base `o.f = ...` - N_ASSIGN single-DOT ptr-base auto-deref `p.f = ...` - N_ASSIGN single-DOT global-base `g.f = ...` - N_ASSIGN chained-DOT depth>=2 `o.m.in = ...` (The four dot-flavors share one shape pattern, hence "5 sites".) Where the dst addr needs scratch (ptr-base/global-base/via_cx), it is loaded into BX after the call so CX stays as the third value word; for structlit field-walks BX is reloaded before each store since cgexpr clobbers AX/BX between fields. wwstage needed a new `structnaturalsize(si)` helper (cgenutil.ww): si.totsize is mis-named — it's slot-padded to 8 by registerstruct for stack-slot use, while the receive ABI wants the type's natural size (max(foff+fsz)). Splitting si.totsize into naturalsize + slotsize is tracked as the wwstage struct sizing follow-up (task #15); until that lands, the helper recovers the natural size at receive sites. Test 701_cgassign_struct.c (18 rows, 3 checks each — cstage value, wwstage value, asm byte-identity), wired in Makefile after 698. The headline ASYMMETRY case is the 20B `{i32×5}` row: sender pads to 24B via three MOVQs, receiver writes MOVQ AX +0, MOVQ DX +8, MOVL CX +16. A regression to a MOVQ tail there overruns 4B past the slot and flips the exit-code check. smoke.combined.ww is the auto-regen ride-along of strings.freeall landing in714d089(worker-shlex). Pre-existing gaps surfaced and tracked separately (not fixed here, out of scope): - task #16: silent drop of `(*p).f = ...` explicit-deref dot lhs. - task #17: silent zero of nested STRUCTLIT field in N_LET / N_ASSIGN initializer — the field_chain and field_global test rows use explicit field writes (`o.m.t = 10i64;`) rather than nested literals as a fixture-level workaround. - task #9: module-name-mangle for fn labels avoided in the field_global_call fixture by `let g: outer;` (no init). make test: 59/59. 994_w6c_ww + 995_self_rebuild PASS — bootstrap byte-identity is the load-bearing proof for this commit's scope.
7064 lines
228 KiB
C
7064 lines
228 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 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;
|
|
}
|
|
|
|
static int
|
|
node_isfloat(Node *n)
|
|
{
|
|
return n && cg_isfloat(n->type);
|
|
}
|
|
|
|
static int
|
|
type_isstr(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL) return 0;
|
|
return t->kind == TY_STR || t->kind == TY_UNTYPED_STR;
|
|
}
|
|
|
|
static int
|
|
node_isstr(Node *n)
|
|
{
|
|
return n && type_isstr(n->type);
|
|
}
|
|
|
|
static int
|
|
type_isslice(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_SLICE;
|
|
}
|
|
|
|
static int
|
|
node_isslice(Node *n)
|
|
{
|
|
return n && type_isslice(n->type);
|
|
}
|
|
|
|
static int
|
|
type_isf32(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_F32;
|
|
}
|
|
|
|
static int
|
|
node_isf32(Node *n)
|
|
{
|
|
return n && type_isf32(n->type);
|
|
}
|
|
|
|
/* fld_isfloat — true iff f's underlying type is f32 or f64. 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.
|
|
* Sets *isf32 to 1 for f32, 0 for f64. */
|
|
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_F32) { if (isf32) *isf32 = 1; return 1; }
|
|
return 0;
|
|
}
|
|
|
|
/* fld_issigned — true iff a sub-word field/element load needs sign
|
|
* extension (i8 → MOVSBQ, i16 → MOVSWQ, i32 → MOVSXD). Follows NAMED
|
|
* and ENUM aliases via type_isunsigned, then peels off the unsigned
|
|
* cases (u*, bool, rune) so what remains is the genuinely-signed
|
|
* narrow integers. The literal-kind ladder this replaces missed
|
|
* TY_ENUM aliases entirely (`type myflag = i8` silently emitted
|
|
* MOVZBQ on a field load). */
|
|
static int
|
|
fld_issigned(Type *t)
|
|
{
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (u == NULL) return 0;
|
|
if (u->kind == TY_BOOL) return 0;
|
|
if (type_isunsigned(u)) return 0;
|
|
return type_isint(u);
|
|
}
|
|
|
|
static int
|
|
fldloadop(Type *t, int sz)
|
|
{
|
|
int sigd = fld_issigned(t);
|
|
if (sz == 1) return sigd ? A_MOVSBQ : A_MOVZBQ;
|
|
if (sz == 2) return sigd ? A_MOVSWQ : A_MOVZWQ;
|
|
if (sz == 4) return sigd ? A_MOVSXD : A_MOVL;
|
|
return A_MOVQ;
|
|
}
|
|
|
|
static int
|
|
fldstoreop(Type *t, int sz)
|
|
{
|
|
(void)t;
|
|
if (sz == 1) return A_MOVB;
|
|
if (sz == 2) return A_MOVW;
|
|
if (sz == 4) return A_MOVL;
|
|
return A_MOVQ;
|
|
}
|
|
|
|
/* localloadop — read instruction for a scalar local/let load. Same
|
|
* dispatch as fldloadop, but keyed on the value's own type. Lets the
|
|
* caller emit MOVSXD / MOVSWQ / MOVSBQ on a signed-narrow slot instead
|
|
* of a raw MOVQ, so a slot that was last written by a narrow deref-
|
|
* store (`*p: *i32 = v` lowers to MOVL, only 4B) reads back as a
|
|
* properly-sign-extended i64. The natural N_ASSIGN / N_LET paths
|
|
* already store the value as a sign-extended 8B word so a MOVQ read
|
|
* accidentally works; deref-stores are the only path that touches
|
|
* fewer bytes than MOVQ reads. Fixing the read makes the slot's
|
|
* representation honest regardless of which store path wrote it. */
|
|
static int
|
|
localloadop(Type *t)
|
|
{
|
|
int sz = (t && t->size > 0) ? (int)t->size : 8;
|
|
if (sz != 1 && sz != 2 && sz != 4) return A_MOVQ;
|
|
return fldloadop(t, sz);
|
|
}
|
|
|
|
/* struct ≤16B all-INTEGER: 1 or 2 eightbyte regs.
|
|
* Returns 0 if not a struct or too large. */
|
|
static int
|
|
struct_arg_size(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_STRUCT) return 0;
|
|
return (int)t->size;
|
|
}
|
|
|
|
/* Tagged-union arg byte size: 16 (8B variants) or 24 (16B variants).
|
|
* Nullable-folded `(*T | void)` collapses to 8 bytes (just the
|
|
* pointer). Returns 0 if not a tagged union or too large to pass
|
|
* in registers. */
|
|
static int
|
|
tagged_arg_size(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
|
/* Param/let/struct contexts have 6 int regs (DI..R9) so a 48B
|
|
* tagged union (6 words) still fits in registers. Return values
|
|
* are stricter (AX:DX:CX, max 24B) — gated separately in
|
|
* cgreturn. */
|
|
if (t->size > 48) return 0;
|
|
return (int)t->size;
|
|
}
|
|
|
|
/* type_isnullable — TY_TAGGED with the (*T | void) one-word fold. */
|
|
static int
|
|
type_isnullable(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_TAGGED && t->nullable;
|
|
}
|
|
|
|
/* nullable_ptr_tag — index of the *T variant in a nullable union.
|
|
* Returns 0 or 1; the void variant takes the other slot. */
|
|
static int
|
|
nullable_ptr_tag(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
|
int i = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, i++) {
|
|
Type *pu = (p->type && p->type->kind == TY_NAMED)
|
|
? p->type->under : p->type;
|
|
if (pu && pu->kind == TY_PTR) return i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
node_istaggedarg(Node *n)
|
|
{
|
|
return n && tagged_arg_size(n->type) > 0;
|
|
}
|
|
|
|
static int
|
|
node_isstructarg(Node *n)
|
|
{
|
|
if (n == NULL) return 0;
|
|
int sz = struct_arg_size(n->type);
|
|
return sz > 0 && sz <= 16;
|
|
}
|
|
|
|
/* Pick the appropriate scalar SSE opcode (SS vs SD) for a node's
|
|
* float type. Untyped float defaults to SD. */
|
|
static int
|
|
op_for(Node *n, int sd_op, int ss_op)
|
|
{
|
|
return node_isf32(n) ? ss_op : sd_op;
|
|
}
|
|
|
|
/* Strict variant matcher. Returns 1 iff a value of `src` should be
|
|
* tagged as variant `vt` in a tagged-union dispatch:
|
|
* - untyped src: first variant whose type can hold it (type_assignable)
|
|
* - both NAMED: pointer-identical (same `type` declaration node)
|
|
* - one NAMED, the other not: no match (different nominal types)
|
|
* - otherwise: structural type_eq
|
|
* The pointer-identity rule is what keeps `(str | linerr)` distinguishable
|
|
* even though linerr unwraps to str. */
|
|
static int
|
|
cg_variant_match(Type *vt, Type *src)
|
|
{
|
|
if (vt == NULL || src == NULL) return 0;
|
|
if (type_isuntyped(src)) return type_assignable(vt, src);
|
|
if (vt->kind == TY_NAMED && src->kind == TY_NAMED) return vt == src;
|
|
if (vt->kind == TY_NAMED || src->kind == TY_NAMED) return 0;
|
|
return type_eq(vt, src);
|
|
}
|
|
|
|
/* cg_tagged_success_tag — index of the success variant in a tagged
|
|
* union. Mirrors check.c tagged_success_type: explicit-flag mode
|
|
* picks the first non-`!`-marked variant; legacy mode picks index 0. */
|
|
static int
|
|
cg_tagged_success_tag(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
|
int has_err = 0;
|
|
for (Tparam *p = t->params; p; p = p->next)
|
|
if (p->type && p->type->iserror) { has_err = 1; break; }
|
|
if (!has_err) return 0;
|
|
int idx = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, idx++)
|
|
if (p->type && !p->type->iserror) return idx;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
cg_variant_is_error(Type *t, int idx)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return 0;
|
|
int has_err = 0;
|
|
for (Tparam *p = t->params; p; p = p->next)
|
|
if (p->type && p->type->iserror) { has_err = 1; break; }
|
|
int i = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, i++) {
|
|
if (i == idx) {
|
|
if (has_err) return p->type && p->type->iserror;
|
|
/* legacy: index 0 is success, rest are errors */
|
|
return idx != 0;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Find the variant-tag index of `vt` inside the tagged-union type `t`.
|
|
* Returns -1 if `t` is not tagged or `vt` does not match a variant.
|
|
* Used by N_MATCH dispatch and by the let/assign/return tag synthesis. */
|
|
static int
|
|
cg_tag_for_variant(Type *t, Type *vt)
|
|
{
|
|
if (t == NULL || vt == NULL) return -1;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
if (t == NULL || t->kind != TY_TAGGED) return -1;
|
|
int idx = 0;
|
|
for (Tparam *p = t->params; p; p = p->next, idx++) {
|
|
if (cg_variant_match(p->type, vt)) return idx;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
type_istagged(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
if (t->kind == TY_NAMED) t = t->under;
|
|
return t && t->kind == TY_TAGGED;
|
|
}
|
|
|
|
/* FFI map: ww-side ident name → linker-side symbol name. Built from
|
|
* @symbol("real_name") attributes on fn declarations. */
|
|
typedef struct Ffi Ffi;
|
|
struct Ffi {
|
|
const char *ident;
|
|
const char *symbol;
|
|
Ffi *next;
|
|
};
|
|
static Ffi *ffi_map;
|
|
|
|
/* Def-as-string-literal map. `def NAME: str = "lit"` doesn't materialise
|
|
* as a real linker symbol; instead, references to NAME load the same
|
|
* (ptr, len) pair that the literal would. Avoids needing relocations
|
|
* inside DATA blocks for the ptr field of a str header. */
|
|
typedef struct Sdef Sdef;
|
|
struct Sdef {
|
|
const char *name;
|
|
const char *bytes;
|
|
u64 len;
|
|
Sdef *next;
|
|
};
|
|
static Sdef *sdefs;
|
|
|
|
/* 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. Each
|
|
* non-exported, non-FFI top-level decl is mangled to <module>.<name>
|
|
* at emission time so two modules can each privately define the same
|
|
* helper without colliding at link time. */
|
|
typedef struct Mod Mod;
|
|
struct Mod {
|
|
const char *name;
|
|
const char *module;
|
|
Mod *next;
|
|
};
|
|
static Mod *mod_map;
|
|
|
|
/* Top-level `let` map. Populated alongside mod_map; consulted by the
|
|
* N_IDENT store path and the &-of path to route reads/writes through
|
|
* a RIP-relative reference rather than dropping them as the (pre-
|
|
* writable-.data) compiler did. emit_lets emits a DATAW for each. */
|
|
typedef struct LetVar LetVar;
|
|
struct LetVar {
|
|
const char *name;
|
|
LetVar *next;
|
|
};
|
|
static LetVar *letvars;
|
|
|
|
/* Slot size for a top-level `let` of type t, or 0 if the type isn't
|
|
* supported as a writable global yet. Tagged unions are deferred.
|
|
* enums route through their storage type.
|
|
* Keep this tight — extending it requires the matching load/store
|
|
* code below. */
|
|
static int
|
|
let_emit_size(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
if (u == NULL) return 0;
|
|
switch (u->kind) {
|
|
case TY_BOOL: case TY_RUNE:
|
|
case TY_I8: case TY_I16: case TY_I32: case TY_I64:
|
|
case TY_U8: case TY_U16: case TY_U32: case TY_U64:
|
|
case TY_INT: case TY_UINT: case TY_UINTPTR:
|
|
case TY_PTR:
|
|
return 8;
|
|
case TY_F32:
|
|
return 4; /* MOVSS loads/stores 4B via LEAQ+indir. */
|
|
case TY_F64:
|
|
return 8; /* MOVSD loads/stores 8B via LEAQ+indir. */
|
|
case TY_STR:
|
|
return 16; /* {ptr, len}; literal-strlit init NYI. */
|
|
case TY_SLICE:
|
|
return 24; /* {ptr, len, cap}; no init only. */
|
|
case TY_STRUCT:
|
|
return (int)u->size; /* zero-init only; field reads/
|
|
* scalar-field writes only. */
|
|
case TY_ARRAY:
|
|
return (int)u->size; /* zero-init only; element
|
|
* loads/stores via cgindex. Mirror
|
|
* of selfhost letemitsize's
|
|
* N_TARRAY branch. */
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Is the unwrapped type a str? Used by the load/store paths so the
|
|
* (AX, BX) pair convention is preserved for str globals, mirroring
|
|
* what we already do for str locals. */
|
|
static int
|
|
let_isstr(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_STR;
|
|
}
|
|
|
|
/* Is the unwrapped type a slice? Slice globals flow as the (AX, BX,
|
|
* CX) triple — same as the local ABI. */
|
|
static int
|
|
let_isslice(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_SLICE;
|
|
}
|
|
|
|
/* Is the unwrapped type a struct? Struct globals only support field
|
|
* access (read + plain `=` write for scalar fields). Whole-struct
|
|
* by-value flow through expressions isn't wired. */
|
|
static int
|
|
let_isstruct(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_STRUCT;
|
|
}
|
|
|
|
/* Is the unwrapped type a fixed-length array? Array globals are
|
|
* zero-init DATAW slots; cgindex addresses them as LEAQ name(SB)
|
|
* and lets the element load/store run as usual. */
|
|
static int
|
|
let_isarray(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && u->kind == TY_ARRAY;
|
|
}
|
|
|
|
/* Is the unwrapped type a float (f32 or f64)? Float globals flow
|
|
* through X0 — load/store goes LEAQ name(SB),CX → MOVSS/MOVSD via the
|
|
* indirect, since the asm has no D_EXTERN form for SSE moves yet. */
|
|
static int
|
|
let_isfloat(Type *t)
|
|
{
|
|
if (t == NULL) return 0;
|
|
Type *u = (t->kind == TY_NAMED) ? t->under : t;
|
|
return u && (u->kind == TY_F32 || u->kind == TY_F64);
|
|
}
|
|
|
|
/* Returns the unwrapped Type — handy when we need to walk struct
|
|
* fields. NULL if t is NULL or unresolved. */
|
|
static Type *
|
|
type_unwrap(Type *t)
|
|
{
|
|
if (t == NULL) return NULL;
|
|
return (t->kind == TY_NAMED) ? t->under : t;
|
|
}
|
|
|
|
/* Element-effective type for indexing. For `*[N]T` we drill through
|
|
* the pointer to the underlying array so esz/esub reflect T, not the
|
|
* whole-array pointee. For everything else returns t unchanged. */
|
|
static Type *
|
|
idx_eff(Type *t)
|
|
{
|
|
if (t == NULL) return NULL;
|
|
Type *u = type_unwrap(t);
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *p = type_unwrap(u->sub);
|
|
if (p && p->kind == TY_ARRAY) return p;
|
|
}
|
|
return u;
|
|
}
|
|
|
|
static int
|
|
decl_has_ffisym(Node *d)
|
|
{
|
|
for (Node *a = d->attr; a; a = a->next) {
|
|
if (a->kind != N_ATTR) continue;
|
|
if (strcmp(a->str, "symbol") == 0) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
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 track = (d->kind == N_FNDECL) || (d->kind == N_TYPEDECL)
|
|
|| (d->kind == N_DEF) || (d->kind == N_LET);
|
|
if (!track) continue;
|
|
if (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. */
|
|
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;
|
|
}
|
|
|
|
/* Collect every top-level `let` whose declared type we can store
|
|
* in a single .data slot. Names not in this map fall through to
|
|
* the old "drop assignment" path; with a clear link-time
|
|
* undefined-symbol error on any read. */
|
|
static void
|
|
let_collect(Cg *c, Node *file)
|
|
{
|
|
letvars = NULL;
|
|
if (file == NULL) return;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_LET) continue;
|
|
if (d->str == NULL || d->str[0] == '\0') continue;
|
|
if (let_emit_size(d->type) == 0) continue;
|
|
LetVar *lv = amalloc(c->a, sizeof *lv);
|
|
lv->name = d->str;
|
|
lv->next = letvars;
|
|
letvars = lv;
|
|
}
|
|
}
|
|
|
|
static int
|
|
let_islet(const char *name)
|
|
{
|
|
if (name == NULL) return 0;
|
|
for (LetVar *lv = letvars; lv; lv = lv->next)
|
|
if (strcmp(lv->name, name) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* 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;
|
|
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;
|
|
}
|
|
|
|
/* 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));
|
|
}
|
|
|
|
void
|
|
cg_init(Cg *c, Arena *a)
|
|
{
|
|
memset(c, 0, sizeof *c);
|
|
c->a = a;
|
|
}
|
|
|
|
Prog *
|
|
newprog(Cg *c, int op)
|
|
{
|
|
Prog *p = amalloc(c->a, sizeof *p);
|
|
p->as = op;
|
|
return p;
|
|
}
|
|
|
|
void
|
|
emit(Cg *c, Prog *p)
|
|
{
|
|
if (c->head == NULL) c->head = p;
|
|
else c->tail->link = p;
|
|
c->tail = p;
|
|
}
|
|
|
|
static Adr
|
|
areg(int r)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = r;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
aimm(long long v)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_CONST;
|
|
a.offset = v;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
amem(int r, long long off)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_INDIR;
|
|
a.reg = r;
|
|
a.offset = off;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
asym(const char *s)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_EXTERN;
|
|
a.sym = s;
|
|
return a;
|
|
}
|
|
|
|
static Adr
|
|
abranch(const char *s)
|
|
{
|
|
Adr a = { 0 };
|
|
a.type = D_BRANCH;
|
|
a.sym = s;
|
|
return a;
|
|
}
|
|
|
|
static char *
|
|
mklabel(Cg *c, const char *prefix)
|
|
{
|
|
return aprintf(c->a, "%s_%s_%d", c->fnname ? c->fnname : "_", prefix,
|
|
c->labelseq++);
|
|
}
|
|
|
|
static void
|
|
ins2(Cg *c, int op, Adr from, Adr to)
|
|
{
|
|
Prog *p = newprog(c, op);
|
|
p->from = from;
|
|
p->to = to;
|
|
emit(c, p);
|
|
}
|
|
|
|
static void
|
|
ins1(Cg *c, int op, Adr to)
|
|
{
|
|
Prog *p = newprog(c, op);
|
|
p->to = to;
|
|
emit(c, p);
|
|
}
|
|
|
|
static void
|
|
ins0(Cg *c, int op)
|
|
{
|
|
emit(c, newprog(c, op));
|
|
}
|
|
|
|
static void
|
|
label(Cg *c, const char *s)
|
|
{
|
|
Prog *p = newprog(c, A_NOP);
|
|
p->label = s;
|
|
emit(c, p);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* per-fn local table: name → stack offset (positive = below FP) */
|
|
|
|
typedef struct Local Local;
|
|
struct Local {
|
|
const char *name;
|
|
int off; /* relative to BP; negative for locals */
|
|
Local *next;
|
|
};
|
|
|
|
static int
|
|
localoff(Cg *c, Local **head, const char *name, int size, int *frame)
|
|
{
|
|
for (Local *l = *head; l; l = l->next)
|
|
if (strcmp(l->name, name) == 0) return l->off;
|
|
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 — always push a fresh slot, never dedup by name. Used for
|
|
* match-arm bindings, where `case let e: T` must shadow any outer `e`
|
|
* with a slot sized to T — localoff's dedup would reuse the outer's
|
|
* (possibly smaller) slot and let multi-word writes overflow into the
|
|
* saved BP / return address. localfind walks from the head, so the
|
|
* fresh entry still wins inside the arm body. */
|
|
static int
|
|
local_alloc(Cg *c, Local **head, const char *name, int size, int *frame)
|
|
{
|
|
int al = 8;
|
|
*frame = (*frame + size + al - 1) & ~(al - 1);
|
|
int off = -*frame;
|
|
Local *l = amalloc(c->a, sizeof *l);
|
|
l->name = name;
|
|
l->off = off;
|
|
l->next = *head;
|
|
*head = l;
|
|
return off;
|
|
}
|
|
|
|
static int
|
|
localfind(Local *head, const char *name)
|
|
{
|
|
for (Local *l = head; l; l = l->next)
|
|
if (strcmp(l->name, name) == 0) return l->off;
|
|
return 0; /* 0 = not found (caller must verify) */
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* expressions: result lands in AX. Returns 1 on success. */
|
|
|
|
static void cgexpr(Cg*, Node*, Local*);
|
|
static void cgstmt(Cg*, Node*, Local**, int*);
|
|
static void cg_widen_tagged_push(Cg*, Local**, Type*, Node*, int);
|
|
static void cg_widen_tagged_store(Cg*, Local**, Type*, Node*, int, int, int);
|
|
static void cg_widen_tag_remap(Cg*, Type*, Type*, int);
|
|
|
|
static void
|
|
cgexpr_int(Cg *c, long long v)
|
|
{
|
|
ins2(c, A_MOVQ, aimm(v), areg(D_AX));
|
|
}
|
|
|
|
/* cg_widen_tag_remap — when widening from one tagged union to another,
|
|
* rewrite the source's variant tag at BP+slot_off+0 to use the dst
|
|
* union's variant indices. No-op when src and dst index orders coincide.
|
|
*
|
|
* Mirrors Hare's tagged-subset assignment: a value of type (A|B) flows
|
|
* into (A|B|C) by re-tagging the discriminator to the position the
|
|
* variant occupies in the wider union. Both must already match by
|
|
* cg_variant_match — the checker enforces that.
|
|
*
|
|
* Emits a CMPQ-chain switch over the source tag because w6a has no
|
|
* CMOVQ encoding. The chain is linear in nvariants; in practice tagged
|
|
* unions are small. */
|
|
static void
|
|
cg_widen_tag_remap(Cg *c, Type *du, Type *su, int slot_off)
|
|
{
|
|
if (du == NULL || du->kind != TY_TAGGED) return;
|
|
if (su == NULL || su->kind != TY_TAGGED) return;
|
|
int identity = 1, idx = 0;
|
|
for (Tparam *p = su->params; p; p = p->next, idx++) {
|
|
int di = cg_tag_for_variant(du, p->type);
|
|
if (di < 0) di = 0;
|
|
if (di != idx) { identity = 0; break; }
|
|
}
|
|
if (identity) return;
|
|
const char *done = mklabel(c, "remap_done");
|
|
ins2(c, A_MOVQ, amem(D_BP, slot_off + 0), areg(D_AX));
|
|
idx = 0;
|
|
for (Tparam *p = su->params; p; p = p->next, idx++) {
|
|
const char *next = mklabel(c, "remap_next");
|
|
int di = cg_tag_for_variant(du, p->type);
|
|
if (di < 0) di = 0;
|
|
ins2(c, A_CMPQ, aimm(idx), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(next));
|
|
ins2(c, A_MOVQ, aimm(di), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0));
|
|
ins1(c, A_JMP, abranch(done));
|
|
label(c, next);
|
|
}
|
|
label(c, done);
|
|
}
|
|
|
|
/* cg_widen_tagged_store — write the tagged-union slot bytes for `src`
|
|
* into base_reg+slot_off, sized to `sz` (8 for nullable fold, else
|
|
* 16/24+). Used by call-site widening (via cg_widen_tagged_push) and
|
|
* by the let/assign/return/struct-field-init paths.
|
|
*
|
|
* base_reg picks the addressing root for every write:
|
|
* - D_BP: function-frame slot. The original layout — callers pass
|
|
* a BP-relative slot_off and the function writes directly.
|
|
* - else (e.g. D_BX for a *struct field, D_CX for a top-level
|
|
* struct field): pointer-rooted dst. cgexpr inside this function
|
|
* trashes every GPR, so we can't carry base_reg across — instead
|
|
* we route every write through a fresh BP-rooted scratch slot,
|
|
* reload base_reg from a temp spill at the end, and word-copy
|
|
* scratch → (base_reg, slot_off). Caller is responsible for
|
|
* loading base_reg with the dst address before the call; the
|
|
* function preserves it across cgexpr via the spill.
|
|
*
|
|
* Branches by source shape (tagged_arg_size > 0 source counts as a
|
|
* tagged subset — possibly with different variant indices):
|
|
* - nullable: dst is folded (*T|void); store pointer at +0.
|
|
* - tagged ident: byte-copy slot words then remap tag at +0.
|
|
* - tagged expression: cgexpr leaves AX=tag, DX=val0, [CX=val1] —
|
|
* spill into slot then remap.
|
|
* - struct ident: zero-fill, byte-copy struct words to +8.
|
|
* - struct literal: zero-fill, store each field at slot+8+field_off.
|
|
* - str: cgexpr leaves AX=ptr, BX=len.
|
|
* - scalar: cgexpr leaves AX; store at +8 with zero pad. */
|
|
static void
|
|
cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
|
|
int base_reg, int slot_off, int sz)
|
|
{
|
|
/* For pointer-rooted dst, materialise into a BP-rooted scratch
|
|
* slot — body writes via `amem(D_BP, write_off + k)` — then copy
|
|
* out. Spill base_reg first so cgexpr can clobber freely. */
|
|
int via_outer = (base_reg != D_BP);
|
|
int base_spill = 0;
|
|
int write_off = slot_off;
|
|
if (via_outer) {
|
|
const char *spname = mklabel(c, "tagbase");
|
|
base_spill = local_alloc(c, locals_p, spname, 8, cg_frame);
|
|
ins2(c, A_MOVQ, areg(base_reg), amem(D_BP, base_spill));
|
|
const char *scname = mklabel(c, "tagscr");
|
|
write_off = local_alloc(c, locals_p, scname, sz, cg_frame);
|
|
/* Pre-zero so str/scalar branches (which leave high words
|
|
* untouched when sz exceeds the variant's footprint) still
|
|
* deliver a clean slot to the copy-out. */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst;
|
|
if (du == NULL || du->kind != TY_TAGGED) return;
|
|
if (du->nullable) {
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* `expr: TaggedAlias` where the cast's destination IS the union
|
|
* itself is a widening, not a re-interpret. cgexpr on the cast
|
|
* leaves the inner expression's register shape (str: AX=ptr,
|
|
* BX=len), not the tagged AX/DX/CX triple — so route through the
|
|
* concrete-variant branches below by peeling the cast. Casts to
|
|
* a concrete variant (`7: i32`) keep their type for proper tag
|
|
* lookup and fall through to the matching branch. */
|
|
if (src && src->kind == N_CAST && src->lhs) {
|
|
Type *castt = src->type;
|
|
Type *castu = (castt && castt->kind == TY_NAMED)
|
|
? castt->under : castt;
|
|
Type *innert = src->lhs->type;
|
|
Type *innu = (innert && innert->kind == TY_NAMED)
|
|
? innert->under : innert;
|
|
int cast_is_widen = (castu == du) ||
|
|
(castu && castu->kind == TY_TAGGED && type_eq(castt, dst));
|
|
int inner_is_tagged = innu && innu->kind == TY_TAGGED;
|
|
if (cast_is_widen && !inner_is_tagged) {
|
|
src = src->lhs;
|
|
}
|
|
}
|
|
Type *st = src ? src->type : NULL;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
/* Tagged → tagged subset: copy slot words then tag-remap. */
|
|
if (su && su->kind == TY_TAGGED) {
|
|
int ssz = (int)su->size;
|
|
if (src->kind == N_IDENT) {
|
|
int soff = localfind(*locals_p, src->str);
|
|
for (int k = 0; k < ssz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
} else {
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 0));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, write_off + 8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, write_off + 16));
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, write_off + 24));
|
|
}
|
|
if (ssz < sz) {
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = ssz; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
}
|
|
cg_widen_tag_remap(c, du, su, write_off);
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* Struct payload: zero the whole slot, then write fields/words
|
|
* at slot+8+ — keeping the tag word at slot+0 from the zero-fill,
|
|
* then patch it with the variant tag. */
|
|
if (su && su->kind == TY_STRUCT) {
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < sz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + k));
|
|
int tag = cg_tag_for_variant(du, st);
|
|
if (src->kind == N_IDENT) {
|
|
int soff = localfind(*locals_p, src->str);
|
|
int ssz = (int)su->size;
|
|
int k = 0;
|
|
while (k + 8 <= ssz) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + k));
|
|
k += 8;
|
|
}
|
|
if (k < ssz) {
|
|
/* Tail word: load with the right width to
|
|
* avoid stepping past the source slot. The
|
|
* zero-fill above means trailing slop is
|
|
* already clean. */
|
|
int tail = ssz - k;
|
|
int lop = (tail == 4) ? A_MOVL :
|
|
(tail == 1 ? A_MOVB : A_MOVQ);
|
|
ins2(c, lop,
|
|
amem(D_BP, soff + k), areg(D_AX));
|
|
ins2(c, lop, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + k));
|
|
}
|
|
} else if (src->kind == N_STRUCTLIT) {
|
|
for (Node *f = src->list; f; f = f->next) {
|
|
u64 foff = 0;
|
|
int fsz = 8;
|
|
Type *ftype = NULL;
|
|
for (Tfield *fl = su->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, f->str) == 0) {
|
|
foff = fl->offset;
|
|
fsz = (int)(fl->type ? fl->type->size : 8);
|
|
ftype = fl->type;
|
|
break;
|
|
}
|
|
}
|
|
cgexpr(c, f->lhs, *locals_p);
|
|
int sl_isf32 = 0;
|
|
if (fld_isfloat(ftype, &sl_isf32)) {
|
|
int mov = sl_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP, write_off + 8 + (int)foff));
|
|
continue;
|
|
}
|
|
Type *fu = (ftype && ftype->kind == TY_NAMED)
|
|
? ftype->under : ftype;
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, write_off + 8 + (int)foff + 8));
|
|
continue;
|
|
}
|
|
int op = A_MOVQ;
|
|
if (fsz == 1) op = A_MOVB;
|
|
else if (fsz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, write_off + 8 + (int)foff));
|
|
}
|
|
}
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* str payload: AX=ptr, BX=len from cgexpr. */
|
|
if (type_isstr(st) || (su && su->kind == TY_STR)) {
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 16));
|
|
int tag = cg_tag_for_variant(du, st);
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* Slice payload: cgexpr leaves (AX=ptr, BX=len, CX=cap). The
|
|
* slot layout is tag@+0, ptr@+8, len@+16, cap@+24 — requires the
|
|
* destination tagged-union slot be at least 32B. */
|
|
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 16));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, write_off + 24));
|
|
int tag = cg_tag_for_variant(du, st);
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
|
|
amem(D_BP, write_off + 0));
|
|
if (via_outer) goto copy_out;
|
|
return;
|
|
}
|
|
/* Scalar / pointer / etc. The high slot word (when sz > 16) is
|
|
* left untouched here — match dispatches on the tag word first
|
|
* and only the str branch reads slot+16, so leaving the pad
|
|
* uninitialised in let/assign matches the pre-refactor asm.
|
|
* cg_widen_tagged_push pre-zeroes the scratch slot before
|
|
* calling us, so the call-site push still sees clean pad. */
|
|
cgexpr(c, src, *locals_p);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
|
|
int tag = cg_tag_for_variant(du, st);
|
|
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, write_off + 0));
|
|
copy_out:
|
|
if (via_outer) {
|
|
/* cgexpr above clobbered base_reg — reload from spill, then
|
|
* word-copy scratch → caller's (base_reg, slot_off). */
|
|
ins2(c, A_MOVQ, amem(D_BP, base_spill), areg(base_reg));
|
|
for (int k = 0; k < sz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, write_off + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(base_reg, slot_off + k));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* cg_widen_tagged_push — call-site widening. For shapes where cgexpr
|
|
* leaves the value directly in registers (str: AX=ptr, BX=len; slice:
|
|
* AX=ptr, BX=len, CX=cap; scalar: AX), push from registers without a
|
|
* scratch slot. Struct payload and tagged-subset re-layout still
|
|
* route through a scratch slot. The direct-push form keeps wwstage's
|
|
* asm byte-identical to cstage on the byteindex / index family. */
|
|
static void
|
|
cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
|
|
{
|
|
Type *du = (dst && dst->kind == TY_NAMED) ? dst->under : dst;
|
|
if (du && du->nullable) {
|
|
/* Single 8B slot: just push the pointer/null. */
|
|
cgexpr(c, src, *locals_p);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
return;
|
|
}
|
|
Type *st = src ? src->type : NULL;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int src_is_struct = su && su->kind == TY_STRUCT;
|
|
int src_is_tagged = su && su->kind == TY_TAGGED;
|
|
if (!src_is_struct && !src_is_tagged) {
|
|
/* Direct-push fast path: str / slice / scalar / pointer. */
|
|
cgexpr(c, src, *locals_p);
|
|
int tag = cg_tag_for_variant(du, st);
|
|
if (tag < 0) tag = 0;
|
|
if (type_isstr(st) || (su && su->kind == TY_STR)) {
|
|
/* slot 24: [+0]=tag, [+8]=ptr, [+16]=len. Push len,
|
|
* ptr, tag (high→low so pop drains tag first). */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
|
|
return;
|
|
}
|
|
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
|
|
/* slot 32: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap. */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
|
|
return;
|
|
}
|
|
/* Scalar / pointer variant. Pad with zero high words when
|
|
* the slot has room for a wider variant. */
|
|
int nwords = sz / 8;
|
|
for (int k = nwords - 1; k >= 2; k--) {
|
|
ins2(c, A_XORQ, areg(D_DX), areg(D_DX));
|
|
ins1(c, A_PUSHQ, areg(D_DX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */
|
|
return;
|
|
}
|
|
const char *scr_name = mklabel(c, "argscr");
|
|
int scr = local_alloc(c, locals_p, scr_name, sz, cg_frame);
|
|
/* 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));
|
|
}
|
|
}
|
|
|
|
static void
|
|
cgexpr(Cg *c, Node *n, Local *locals)
|
|
{
|
|
if (n == NULL) {
|
|
cgexpr_int(c, 0);
|
|
return;
|
|
}
|
|
switch (n->kind) {
|
|
case N_INTLIT:
|
|
case N_RUNELIT:
|
|
cgexpr_int(c, (long long)n->uval);
|
|
break;
|
|
case N_FLOATLIT: {
|
|
union { double d; u64 u; } x;
|
|
x.d = n->fval;
|
|
ins2(c, A_MOVQ, aimm((long long)x.u), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVSD, amem(D_SP, 0), areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
break;
|
|
}
|
|
case N_STRLIT: {
|
|
/* result lives as the (ptr, len) pair: ptr in AX, len in BX.
|
|
* Call sites that pass a str arg pick these up directly. */
|
|
const char *lab = intern_strlit(c, n->str, n->strlen);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_BX));
|
|
break;
|
|
}
|
|
case N_TRUE: cgexpr_int(c, 1); break;
|
|
case N_FALSE:
|
|
case N_NIL:
|
|
case N_VOIDLIT: cgexpr_int(c, 0); break;
|
|
case N_IDENT: {
|
|
int off = localfind(locals, n->str);
|
|
if (off != 0) {
|
|
if (node_isfloat(n)) {
|
|
int op = op_for(n, A_MOVSD, A_MOVSS);
|
|
ins2(c, op, amem(D_BP, off), areg(D_X0));
|
|
} else if (node_isstr(n)) {
|
|
/* str values flow as (AX=ptr, BX=len) so they
|
|
* can be returned in AX:DX or pushed to the
|
|
* call-arg stack uniformly. */
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
|
|
} else if (node_isslice(n)) {
|
|
/* slice values flow as (AX=ptr, BX=len, CX=cap)
|
|
* — mirror the global-slice load so a slice
|
|
* local can be reassigned, returned, or copied
|
|
* with the same triple convention. */
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
|
|
} else {
|
|
ins2(c, localloadop(n->type),
|
|
amem(D_BP, off), areg(D_AX));
|
|
}
|
|
} else {
|
|
/* Non-local: function symbols load by address (LEAQ),
|
|
* str-typed `def`s expand to (ptr, len) of the literal,
|
|
* other globals (def constants) load by value (MOVQ). */
|
|
Type *t = n->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (u && u->kind == TY_FN) {
|
|
/* Take the address of a function. Apply
|
|
* @symbol resolution so taking the address
|
|
* of a body-less FFI binding yields the C
|
|
* symbol, not the ww-side ident. Hare emits
|
|
* the same `$symname` for both call and
|
|
* address-of via QBE; here we mirror that. */
|
|
ins2(c, A_LEAQ,
|
|
masym(c, n->str), areg(D_AX));
|
|
break;
|
|
}
|
|
for (Sdef *s = sdefs; s; s = s->next) {
|
|
if (strcmp(s->name, n->str) != 0) continue;
|
|
const char *lab = intern_strlit(c, s->bytes,
|
|
s->len);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ, aimm((long long)s->len),
|
|
areg(D_BX));
|
|
goto ident_done;
|
|
}
|
|
if (let_islet(n->str)
|
|
&& (let_isstr(n->type) || let_isslice(n->type))) {
|
|
/* Top-level str/slice global: load each half
|
|
* via its address (the asm has no `name+8(SB)`
|
|
* operand form). Slice has a third 8B (cap)
|
|
* — the address holder CX gets overwritten by
|
|
* the cap as the last step, after we no longer
|
|
* need it. */
|
|
int is_slice = let_isslice(n->type);
|
|
ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX));
|
|
if (is_slice)
|
|
ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX));
|
|
goto ident_done;
|
|
}
|
|
if (let_islet(n->str) && let_isfloat(n->type)) {
|
|
/* Top-level float global: same LEAQ-indirect
|
|
* shape as str/slice, since MOVSS/MOVSD have
|
|
* no D_EXTERN operand form in w6a. */
|
|
int op = type_isf32(n->type) ? A_MOVSS : A_MOVSD;
|
|
ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX));
|
|
ins2(c, op, amem(D_CX, 0), areg(D_X0));
|
|
goto ident_done;
|
|
}
|
|
/* Top-level lets can be the target of `*p` deref-stores
|
|
* (via `&letname: *iN`), so a signed-narrow scalar let
|
|
* needs MOVSXD/MOVSWQ/MOVSBQ on the read. Defs are
|
|
* read-only constants — their address cannot escape,
|
|
* so they keep the simpler MOVQ shape (and the wwstage
|
|
* defent registry, which doesn't track the declared
|
|
* type, agrees byte-for-byte). */
|
|
int gop = let_islet(n->str)
|
|
? localloadop(n->type) : A_MOVQ;
|
|
if (gop == A_MOVQ) {
|
|
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
|
|
} else {
|
|
/* w6a has no MOVSXD/MOVSWQ/MOVSBQ D_EXTERN
|
|
* source form, so route through a LEAQ scratch
|
|
* the same way top-level str/slice/float lets
|
|
* do. */
|
|
ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX));
|
|
ins2(c, gop, amem(D_CX, 0), areg(D_AX));
|
|
}
|
|
}
|
|
ident_done:
|
|
break;
|
|
}
|
|
case N_UN:
|
|
/* Address-of has its own evaluation strategy — we want the
|
|
* address of the operand, not its value. Special-case before
|
|
* the cgexpr pre-eval below so `&arr[i]` doesn't compile the
|
|
* value load and then discard it. */
|
|
if (n->op == TK_AMP) {
|
|
Node *opnd = n->lhs;
|
|
if (opnd && opnd->kind == N_IDENT) {
|
|
int off = localfind(locals, opnd->str);
|
|
if (off != 0) {
|
|
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
|
|
} else if (let_islet(opnd->str)) {
|
|
ins2(c, A_LEAQ, masym(c, opnd->str),
|
|
areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
if (opnd && opnd->kind == N_DOT) {
|
|
/* Address-of through a DOT chain. The early-exit
|
|
* above handled `&ident` and `&base[i]`; everything
|
|
* else was silently dropped. Three shapes converge
|
|
* here, all returning an 8B address (so no
|
|
* fldloadop dispatch — just LEAQ).
|
|
*
|
|
* 1. Value-struct fields, any depth (`&o.f`,
|
|
* `&o.i.a`, `&o.a.b.c`): walk the spine to a
|
|
* root ident, sum field offsets, emit LEAQ at
|
|
* base + sum. Mirror of the read at line 3722.
|
|
* 2. Slice/str pseudo-field tail (`&s.len`,
|
|
* `&b.buf.len`): folds into the spine walk
|
|
* with slice_delta 0/8/16.
|
|
* 3. Pointer-field (`&p.f` where p:*T): the spine
|
|
* walk aborts at the *T base; the fallback
|
|
* below loads p into AX and adds field_off.
|
|
*/
|
|
int amped = 0;
|
|
/* Spine walk — same shape as the read at 3722.
|
|
* Records (parent_struct, field_name) leaf-first,
|
|
* then iterates root-first to sum offsets. */
|
|
struct { Type *pu; const char *name; } steps[16];
|
|
int nsteps = 0;
|
|
Node *cur = opnd;
|
|
int abort = 0;
|
|
while (cur && cur->kind == N_DOT && cur->lhs) {
|
|
Type *pt = cur->lhs->type;
|
|
Type *pu = (pt && pt->kind == TY_NAMED)
|
|
? pt->under : pt;
|
|
if (!pu) { abort = 1; break; }
|
|
if (cur == opnd && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR)) {
|
|
/* leaf pseudo on slice/str header */
|
|
} else if (pu->kind != TY_STRUCT) {
|
|
abort = 1;
|
|
break;
|
|
}
|
|
if (nsteps >= 16) { abort = 1; break; }
|
|
steps[nsteps].pu = pu;
|
|
steps[nsteps].name = cur->str;
|
|
nsteps++;
|
|
cur = cur->lhs;
|
|
}
|
|
if (!abort && cur && cur->kind == N_IDENT
|
|
&& nsteps > 0) {
|
|
int total_off = 0;
|
|
int slice_delta = -1;
|
|
int ok = 1;
|
|
for (int i = nsteps - 1; i >= 0; i--) {
|
|
Type *pu = steps[i].pu;
|
|
if (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR) {
|
|
if (strcmp(steps[i].name, "ptr") == 0)
|
|
slice_delta = 0;
|
|
else if (strcmp(steps[i].name, "len") == 0)
|
|
slice_delta = 8;
|
|
else if (strcmp(steps[i].name, "cap") == 0)
|
|
slice_delta = 16;
|
|
else { ok = 0; break; }
|
|
} else {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = pu->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, steps[i].name) == 0)
|
|
{ f = fl; break; }
|
|
if (!f) { ok = 0; break; }
|
|
total_off += (int)f->offset;
|
|
}
|
|
}
|
|
if (ok) {
|
|
int extra = (slice_delta >= 0)
|
|
? slice_delta : 0;
|
|
int root_off = localfind(locals, cur->str);
|
|
if (root_off != 0) {
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP,
|
|
root_off + total_off + extra),
|
|
areg(D_AX));
|
|
amped = 1;
|
|
} else if (let_islet(cur->str)) {
|
|
/* Two-step global form mirrors the
|
|
* read path's `LEAQ name,CX → MOVQ
|
|
* disp(CX),AX`, swapping the MOVQ
|
|
* for LEAQ. */
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str), areg(D_CX));
|
|
ins2(c, A_LEAQ,
|
|
amem(D_CX, total_off + extra),
|
|
areg(D_AX));
|
|
amped = 1;
|
|
}
|
|
}
|
|
}
|
|
/* Pointer-field fallback for `&p.f` where p:*T —
|
|
* the spine walker aborts on the *T base. Load p
|
|
* into AX, then LEAQ field_off(AX),AX. Mirror of
|
|
* the read at line 4033. */
|
|
if (!amped && opnd->lhs
|
|
&& opnd->lhs->kind == N_IDENT) {
|
|
Type *bt = opnd->lhs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
if (bu && bu->kind == TY_PTR && bu->sub) {
|
|
Type *inner = bu->sub;
|
|
if (inner->kind == TY_NAMED)
|
|
inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
for (Tfield *f = inner->fields;
|
|
f; f = f->next) {
|
|
if (strcmp(f->name, opnd->str) != 0)
|
|
continue;
|
|
int off = localfind(locals,
|
|
opnd->lhs->str);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_AX));
|
|
ins2(c, A_LEAQ,
|
|
amem(D_AX, (int)f->offset),
|
|
areg(D_AX));
|
|
amped = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (amped) break;
|
|
/* Fall through to silent-drop fallback below. */
|
|
}
|
|
if (opnd && opnd->kind == N_INDEX) {
|
|
/* &base[i] = base + i*esz, no dereference. */
|
|
Node *base = opnd->lhs;
|
|
Node *idx = opnd->rhs;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
int esz = (bu && bu->sub)
|
|
? (int)bu->sub->size : 1;
|
|
if (bu && bu->kind == TY_STR) esz = 1;
|
|
cgexpr(c, idx, locals); /* idx → AX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (base && base->kind == N_IDENT) {
|
|
int boff = localfind(locals,
|
|
base->str);
|
|
int is_arr = bu &&
|
|
bu->kind == TY_ARRAY;
|
|
if (boff != 0) {
|
|
if (is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
}
|
|
} else if (let_islet(base->str)) {
|
|
if (is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
}
|
|
} else {
|
|
ins2(c, A_XORQ, areg(D_BX),
|
|
areg(D_BX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_BX),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
/* Complex base: eval to AX, swap into BX,
|
|
* then add the saved scaled idx. */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, base, locals);
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
}
|
|
/* Other shapes (& on a complex expr): silent drop,
|
|
* mirrors the pre-existing fallback. */
|
|
break;
|
|
}
|
|
cgexpr(c, n->lhs, locals);
|
|
switch (n->op) {
|
|
case TK_MINUS:
|
|
if (node_isfloat(n->lhs)) {
|
|
/* Float negate: X0 = 0 - X0. cgexpr left the
|
|
* value in X0; AX-only NEGQ wouldn't touch it. */
|
|
int isf32 = node_isf32(n->lhs);
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
int sub = isf32 ? A_SUBSS : A_SUBSD;
|
|
/* save orig X0 → stack */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
/* load 0.0 into X0 (zero bit pattern == 0.0) */
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
/* X1 = orig; X0 = X0 - X1 = -orig */
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, sub, areg(D_X1), areg(D_X0));
|
|
} else {
|
|
ins1(c, A_NEGQ, areg(D_AX));
|
|
}
|
|
break;
|
|
case TK_TILDE:
|
|
/* NOTQ inverts the whole 64-bit register. For unsigned
|
|
* narrow types we clamp to the type width so the
|
|
* upper bits are 0, matching how zero-extended loads
|
|
* leave the register. Signed narrow types already
|
|
* end up sign-extended (NOTQ on a sign-extended
|
|
* positive becomes sign-extended negative), so they
|
|
* need no fix-up. u32 uses MOVL r,r (zero-extends
|
|
* upper 32) because ANDQ $0xFFFFFFFF would sign-extend
|
|
* the imm32 to all-ones and act as a no-op. */
|
|
ins1(c, A_NOTQ, areg(D_AX));
|
|
if (n->type && type_isunsigned(n->type)
|
|
&& n->type->size < 8) {
|
|
if (n->type->size == 4) {
|
|
ins2(c, A_MOVL, areg(D_AX), areg(D_AX));
|
|
} else {
|
|
u64 mask = ((u64)1 << (n->type->size * 8)) - 1;
|
|
ins2(c, A_ANDQ, aimm((i64)mask), areg(D_AX));
|
|
}
|
|
}
|
|
break;
|
|
case TK_NOT: {
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
char *t = mklabel(c, "tt");
|
|
char *e = mklabel(c, "te");
|
|
ins1(c, A_JE, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
case TK_AMP:
|
|
/* Handled in the pre-cgexpr early-exit above. */
|
|
break;
|
|
case TK_STAR: /* deref */
|
|
ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX));
|
|
break;
|
|
default: break;
|
|
}
|
|
break;
|
|
case N_BIN: {
|
|
/* Short-circuit `&&` / `||`. Operands are bool (0/1); the
|
|
* type checker enforces it. Eval LHS into AX, branch over
|
|
* RHS on the short-circuit polarity, otherwise eval RHS
|
|
* into AX. The surviving AX is the result. Must precede
|
|
* any eager-eval path below — `if (p != nil && p.x > 0)`
|
|
* would segfault on a nil deref otherwise. */
|
|
if (n->op == TK_AND || n->op == TK_OR) {
|
|
char *end = mklabel(c, n->op == TK_AND ? "andend" : "orend");
|
|
int jshrt = (n->op == TK_AND) ? A_JE : A_JNE;
|
|
cgexpr(c, n->lhs, locals);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, jshrt, abranch(end));
|
|
cgexpr(c, n->rhs, locals);
|
|
label(c, end);
|
|
break;
|
|
}
|
|
/* str == str / str != str — delegate to rt_streq, which
|
|
* does the byte-by-byte compare. */
|
|
if ((n->op == TK_EQ || n->op == TK_NEQ) &&
|
|
node_isstr(n->lhs) && node_isstr(n->rhs)) {
|
|
/* Push rhs (len, then ptr top) */
|
|
if (n->rhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->rhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
} else {
|
|
cgexpr(c, n->rhs, locals); /* AX=ptr, BX=len */
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
/* Push lhs */
|
|
if (n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
} else {
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_DI));
|
|
ins1(c, A_POPQ, areg(D_SI));
|
|
ins1(c, A_POPQ, areg(D_DX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins1(c, A_CALL, asym("rt_streq"));
|
|
if (n->op == TK_NEQ)
|
|
ins2(c, A_XORQ, aimm(1), areg(D_AX));
|
|
break;
|
|
}
|
|
/* Float comparison: operands are float but the BIN node's
|
|
* type is bool, so node_isfloat(n) is false — we have to
|
|
* inspect n->lhs. UCOMISD/UCOMISS sets ZF/CF as if an
|
|
* unsigned compare, so the JA family is the right Jcc set
|
|
* regardless of how the operand types are signed. Plan 9's
|
|
* own w6c picks the same pattern (txt.c around AUCOMISD).
|
|
* NaN handling: UCOMI sets PF on unordered; we ignore it,
|
|
* which means NaN compares behave like Hare's default. */
|
|
if (n->lhs && node_isfloat(n->lhs) &&
|
|
(n->op == TK_EQ || n->op == TK_NEQ
|
|
|| n->op == TK_LT || n->op == TK_LE
|
|
|| n->op == TK_GT || n->op == TK_GE)) {
|
|
int isf32 = node_isf32(n->lhs);
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
int ucomi = isf32 ? A_UCOMISS : A_UCOMISD;
|
|
cgexpr(c, n->rhs, locals); /* rhs → X0 */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
cgexpr(c, n->lhs, locals); /* lhs → X0 */
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, ucomi, areg(D_X1), areg(D_X0));
|
|
int op = A_JE;
|
|
switch (n->op) {
|
|
case TK_EQ: op = A_JE; break;
|
|
case TK_NEQ: op = A_JNE; break;
|
|
case TK_LT: op = A_JB; break;
|
|
case TK_LE: op = A_JBE; break;
|
|
case TK_GT: op = A_JA; break;
|
|
case TK_GE: op = A_JAE; break;
|
|
default: break;
|
|
}
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, op, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
if (node_isfloat(n)) {
|
|
int isf32 = node_isf32(n);
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals); /* X0 */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
cgexpr(c, n->lhs, locals); /* X0 */
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X1));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
switch (n->op) {
|
|
case TK_PLUS:
|
|
ins2(c, isf32 ? A_ADDSS : A_ADDSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_MINUS:
|
|
ins2(c, isf32 ? A_SUBSS : A_SUBSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_STAR:
|
|
ins2(c, isf32 ? A_MULSS : A_MULSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
case TK_SLASH:
|
|
ins2(c, isf32 ? A_DIVSS : A_DIVSD, areg(D_X1), areg(D_X0));
|
|
break;
|
|
default: break;
|
|
}
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUS: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUS: ins2(c, A_SUBQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_STAR: ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_SLASH: {
|
|
/* Use DIV (unsigned) when either operand is an unsigned
|
|
* integer type — IDIV would sign-extend a u64 with high
|
|
* bit set into a negative i64 and produce wrong results
|
|
* (see strconv.u64tos with v = 1 << 63). */
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
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));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DX));
|
|
ins1(c, unsignd ? A_DIVQ : A_IDIVQ, areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case TK_AMP: ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_PIPE: ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_CARET: ins2(c, A_XORQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_LSHIFT: case TK_RSHIFT:
|
|
/* shift amount must be in CL */
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
ins2(c, n->op == TK_LSHIFT ? A_SHLQ : A_SHRQ,
|
|
areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_EQ: case TK_NEQ: case TK_LT: case TK_LE:
|
|
case TK_GT: case TK_GE: {
|
|
/* For ordered comparisons on unsigned operands we must
|
|
* use the JA/JAE/JB/JBE family — signed Jcc would treat
|
|
* a u64 with the high bit set as negative (e.g. the
|
|
* loop guard `n > 0` in strconv.u64tos with n=1<<63). */
|
|
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|
|
|| (n->rhs && type_isunsigned(n->rhs->type));
|
|
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
|
|
int op = A_JE;
|
|
switch (n->op) {
|
|
case TK_EQ: op = A_JE; break;
|
|
case TK_NEQ:op = A_JNE; break;
|
|
case TK_LT: op = unsignd ? A_JB : A_JL; break;
|
|
case TK_LE: op = unsignd ? A_JBE : A_JLE; break;
|
|
case TK_GT: op = unsignd ? A_JA : A_JG; break;
|
|
case TK_GE: op = unsignd ? A_JAE : A_JGE; break;
|
|
default: break;
|
|
}
|
|
char *t = mklabel(c, "ct");
|
|
char *e = mklabel(c, "ce");
|
|
ins1(c, op, abranch(t));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(e));
|
|
label(c, t);
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
label(c, e);
|
|
break;
|
|
}
|
|
/* TK_AND / TK_OR handled with short-circuit codegen at the
|
|
* top of N_BIN — they never reach this eager-eval switch. */
|
|
default: break;
|
|
}
|
|
break;
|
|
}
|
|
case N_ASSIGN: {
|
|
/* Discard lvalue `_ = expr;` — evaluate rhs for side effects,
|
|
* write nothing. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT &&
|
|
n->lhs->str && n->lhs->str[0] == '\0' &&
|
|
n->op == TK_ASSIGN) {
|
|
cgexpr(c, n->rhs, locals);
|
|
break;
|
|
}
|
|
/* p.x = v or p.x += v where p.x is a struct field
|
|
* (direct or via *struct). For compound ops we read-modify-
|
|
* write the field; for plain `=` we just write. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs &&
|
|
n->lhs->lhs->kind == N_IDENT) {
|
|
Node *base = n->lhs->lhs;
|
|
Type *bt = base->type;
|
|
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
int via_ptr = 0;
|
|
if (u && u->kind == TY_PTR) {
|
|
via_ptr = 1;
|
|
u = u->sub;
|
|
if (u && u->kind == TY_NAMED) u = u->under;
|
|
}
|
|
/* slice/str pseudo-field write (.ptr/.len/.cap) */
|
|
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)) {
|
|
const char *fld = n->lhs->str;
|
|
int delta = -1;
|
|
if (strcmp(fld, "ptr") == 0) delta = 0;
|
|
else if (strcmp(fld, "len") == 0) delta = 8;
|
|
else if (strcmp(fld, "cap") == 0) delta = 16;
|
|
if (delta < 0) goto after_dot_assign;
|
|
int boff = localfind(locals, base->str);
|
|
if (n->op != TK_ASSIGN) {
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_BX, delta), areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + delta), areg(D_BX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
if (n->op != TK_ASSIGN) {
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUSEQ:
|
|
/* old in BX, rhs in AX; want AX = old-rhs.
|
|
* SUBQ src,dst is dst -= src in Plan 9. */
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, delta));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + delta));
|
|
}
|
|
break;
|
|
}
|
|
after_dot_assign:
|
|
if (u && u->kind == TY_STRUCT) {
|
|
/* find field metadata */
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = u->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, n->lhs->str) == 0)
|
|
{ f = fl; break; }
|
|
if (f == NULL) break;
|
|
/* Tagged-union field: synthesise tag and store
|
|
* value bytes. Compound ops on tagged fields are
|
|
* not meaningful, so only plain `=` is wired.
|
|
* Three base shapes:
|
|
* - via_ptr: base is *struct local; address
|
|
* pre-loaded into BX. Buggy with a str
|
|
* variant since cgexpr will overwrite BX,
|
|
* but matches the existing pre-global
|
|
* behaviour.
|
|
* - is_global: struct global. LEAQ after
|
|
* cgexpr drops the slot address into CX
|
|
* without touching AX/BX, so str variants
|
|
* work cleanly.
|
|
* - else: struct local, BP-relative. */
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
/* Tagged-union field — full slot rewrite via the
|
|
* shared widener so every rhs shape (whole-tagged
|
|
* ident or expr with tag-remap, concrete-variant
|
|
* widening of str/slice/struct/scalar/void) lands
|
|
* the right tag + payload bytes. The pre-#26
|
|
* branch synthesised a single tag from
|
|
* cg_tag_for_variant and stored only AX at +8, so
|
|
* whole-tagged rhs (vt == fu, no concrete tag)
|
|
* silently wrote tag 0 and dropped trailing words.
|
|
* cg_widen_tagged_store handles every shape by
|
|
* branching on the source's resolved type. */
|
|
if (fu && fu->kind == TY_TAGGED
|
|
&& n->op == TK_ASSIGN) {
|
|
int boff = localfind(locals, base->str);
|
|
int is_global = (boff == 0 && !via_ptr
|
|
&& let_islet(base->str));
|
|
int foff = (int)f->offset;
|
|
int fsz = (int)fu->size;
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BX, foff, fsz);
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BX, foff, fsz);
|
|
} else {
|
|
cg_widen_tagged_store(c, &locals,
|
|
fu, n->rhs, D_BP,
|
|
boff + foff, fsz);
|
|
}
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int load_op = fldloadop(f->type, fsz);
|
|
int store_op = fldstoreop(f->type, fsz);
|
|
int boff = localfind(locals, base->str);
|
|
int is_global = (boff == 0 && !via_ptr
|
|
&& let_islet(base->str));
|
|
int foff = (int)f->offset;
|
|
/* str-typed field: rhs cgexpr leaves (AX=ptr, BX=len);
|
|
* store both halves at field+0 and field+8. The 8/16
|
|
* trailing-padding bytes are left untouched, which
|
|
* matches the let-init shape elsewhere in cgen. Only
|
|
* plain `=` is wired; compound on a str field is not
|
|
* meaningful. */
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STR) {
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 8));
|
|
}
|
|
break;
|
|
}
|
|
/* slice-typed field: rhs cgexpr leaves (AX=ptr,
|
|
* BX=len, CX=cap); store all three at
|
|
* field+0/+8/+16. Address scratch must dodge CX
|
|
* (holds cap), so via_ptr/is_global stage the
|
|
* struct base in DX. Without this branch the
|
|
* generic store_op below writes only AX, silently
|
|
* dropping .len and .cap. */
|
|
if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_SLICE) {
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, boff + foff + 16));
|
|
}
|
|
break;
|
|
}
|
|
/* struct-typed field, three rhs shapes:
|
|
* - N_IDENT: word-copy from the rhs slot directly
|
|
* onto the destination field. cgexpr cannot
|
|
* materialise a whole struct value in registers
|
|
* for an arbitrary local, so we read field words
|
|
* straight from the source slot.
|
|
* - N_CALL (added with #5): cgexpr leaves the value
|
|
* in AX/DX/CX per #4's cgreturn ABI; sized stores
|
|
* write only the declared field size — MOVQ for
|
|
* full 8B chunks plus MOVL/MOVW/MOVB tail. See
|
|
* the N_LET receive site for the ASYMMETRY
|
|
* rationale. cgreturn touches only AX/DX/CX, so
|
|
* BX stays free for the dst-addr load after the
|
|
* call.
|
|
* - N_STRUCTLIT (added with #5): field-by-field
|
|
* store; for via_ptr/is_global the dst base addr
|
|
* is reloaded into BX before each store so cgexpr
|
|
* can clobber AX/BX between fields. */
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& (int)str_fu->size <= 24
|
|
&& n->rhs && n->rhs->kind == N_CALL
|
|
&& (str_fu->size % 8 == 0
|
|
|| str_fu->size % 8 == 1
|
|
|| str_fu->size % 8 == 2
|
|
|| str_fu->size % 8 == 4)) {
|
|
int ssz = (int)str_fu->size;
|
|
cgexpr(c, n->rhs, locals);
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = ssz / 8;
|
|
int tail = ssz % 8;
|
|
int base_reg, base_disp;
|
|
if (via_ptr || is_global) {
|
|
if (via_ptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, boff),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
base_reg = D_BX;
|
|
base_disp = foff;
|
|
} else {
|
|
base_reg = D_BP;
|
|
base_disp = boff + foff;
|
|
}
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(base_reg,
|
|
base_disp + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW
|
|
: A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(base_reg,
|
|
base_disp + full * 8));
|
|
}
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
|
|
int ssz = (int)str_fu->size;
|
|
/* TK_ELLIPSIS autofill: zero-fill the field
|
|
* region first so unmentioned inner fields
|
|
* read as 0 (mirrors N_LET / N_IDENT-lhs
|
|
* structlit branches). */
|
|
if (n->rhs->op == TK_ELLIPSIS) {
|
|
ins2(c, A_XORQ, areg(D_AX),
|
|
areg(D_AX));
|
|
int zbase_reg, zbase_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));
|
|
zbase_reg = D_BX;
|
|
zbase_disp = foff;
|
|
} else {
|
|
zbase_reg = D_BP;
|
|
zbase_disp = boff + foff;
|
|
}
|
|
int zi = 0;
|
|
while (zi + 8 <= ssz) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(zbase_reg,
|
|
zbase_disp + zi));
|
|
zi += 8;
|
|
}
|
|
while (zi + 4 <= ssz) {
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(zbase_reg,
|
|
zbase_disp + zi));
|
|
zi += 4;
|
|
}
|
|
while (zi < ssz) {
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(zbase_reg,
|
|
zbase_disp + zi));
|
|
zi += 1;
|
|
}
|
|
}
|
|
for (Node *fn = n->rhs->list; fn;
|
|
fn = fn->next) {
|
|
u64 inner_foff = 0;
|
|
int fsz = 8;
|
|
Type *ft = NULL;
|
|
for (Tfield *fl = str_fu->fields;
|
|
fl; fl = fl->next) {
|
|
if (strcmp(fl->name, fn->str) == 0) {
|
|
inner_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) {
|
|
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));
|
|
cg_widen_tagged_store(c,
|
|
&locals, fu, fn->lhs,
|
|
D_BX,
|
|
foff + (int)inner_foff,
|
|
(int)fu->size);
|
|
} else {
|
|
cg_widen_tagged_store(c,
|
|
&locals, fu, fn->lhs,
|
|
D_BP,
|
|
boff + foff + (int)inner_foff,
|
|
(int)fu->size);
|
|
}
|
|
continue;
|
|
}
|
|
cgexpr(c, fn->lhs, locals);
|
|
int sl_isf32 = 0;
|
|
if (fld_isfloat(ft, &sl_isf32)) {
|
|
int mov = sl_isf32
|
|
? A_MOVSS : A_MOVSD;
|
|
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));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BX,
|
|
foff + (int)inner_foff));
|
|
} else {
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP,
|
|
boff + foff + (int)inner_foff));
|
|
}
|
|
continue;
|
|
}
|
|
int op = A_MOVQ;
|
|
if (fsz == 1) op = A_MOVB;
|
|
else if (fsz == 4) op = A_MOVL;
|
|
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));
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BX,
|
|
foff + (int)inner_foff));
|
|
} else {
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP,
|
|
boff + foff + (int)inner_foff));
|
|
}
|
|
}
|
|
(void)ssz;
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN && str_fu
|
|
&& str_fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_IDENT
|
|
&& localfind(locals, n->rhs->str) != 0) {
|
|
int soff = localfind(locals, n->rhs->str);
|
|
int ssz = (int)str_fu->size;
|
|
if (via_ptr)
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
else if (is_global)
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
int k = 0;
|
|
while (k + 8 <= ssz) {
|
|
ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX));
|
|
if (via_ptr || is_global)
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + k));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + k));
|
|
k += 8;
|
|
}
|
|
if (k < ssz) {
|
|
int tail = ssz - k;
|
|
int lop = (tail == 4) ? A_MOVL
|
|
: (tail == 1 ? A_MOVB : A_MOVQ);
|
|
ins2(c, lop, amem(D_BP, soff + k), areg(D_AX));
|
|
if (via_ptr || is_global)
|
|
ins2(c, lop, areg(D_AX), amem(D_BX, foff + k));
|
|
else
|
|
ins2(c, lop, areg(D_AX), amem(D_BP, boff + foff + k));
|
|
}
|
|
break;
|
|
}
|
|
/* compound: load current value into BX */
|
|
if (n->op != TK_ASSIGN) {
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, foff), areg(D_BX));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, foff), areg(D_BX));
|
|
} else {
|
|
ins2(c, load_op, amem(D_BP, boff + foff), areg(D_BX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
}
|
|
cgexpr(c, n->rhs, locals); /* AX = rhs */
|
|
if (n->op != TK_ASSIGN) {
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
|
|
case TK_MINUSEQ:
|
|
/* old in BX, rhs in AX; want AX=old-rhs */
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
|
|
break;
|
|
default: break; /* others rare */
|
|
}
|
|
}
|
|
/* f64/f32 field, plain `=`: cgexpr left the value in
|
|
* X0, not AX. Route the store via MOVSD/MOVSS.
|
|
* Compound ops on float fields aren't wired here —
|
|
* see CLAUDE.md #8 in examples/lisp; same in the
|
|
* structlit-init path below. */
|
|
int b_isf32 = 0;
|
|
if (n->op == TK_ASSIGN
|
|
&& fld_isfloat(f->type, &b_isf32)) {
|
|
int mov = b_isf32 ? A_MOVSS : A_MOVSD;
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, foff));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str), areg(D_BX));
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, foff));
|
|
} else {
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP, boff + foff));
|
|
}
|
|
break;
|
|
}
|
|
/* now store AX into target */
|
|
if (via_ptr) {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, foff));
|
|
} else if (is_global) {
|
|
ins2(c, A_LEAQ, masym(c, base->str), areg(D_BX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, foff));
|
|
} else {
|
|
ins2(c, store_op, areg(D_AX), amem(D_BP, boff + foff));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric
|
|
* write-side of the cgdot N_INDEX-lhs branch. Compute &arr[i]
|
|
* inline (LEAQ for `[N]Struct`, MOVQ-load for `[N]*Struct` /
|
|
* `[]Struct` / `*Struct`), deref once when the element is
|
|
* `*Struct`, then store rhs at `field.offset(addr)`. The
|
|
* chained-pointer-field branch below catches `[N]*Struct`
|
|
* writes via its `!= N_IDENT` guard, but `[N]Struct` value-arrays
|
|
* fall through and silently drop the store. Placed before the
|
|
* `!= N_IDENT` branch so both shapes share one path. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs
|
|
&& n->lhs->lhs->kind == N_INDEX) {
|
|
Node *idxbase = n->lhs->lhs->lhs;
|
|
Node *idx = n->lhs->lhs->rhs;
|
|
if (idxbase && idxbase->kind == N_IDENT && idx) {
|
|
Type *elemt = n->lhs->lhs->type;
|
|
Type *elemu = (elemt && elemt->kind == TY_NAMED)
|
|
? elemt->under : elemt;
|
|
Type *struct_t = NULL;
|
|
int viaptr = 0;
|
|
if (elemu && elemu->kind == TY_PTR) {
|
|
Type *inner = elemu->sub;
|
|
if (inner && inner->kind == TY_NAMED)
|
|
inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
struct_t = inner;
|
|
viaptr = 1;
|
|
}
|
|
} else if (elemu && elemu->kind == TY_STRUCT) {
|
|
struct_t = elemu;
|
|
}
|
|
if (struct_t) {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = struct_t->fields; fl;
|
|
fl = fl->next)
|
|
if (strcmp(fl->name,
|
|
n->lhs->str) == 0)
|
|
{ f = fl; break; }
|
|
Type *bt = idxbase->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
int is_arr = bu && bu->kind == TY_ARRAY;
|
|
int is_sl = bu && bu->kind == TY_SLICE;
|
|
int is_ptr = bu && bu->kind == TY_PTR;
|
|
int off = localfind(locals, idxbase->str);
|
|
if (f != NULL && (is_arr || is_sl || is_ptr)
|
|
&& off != 0) {
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int store_op = fldstoreop(ft, fsz);
|
|
int foff = (int)f->offset;
|
|
int esz = (int)elemt->size;
|
|
int h_isf32 = 0;
|
|
if (n->op == TK_ASSIGN
|
|
&& fld_isfloat(ft, &h_isf32)) {
|
|
int mov = h_isf32
|
|
? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_SUBQ, aimm(8),
|
|
areg(D_SP));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_SP, 0));
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ,
|
|
areg(D_AX),
|
|
areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0),
|
|
areg(D_BX));
|
|
ins2(c, mov,
|
|
amem(D_SP, 0),
|
|
areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8),
|
|
areg(D_SP));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN
|
|
&& fu && fu->kind == TY_STR) {
|
|
/* str rhs: AX=ptr, BX=len.
|
|
* Stash both, compute addr
|
|
* in CX so the pop pair
|
|
* restores AX/BX cleanly. */
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ,
|
|
areg(D_BX));
|
|
ins1(c, A_PUSHQ,
|
|
areg(D_AX));
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_CX));
|
|
ins2(c, A_ADDQ,
|
|
areg(D_AX),
|
|
areg(D_CX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_CX, 0),
|
|
areg(D_CX));
|
|
ins1(c, A_POPQ,
|
|
areg(D_AX));
|
|
ins1(c, A_POPQ,
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
areg(D_AX),
|
|
amem(D_CX, foff + 0));
|
|
ins2(c, A_MOVQ,
|
|
areg(D_BX),
|
|
amem(D_CX, foff + 8));
|
|
break;
|
|
}
|
|
if (n->op == TK_ASSIGN) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ,
|
|
areg(D_AX));
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ,
|
|
areg(D_AX),
|
|
areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0),
|
|
areg(D_BX));
|
|
ins1(c, A_POPQ,
|
|
areg(D_AX));
|
|
ins2(c, store_op,
|
|
areg(D_AX),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
/* compound: rhs→push; compute
|
|
* struct addr→BX (deref if *T);
|
|
* push addr; load old field→AX;
|
|
* pop addr→BX, rhs→CX; combine;
|
|
* store. Float/str compound
|
|
* not wired. */
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, idx, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ,
|
|
aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_AX),
|
|
areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0),
|
|
areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
int load_op = fldloadop(ft, fsz);
|
|
ins2(c, load_op,
|
|
amem(D_BX, foff),
|
|
areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ:
|
|
ins2(c, A_ADDQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_MINUSEQ:
|
|
ins2(c, A_SUBQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_STAREQ:
|
|
ins2(c, A_IMULQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_AMPEQ:
|
|
ins2(c, A_ANDQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_PIPEEQ:
|
|
ins2(c, A_ORQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
case TK_CARETEQ:
|
|
ins2(c, A_XORQ,
|
|
areg(D_CX),
|
|
areg(D_AX));
|
|
break;
|
|
default: break;
|
|
}
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* Chained `<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 *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
if (bu && bu->kind == TY_PTR && bu->sub) {
|
|
Type *inner = bu->sub;
|
|
if (inner->kind == TY_NAMED) inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = inner->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, n->lhs->str) == 0)
|
|
{ f = fl; break; }
|
|
if (f != NULL) {
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int store_op = fldstoreop(ft, fsz);
|
|
int foff = (int)f->offset;
|
|
if (n->op == TK_ASSIGN) {
|
|
int c_isf32 = 0;
|
|
if (fld_isfloat(ft, &c_isf32)) {
|
|
/* f64/f32 chained-store: cgexpr rhs
|
|
* left the value in X0. Spill to stack
|
|
* so cgexpr on the inner pointer can
|
|
* use AX, then reload into X0 and
|
|
* MOVSD/MOVSS into the slot. */
|
|
int mov = c_isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_SP, 0));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_BX));
|
|
ins2(c, mov, amem(D_SP, 0),
|
|
areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
if (fu && fu->kind == TY_STR) {
|
|
/* str rhs: (AX=ptr, BX=len). Stash
|
|
* both, then load the struct ptr
|
|
* into CX and write both halves. */
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_CX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_CX, foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_CX, foff + 8));
|
|
} else {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_BX, foff));
|
|
}
|
|
break;
|
|
}
|
|
/* compound op: AX=rhs → push; eval ptr → push;
|
|
* load old field → AX; pop ptr→BX, rhs→CX;
|
|
* combine; store. Float/str compound on a
|
|
* chained pointer-field is not wired. */
|
|
cgexpr(c, n->rhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
int load_op = fldloadop(ft, fsz);
|
|
ins2(c, load_op, amem(D_AX, foff),
|
|
areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ:
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_MINUSEQ:
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_STAREQ:
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_AMPEQ:
|
|
ins2(c, A_ANDQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_PIPEEQ:
|
|
ins2(c, A_ORQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
case TK_CARETEQ:
|
|
ins2(c, A_XORQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
default: break;
|
|
}
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_BX, foff));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* Chained `<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) {
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_CX, total_off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_CX, total_off + 8));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_BP, base_disp + total_off + 8));
|
|
}
|
|
break;
|
|
}
|
|
/* TY_STRUCT terminal in the chained-DOT walker:
|
|
* three rhs shapes — mirror of the single-dot
|
|
* branch.
|
|
* - N_IDENT: word-copy from rhs local slot.
|
|
* - N_CALL (added with #5): cgexpr → AX/DX/CX
|
|
* per #4's cgreturn ABI; sized stores per
|
|
* declared field size. cgreturn touches only
|
|
* AX/DX/CX so via_cx loads the dst addr into
|
|
* BX (not CX) after the call to keep CX as
|
|
* the third value word.
|
|
* - N_STRUCTLIT (added with #5): field-by-field
|
|
* store; via_cx reloads BX before each store
|
|
* so cgexpr can clobber AX/BX between fields.
|
|
*/
|
|
if (fu && fu->kind == TY_STRUCT
|
|
&& fsz <= 24
|
|
&& n->rhs && n->rhs->kind == N_CALL
|
|
&& (fsz % 8 == 0 || fsz % 8 == 1
|
|
|| fsz % 8 == 2 || fsz % 8 == 4)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = fsz / 8;
|
|
int tail = fsz % 8;
|
|
int base_reg, base_off;
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_BX));
|
|
base_reg = D_BX;
|
|
base_off = total_off;
|
|
} else {
|
|
base_reg = D_BP;
|
|
base_off = base_disp + total_off;
|
|
}
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(base_reg,
|
|
base_off + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW
|
|
: A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(base_reg,
|
|
base_off + full * 8));
|
|
}
|
|
break;
|
|
}
|
|
if (fu && fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_STRUCTLIT) {
|
|
int ssz = fsz;
|
|
if (n->rhs->op == TK_ELLIPSIS) {
|
|
ins2(c, A_XORQ, areg(D_AX),
|
|
areg(D_AX));
|
|
int zbase_reg, zbase_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));
|
|
zbase_reg = D_BX;
|
|
zbase_off = total_off;
|
|
} else {
|
|
zbase_reg = D_BP;
|
|
zbase_off = base_disp + total_off;
|
|
}
|
|
int zi = 0;
|
|
while (zi + 8 <= ssz) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(zbase_reg,
|
|
zbase_off + zi));
|
|
zi += 8;
|
|
}
|
|
while (zi + 4 <= ssz) {
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(zbase_reg,
|
|
zbase_off + zi));
|
|
zi += 4;
|
|
}
|
|
while (zi < ssz) {
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(zbase_reg,
|
|
zbase_off + zi));
|
|
zi += 1;
|
|
}
|
|
}
|
|
for (Node *fn = n->rhs->list; fn;
|
|
fn = fn->next) {
|
|
u64 inner_foff = 0;
|
|
int ifsz = 8;
|
|
Type *ift = NULL;
|
|
for (Tfield *fl = fu->fields; fl;
|
|
fl = fl->next) {
|
|
if (strcmp(fl->name,
|
|
fn->str) == 0) {
|
|
inner_foff = fl->offset;
|
|
ifsz = (int)(fl->type
|
|
? fl->type->size : 8);
|
|
ift = fl->type;
|
|
break;
|
|
}
|
|
}
|
|
Type *ifu = (ift
|
|
&& ift->kind == TY_NAMED)
|
|
? ift->under : ift;
|
|
if (ifu && ifu->kind == TY_TAGGED) {
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_BX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_BX));
|
|
cg_widen_tagged_store(c,
|
|
&locals, ifu, fn->lhs,
|
|
D_BX,
|
|
total_off + (int)inner_foff,
|
|
(int)ifu->size);
|
|
} else {
|
|
cg_widen_tagged_store(c,
|
|
&locals, ifu, fn->lhs,
|
|
D_BP,
|
|
base_disp + total_off + (int)inner_foff,
|
|
(int)ifu->size);
|
|
}
|
|
continue;
|
|
}
|
|
cgexpr(c, fn->lhs, locals);
|
|
int sf32 = 0;
|
|
if (fld_isfloat(ift, &sf32)) {
|
|
int mov = sf32
|
|
? A_MOVSS : A_MOVSD;
|
|
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));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BX,
|
|
total_off + (int)inner_foff));
|
|
} else {
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP,
|
|
base_disp + total_off + (int)inner_foff));
|
|
}
|
|
continue;
|
|
}
|
|
int op = A_MOVQ;
|
|
if (ifsz == 1) op = A_MOVB;
|
|
else if (ifsz == 4) op = A_MOVL;
|
|
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));
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BX,
|
|
total_off + (int)inner_foff));
|
|
} else {
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP,
|
|
base_disp + total_off + (int)inner_foff));
|
|
}
|
|
}
|
|
(void)ssz;
|
|
break;
|
|
}
|
|
if (fu && fu->kind == TY_STRUCT
|
|
&& n->rhs && n->rhs->kind == N_IDENT
|
|
&& localfind(locals, n->rhs->str) != 0) {
|
|
int soff = localfind(locals, n->rhs->str);
|
|
int ssz = fsz;
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_CX));
|
|
}
|
|
int k = 0;
|
|
while (k + 8 <= ssz) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
if (via_cx)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_CX, total_off + k));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off + k));
|
|
k += 8;
|
|
}
|
|
if (k < ssz) {
|
|
int tail = ssz - k;
|
|
int lop = (tail == 4) ? A_MOVL
|
|
: (tail == 1 ? A_MOVB : A_MOVQ);
|
|
ins2(c, lop,
|
|
amem(D_BP, soff + k),
|
|
areg(D_AX));
|
|
if (via_cx)
|
|
ins2(c, lop, areg(D_AX),
|
|
amem(D_CX, total_off + k));
|
|
else
|
|
ins2(c, lop, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off + k));
|
|
}
|
|
break;
|
|
}
|
|
int sf32 = 0;
|
|
if (fld_isfloat(leaf_type, &sf32)) {
|
|
int mov = sf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_CX));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_CX, total_off));
|
|
} else {
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BP, base_disp + total_off));
|
|
}
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
if (via_cx) {
|
|
if (ptr_root)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
else
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str),
|
|
areg(D_CX));
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_CX, total_off));
|
|
} else {
|
|
ins2(c, store_op, areg(D_AX),
|
|
amem(D_BP, base_disp + total_off));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* float assignment to a local or top-level global. Globals
|
|
* route through LEAQ+indirect (no D_EXTERN SSE in w6a).
|
|
* Compound (`acc += d` etc.) loads slot into X1, combines
|
|
* into X1 (Plan 9 syntax: OP src, dst), stores X1 back —
|
|
* w6a's ADDSD/SUBSD/MULSD/DIVSD are register-register only,
|
|
* so we can't use a direct mem-form like the integer ADDQ. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) {
|
|
cgexpr(c, n->rhs, locals); /* X0 */
|
|
int mvop = op_for(n, A_MOVSD, A_MOVSS);
|
|
int addop = op_for(n, A_ADDSD, A_ADDSS);
|
|
int subop = op_for(n, A_SUBSD, A_SUBSS);
|
|
int mulop = op_for(n, A_MULSD, A_MULSS);
|
|
int divop = op_for(n, A_DIVSD, A_DIVSS);
|
|
int off = localfind(locals, n->lhs->str);
|
|
int isglobal = (off == 0) && let_islet(n->lhs->str);
|
|
if (off == 0 && !isglobal) break;
|
|
if (n->op == TK_ASSIGN) {
|
|
if (off != 0) {
|
|
ins2(c, mvop, areg(D_X0), amem(D_BP, off));
|
|
} else {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, mvop, areg(D_X0), amem(D_CX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* Compound: X1 = load; X1 OP= X0; store X1. */
|
|
int fop = -1;
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: fop = addop; break;
|
|
case TK_MINUSEQ: fop = subop; break;
|
|
case TK_STAREQ: fop = mulop; break;
|
|
case TK_SLASHEQ: fop = divop; break;
|
|
default: break;
|
|
}
|
|
if (off != 0) {
|
|
if (fop < 0) {
|
|
/* Unsupported compound (e.g., %= on float):
|
|
* fall back to plain store of rhs. */
|
|
ins2(c, mvop, areg(D_X0),
|
|
amem(D_BP, off));
|
|
break;
|
|
}
|
|
ins2(c, mvop, amem(D_BP, off), areg(D_X1));
|
|
ins2(c, fop, areg(D_X0), areg(D_X1));
|
|
ins2(c, mvop, areg(D_X1), amem(D_BP, off));
|
|
} else {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
if (fop < 0) {
|
|
ins2(c, mvop, areg(D_X0),
|
|
amem(D_CX, 0));
|
|
break;
|
|
}
|
|
ins2(c, mvop, amem(D_CX, 0), areg(D_X1));
|
|
ins2(c, fop, areg(D_X0), areg(D_X1));
|
|
ins2(c, mvop, areg(D_X1), amem(D_CX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* arr[i] = v store. Base may be a simple ident (array/slice/
|
|
* ptr local) or a more complex expression like s.ptr where
|
|
* s: *[]u8. We compute the base address, scale the index by
|
|
* elem size, and store with the right size. */
|
|
if (n->lhs->kind == N_INDEX && n->lhs->lhs) {
|
|
Node *base = n->lhs->lhs;
|
|
Type *bt = base->type;
|
|
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
int is_arr = u && u->kind == TY_ARRAY;
|
|
int is_sl = u && u->kind == TY_SLICE;
|
|
int is_ptr = u && u->kind == TY_PTR;
|
|
/* For `*[N]T` drill through to the array so esz reflects
|
|
* T, not sizeof(array). Base load still uses u (MOVQ
|
|
* because is_ptr stays true). */
|
|
Type *eff = idx_eff(bt);
|
|
int esz = (eff && eff->sub) ? (int)eff->sub->size : 1;
|
|
int elem_is_str = eff && eff->sub && type_isstr(eff->sub);
|
|
Type *esub = eff ? eff->sub : NULL;
|
|
Type *esubu = (esub && esub->kind == TY_NAMED)
|
|
? esub->under : esub;
|
|
int elem_tagged = esubu && esubu->kind == TY_TAGGED;
|
|
/* Tagged-union element: route widening through a
|
|
* scratch slot, then copy slot bytes to &arr[i].
|
|
* Materialising into the scratch first lets us reuse
|
|
* the full cg_widen_tagged_store machinery — scalar /
|
|
* str / struct / subset payloads, tag remap, nullable
|
|
* fold — without duplicating it. The scratch lives in
|
|
* the function frame; no cleanup needed. */
|
|
if ((is_arr || is_sl || is_ptr) && elem_tagged) {
|
|
int ssz = esz;
|
|
const char *scrn = mklabel(c, "idxscr");
|
|
int scr = local_alloc(c, &locals, scrn, ssz,
|
|
cg_frame);
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
for (int k = 0; k < ssz; k += 8)
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
cg_widen_tagged_store(c, &locals, esubu,
|
|
n->rhs, D_BP, scr, ssz);
|
|
/* Compute &arr[i] → BX. */
|
|
cgexpr(c, n->lhs->rhs, locals);
|
|
if (ssz > 1) {
|
|
ins2(c, A_MOVQ, aimm(ssz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
if (base->kind == N_IDENT && is_arr) {
|
|
int boff = localfind(locals, base->str);
|
|
ins2(c, A_LEAQ, amem(D_BP, boff),
|
|
areg(D_BX));
|
|
} else if (base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, boff),
|
|
areg(D_BX));
|
|
} else {
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, base, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
/* Copy scratch slot → dest. */
|
|
for (int k = 0; k < ssz; k += 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BX, k));
|
|
}
|
|
break;
|
|
}
|
|
if (is_arr || is_sl || is_ptr) {
|
|
cgexpr(c, n->rhs, locals); /* AX (and BX if str) */
|
|
/* str element: also stash len so we can store both */
|
|
if (elem_is_str)
|
|
ins1(c, A_PUSHQ, areg(D_BX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->rhs, locals); /* idx → AX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */
|
|
/* base address → BX. Top-level array → LEAQ
|
|
* name(SB); top-level ptr → MOVQ name(SB); locals
|
|
* route off BP. */
|
|
if (base->kind == N_IDENT) {
|
|
int off = localfind(locals, base->str);
|
|
int isglobal = (off == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal && is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_BX));
|
|
} else if (is_arr) {
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off),
|
|
areg(D_BX));
|
|
}
|
|
} else {
|
|
cgexpr(c, base, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_AX)); /* scaled idx */
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX)); /* value (ptr if str) */
|
|
if (elem_is_str) {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
|
|
break;
|
|
}
|
|
int store_op = fldstoreop(esub, esz);
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
break;
|
|
}
|
|
}
|
|
/* Plain `r = expr;` where r is a tagged-union local.
|
|
* Delegates to cg_widen_tagged_store: covers nullable fold,
|
|
* tagged→tagged (with tag remap), struct payload (ident or
|
|
* literal), str payload, and scalar payload. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
|
|
&& n->lhs->type) {
|
|
Type *lt = n->lhs->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
if (lu && lu->kind == TY_TAGGED) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off == 0) break;
|
|
cg_widen_tagged_store(c, &locals, lu, n->rhs,
|
|
D_BP, off, (int)lu->size);
|
|
break;
|
|
}
|
|
}
|
|
/* Deref-target assignment `*p = v;`. The size of the store is
|
|
* determined by the type *p points at; the pointer expression
|
|
* is evaluated after the value so we don't need to spill BX. */
|
|
if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR
|
|
&& n->op == TK_ASSIGN) {
|
|
Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL;
|
|
Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
|
|
Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL;
|
|
if (vt && vt->kind == TY_NAMED) vt = vt->under;
|
|
/* `*p = v` for *f64 / *f32: cgexpr leaves the value in X0,
|
|
* not AX. Spill X0 to the stack, evaluate the pointer
|
|
* (clobbers AX/BX freely), then reload X0 and MOVSD/MOVSS
|
|
* through the pointer. */
|
|
int deref_isf32 = 0;
|
|
if (vt && fld_isfloat(vt, &deref_isf32)) {
|
|
int mov = deref_isf32 ? A_MOVSS : A_MOVSD;
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_SP, 0));
|
|
cgexpr(c, n->lhs->lhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, mov, amem(D_SP, 0), areg(D_X0));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
ins2(c, mov, areg(D_X0), amem(D_BX, 0));
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals); /* AX = value (BX too if str) */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (vt && vt->kind == TY_STR) ins1(c, A_PUSHQ, areg(D_BX));
|
|
cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
if (vt && vt->kind == TY_STR) {
|
|
ins1(c, A_POPQ, areg(D_CX)); /* len */
|
|
ins1(c, A_POPQ, areg(D_AX)); /* ptr */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
|
|
} else {
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
int sz = vt ? (int)vt->size : 8;
|
|
int store_op = fldstoreop(vt, sz);
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* `*p OP= v` — compound assign through a pointer deref. The
|
|
* plain-assign branch above only fires for TK_ASSIGN; without
|
|
* this, compound ops fall through the switch and emit nothing
|
|
* (silent no-op). Evaluate rhs → save, evaluate ptr → BX, load
|
|
* *BX (sized + extended), combine with rhs in CX, sized store
|
|
* back. Scalar deref targets only — float and aggregate deref
|
|
* compounds (rare) still fall through. */
|
|
if (n->lhs && n->lhs->kind == N_UN && n->lhs->op == TK_STAR
|
|
&& n->op != TK_ASSIGN) {
|
|
Type *pt = n->lhs->lhs ? n->lhs->lhs->type : NULL;
|
|
Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
|
|
Type *vt = (pu && pu->kind == TY_PTR) ? pu->sub : NULL;
|
|
if (vt && vt->kind == TY_NAMED) vt = vt->under;
|
|
int sz = vt ? (int)vt->size : 8;
|
|
int load_op = fldloadop(vt, sz);
|
|
int store_op = fldstoreop(vt, sz);
|
|
int handled = (sz == 1 || sz == 2 || sz == 4 || sz == 8);
|
|
if (handled) {
|
|
cgexpr(c, n->rhs, locals); /* AX = rhs */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs->lhs, locals); /* AX = ptr */
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_LSHIFTEQ: ins2(c, A_SHLQ, areg(D_CX), areg(D_AX)); break;
|
|
case TK_RSHIFTEQ: ins2(c, A_SHRQ, areg(D_CX), areg(D_AX)); break;
|
|
default:
|
|
/* TK_SLASHEQ / unknown: store rhs only,
|
|
* matching the IDENT-compound fallback. */
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_AX));
|
|
break;
|
|
}
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
break;
|
|
}
|
|
}
|
|
/* Plain `name = strexpr;` for a str-typed local. cgexpr leaves
|
|
* (AX=ptr, BX=len); store both halves at off+0 and off+8.
|
|
* Mirrors the let-init shape so reassignment doesn't truncate.
|
|
* Top-level str globals follow the same shape but go through
|
|
* &name(SB) since the asm has no `name+8(SB)` operand form. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
|
|
&& n->lhs->type) {
|
|
Type *lt = n->lhs->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
if (lu && lu->kind == TY_STR) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off != 0) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
break;
|
|
}
|
|
if (let_islet(n->lhs->str)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, 8));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
/* Slice reassignment: cgexpr produces (AX=ptr, BX=len,
|
|
* CX=cap). Store all three at off+0/+8/+16 (local) or
|
|
* via &name(SB) → DI scratch (global — CX holds the
|
|
* cap, so we need a different address register). */
|
|
if (lu && lu->kind == TY_SLICE) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off != 0) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
if (let_islet(n->lhs->str)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_DI));
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, 8));
|
|
ins2(c, A_MOVQ, areg(D_DI), amem(D_CX, 16));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
/* Struct local reassignment: `s = expr;` where s is
|
|
* a TY_STRUCT local of size <=24B. Two rhs shapes,
|
|
* mirroring cglet's N_STRUCTLIT and the call-result
|
|
* branch above:
|
|
* - N_STRUCTLIT: walk fields, store at off+foff
|
|
* directly (same shape as the let-init branch).
|
|
* - N_CALL: cgexpr → AX/DX/CX, sized stores per the
|
|
* same ASYMMETRY rules documented at the N_LET
|
|
* receive site (MOVQ for full 8B chunks plus
|
|
* MOVL/MOVW/MOVB tail). The struct-IDENT word-copy
|
|
* rhs shape (s = p) is left unwired; #5 is scoped to
|
|
* the receive side of #4's cgreturn (calls + literals).
|
|
* Sizes >24B and non-{0,1,2,4}-byte tails fall through
|
|
* to the existing scalar path. */
|
|
if (lu && lu->kind == TY_STRUCT
|
|
&& (int)lu->size <= 24) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off != 0) {
|
|
int sz = (int)lu->size;
|
|
if (n->rhs && n->rhs->kind == N_STRUCTLIT) {
|
|
/* TK_ELLIPSIS autofill: zero-fill the
|
|
* slot first so unmentioned fields read
|
|
* as 0 (mirrors cglet's structlit). */
|
|
if (n->rhs->op == TK_ELLIPSIS) {
|
|
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;
|
|
}
|
|
}
|
|
for (Node *f = n->rhs->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) {
|
|
cg_widen_tagged_store(c,
|
|
&locals, fu, f->lhs,
|
|
D_BP, off + (int)foff,
|
|
(int)fu->size);
|
|
continue;
|
|
}
|
|
cgexpr(c, f->lhs, locals);
|
|
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(D_BP,
|
|
off + (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(D_BP,
|
|
off + (int)foff));
|
|
}
|
|
break;
|
|
}
|
|
if (n->rhs && n->rhs->kind == N_CALL
|
|
&& (sz % 8 == 0 || sz % 8 == 1
|
|
|| sz % 8 == 2
|
|
|| sz % 8 == 4)) {
|
|
cgexpr(c, n->rhs, locals);
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = sz / 8;
|
|
int tail = sz % 8;
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(D_BP, off + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW
|
|
: A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(D_BP, off + full * 8));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off == 0) {
|
|
/* Top-level let target — RIP-relative store
|
|
* (or load→combine→store for compound). Names
|
|
* we don't recognise as scalar lets fall through
|
|
* to the existing drop behaviour, which produces
|
|
* a clean link-time undefined-symbol error if
|
|
* the binding was ever supposed to exist. */
|
|
if (!let_islet(n->lhs->str)) break;
|
|
cgexpr(c, n->rhs, locals);
|
|
if (n->op == TK_ASSIGN) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
masym(c, n->lhs->str));
|
|
break;
|
|
}
|
|
/* Compound: BX = load; combine with AX; store
|
|
* BX. The asm has no RIP-relative ADDQ/SUBQ
|
|
* mem-form, so we use the explicit load→
|
|
* combine→store sequence uniformly. Narrow
|
|
* lets go through LEAQ + indirect load with
|
|
* localloadop so a prior `*(&letname): *iN`
|
|
* deref-store doesn't leave stale upper bytes
|
|
* in the read. */
|
|
int glop = localloadop(n->lhs->type);
|
|
if (glop == A_MOVQ) {
|
|
ins2(c, A_MOVQ, masym(c, n->lhs->str),
|
|
areg(D_BX));
|
|
} else {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, glop, amem(D_CX, 0),
|
|
areg(D_BX));
|
|
}
|
|
int did_compound = 1;
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_LSHIFTEQ:
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, A_SHLQ, areg(D_CX), areg(D_BX));
|
|
break;
|
|
case TK_RSHIFTEQ:
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, A_SHRQ, areg(D_CX), areg(D_BX));
|
|
break;
|
|
default:
|
|
/* Unsupported compound op: store rhs
|
|
* directly. Mirrors the local path's
|
|
* fallback for TK_SLASHEQ etc. */
|
|
did_compound = 0;
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
masym(c, n->lhs->str));
|
|
break;
|
|
}
|
|
if (did_compound)
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
masym(c, n->lhs->str));
|
|
break;
|
|
}
|
|
cgexpr(c, n->rhs, locals);
|
|
if (n->op == TK_ASSIGN) {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
|
|
break;
|
|
}
|
|
/* Compound: load → combine into BX → store. The two
|
|
* direct mem-form combines (ADDQ/SUBQ) are kept for
|
|
* the simple cases; the rest go through the generic
|
|
* register form. Signed-narrow slots take the explicit
|
|
* load-combine-store path so the load can sign-extend
|
|
* through localloadop — ADDQ/SUBQ on amem would read
|
|
* the raw 8B, which is wrong when the slot was last
|
|
* written by a 4B deref-store. */
|
|
int lop = localloadop(n->lhs->type);
|
|
if (lop == A_MOVQ && n->op == TK_PLUSEQ) {
|
|
ins2(c, A_ADDQ, areg(D_AX), amem(D_BP, off));
|
|
break;
|
|
}
|
|
if (lop == A_MOVQ && n->op == TK_MINUSEQ) {
|
|
ins2(c, A_SUBQ, areg(D_AX), amem(D_BP, off));
|
|
break;
|
|
}
|
|
ins2(c, lop, amem(D_BP, off), areg(D_BX));
|
|
switch (n->op) {
|
|
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_MINUSEQ: ins2(c, A_SUBQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_AX), areg(D_BX)); break;
|
|
case TK_LSHIFTEQ:
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, A_SHLQ, areg(D_CX), areg(D_BX));
|
|
break;
|
|
case TK_RSHIFTEQ:
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
|
|
ins2(c, A_SHRQ, areg(D_CX), areg(D_BX));
|
|
break;
|
|
case TK_SLASHEQ:
|
|
/* BX = BX / AX. IDIV uses DX:AX/RAX. Move
|
|
* BX→AX first, sign-extend via CQO would be
|
|
* cleanest; skip for now and fall back to
|
|
* MOVQ to avoid emitting wrong code. */
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off));
|
|
goto skip_assign_store;
|
|
default:
|
|
/* unknown: just store rhs (legacy fallback) */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
|
|
goto skip_assign_store;
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off));
|
|
skip_assign_store: ;
|
|
}
|
|
break;
|
|
}
|
|
case N_CALL: {
|
|
/* abort([msg]) — call rt_abort. Empty msg becomes (NULL, 0).
|
|
* Only fires when the checker tagged the callee as a builtin
|
|
* (lhs->type == ty_err); a user-declared `abort` in scope is
|
|
* resolved through the regular call path. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
n->lhs->type == ty_err &&
|
|
strcmp(n->lhs->str, "abort") == 0) {
|
|
if (n->list) {
|
|
cgexpr(c, n->list, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
|
|
} else {
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
|
|
}
|
|
ins1(c, A_CALL, asym("rt_abort"));
|
|
break;
|
|
}
|
|
/* assert(cond[, msg]) — if !cond, call rt_abort. Compiles to:
|
|
* CMPQ $0, AX
|
|
* JNE skip
|
|
* <abort body>
|
|
* skip: */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
n->lhs->type == ty_err &&
|
|
strcmp(n->lhs->str, "assert") == 0 && n->list) {
|
|
cgexpr(c, n->list, locals);
|
|
char *skip = mklabel(c, "as");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(skip));
|
|
Node *msg = n->list->next;
|
|
if (msg) {
|
|
cgexpr(c, msg, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_SI));
|
|
} else {
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_SI));
|
|
}
|
|
ins1(c, A_CALL, asym("rt_abort"));
|
|
label(c, skip);
|
|
break;
|
|
}
|
|
/* Hare-style builtins: len(x) and append(s, v). */
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
strcmp(n->lhs->str, "len") == 0 && n->list) {
|
|
Node *a = n->list;
|
|
Type *at = a->type;
|
|
Type *u = (at && at->kind == TY_NAMED) ? at->under : at;
|
|
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
|
|
&& a->kind == N_IDENT) {
|
|
int off = localfind(locals, a->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
} else if (u && u->kind == TY_ARRAY) {
|
|
ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX));
|
|
} else {
|
|
/* fall back: load via .len pseudo-field */
|
|
cgexpr(c, a, locals);
|
|
}
|
|
break;
|
|
}
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
strcmp(n->lhs->str, "free") == 0 && n->list &&
|
|
n->list->next == NULL) {
|
|
Node *p = n->list;
|
|
Type *pt = p->type;
|
|
Type *u = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
int sz = (int)u->sub->size;
|
|
if (sz == 0) sz = 8;
|
|
cgexpr(c, p, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(sz), areg(D_SI));
|
|
ins1(c, A_CALL, asym(ffi_resolve("free")));
|
|
} else if (u && u->kind == TY_SLICE
|
|
&& p->kind == N_IDENT) {
|
|
int off = localfind(locals, p->str);
|
|
int esz = (int)(u->sub ? u->sub->size : 1);
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX));
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
|
|
ins2(c, A_IMULQ, areg(D_BX), areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_SI));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_DI));
|
|
ins1(c, A_CALL, asym(ffi_resolve("free")));
|
|
}
|
|
break;
|
|
}
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
strcmp(n->lhs->str, "alloc") == 0 && n->list) {
|
|
/* alloc(value): heap-init a fresh *T with the value's
|
|
* bytes. Size comes from the value's static type. */
|
|
Node *v = n->list;
|
|
Type *t = v->type;
|
|
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
Type *def = type_default(t);
|
|
int sz = def ? (int)def->size : 8;
|
|
if (sz == 0) sz = 8;
|
|
/* call os.alloc(sz) */
|
|
ins2(c, A_MOVQ, aimm(sz), areg(D_DI));
|
|
ins1(c, A_CALL, asym(ffi_resolve("alloc")));
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* save ptr */
|
|
if (v->kind == N_STRUCTLIT && u && u->kind == TY_STRUCT) {
|
|
for (Node *f = v->list; f; f = f->next) {
|
|
u64 foff = 0;
|
|
int fsz = 8;
|
|
Type *ftype = NULL;
|
|
for (Tfield *fl = u->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, f->str) == 0) {
|
|
foff = fl->offset;
|
|
fsz = (int)(fl->type ? fl->type->size : 8);
|
|
ftype = fl->type;
|
|
break;
|
|
}
|
|
}
|
|
cgexpr(c, f->lhs, locals); /* AX or (AX,BX) or X0 */
|
|
int f_isf32 = 0;
|
|
if (fld_isfloat(ftype, &f_isf32)) {
|
|
int mov = f_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
|
|
ins2(c, mov, areg(D_X0),
|
|
amem(D_BX, (int)foff));
|
|
continue;
|
|
}
|
|
/* str-typed field: cgexpr leaves (AX=ptr, BX=len).
|
|
* Route the heap base through CX so both halves
|
|
* survive — using BX would clobber len. */
|
|
Type *fu = (ftype && ftype->kind == TY_NAMED)
|
|
? ftype->under : ftype;
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_CX, (int)foff + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
amem(D_CX, (int)foff + 8));
|
|
continue;
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
|
|
int op = A_MOVQ;
|
|
if (fsz == 1) op = A_MOVB;
|
|
else if (fsz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX), amem(D_BX, (int)foff));
|
|
}
|
|
} else {
|
|
cgexpr(c, v, locals); /* AX = value */
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
|
|
int op = A_MOVQ;
|
|
if (sz == 1) op = A_MOVB;
|
|
else if (sz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX), amem(D_BX, 0));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_AX)); /* return the ptr */
|
|
break;
|
|
}
|
|
if (n->lhs && n->lhs->kind == N_IDENT && n->lhs->str &&
|
|
strcmp(n->lhs->str, "append") == 0 && n->list &&
|
|
n->list->next) {
|
|
/* append(s, v) lowering — Hare's rt::ensure model.
|
|
* ; AX = value
|
|
* ; PUSHQ AX ; save
|
|
* ; ADDQ $1, sn_off+8(BP) ; s.len += 1
|
|
* ; LEAQ sn_off(BP), DI ; arg1 = &s
|
|
* ; MOVQ esz, SI ; arg2 = membsz
|
|
* ; CALL rt_ensure(SB) ; may realloc s.ptr
|
|
* ; MOVQ sn_off+8(BP), CX ; CX = new len
|
|
* ; SUBQ $1, CX ; slot index
|
|
* ; [IMULQ esz, CX] ; byte offset (esz>1)
|
|
* ; MOVQ sn_off(BP), BX ; reread s.ptr
|
|
* ; ADDQ CX, BX ; BX = target
|
|
* ; POPQ AX ; v
|
|
* ; MOV* AX, (BX) ; store (MOVB / MOVQ)
|
|
*
|
|
* Spread form `append(s, items...)` runs this same body
|
|
* in a counted loop over items. */
|
|
Node *sn = n->list;
|
|
Type *st = sn->type;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int esz = (su && su->sub) ? (int)su->sub->size : 1;
|
|
Type *esub = su ? su->sub : NULL;
|
|
int sn_off = (sn->kind == N_IDENT)
|
|
? localfind(locals, sn->str) : 0;
|
|
int store_op = fldstoreop(esub, esz);
|
|
for (Node *vn = sn->next; vn; vn = vn->next) {
|
|
if (vn->kind == N_SPREAD &&
|
|
vn->lhs && vn->lhs->kind == N_IDENT) {
|
|
int it_off = localfind(locals, vn->lhs->str);
|
|
int load_op = fldloadop(esub, esz);
|
|
/* push counter (i) on stack */
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, A_MOVQ, aimm(0), amem(D_SP, 0));
|
|
char *ll = mklabel(c, "spr_l");
|
|
char *le = mklabel(c, "spr_e");
|
|
label(c, ll);
|
|
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_BP, it_off + 8), areg(D_DX));
|
|
ins2(c, A_CMPQ, areg(D_DX), areg(D_CX));
|
|
ins1(c, A_JGE, abranch(le));
|
|
/* AX = items.ptr[i] */
|
|
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
|
|
/* ensure + store one element */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
|
|
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
|
|
ins1(c, A_CALL, masym(c, "rt_ensure"));
|
|
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
|
|
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
/* loop tail */
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_SP, 0));
|
|
ins1(c, A_JMP, abranch(ll));
|
|
label(c, le);
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
continue;
|
|
}
|
|
cgexpr(c, vn, locals); /* val → AX */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
|
|
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
|
|
ins1(c, A_CALL, masym(c, "rt_ensure"));
|
|
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
|
|
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
|
|
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, store_op, areg(D_AX), amem(D_BX, 0));
|
|
}
|
|
break;
|
|
}
|
|
/* up to 6 integer + 8 float args via SysV registers.
|
|
* str args occupy two integer eightbytes (ptr, len). The
|
|
* arg-buffer cap accommodates Hare-style variadic gather
|
|
* (`fmt.println(a, b, c, ...)`) where N args of element
|
|
* type T fold into a single []T slice slot below. */
|
|
int argcount = 0;
|
|
Node *args[64] = {0};
|
|
for (Node *a = n->list; a; a = a->next)
|
|
if (argcount < 64) args[argcount++] = a;
|
|
/* Resolve callee fn-type so we can match each arg against
|
|
* its declared parameter type — needed to detect implicit
|
|
* widening of a concrete variant into a tagged-union slot. */
|
|
Type *callee_t = n->lhs ? n->lhs->type : NULL;
|
|
Type *cu = (callee_t && callee_t->kind == TY_NAMED) ?
|
|
callee_t->under : callee_t;
|
|
Tparam *callee_params = (cu && cu->kind == TY_FN) ?
|
|
cu->params : NULL;
|
|
/* Hare-style variadic last param: gather N tail args into a
|
|
* stack-resident []T or forward an `xs...` spread, then
|
|
* splice in a single slice arg so the downstream widen/push/
|
|
* pop machinery sees one 24B slice slot for the variadic.
|
|
*
|
|
* Forward shape: `f(... , xs...)` becomes `f(... , xs)`.
|
|
* Gather shape: `f(... , e0, e1, eN)` materialises e0..eN
|
|
* into a frame-resident `[N]T` (widening each element when T
|
|
* is a tagged union), writes a 24B slice descriptor
|
|
* {ptr=&data, len=N, cap=N}, and replaces the tail args with
|
|
* an N_IDENT pointing at the descriptor. Empty form
|
|
* (`f(...)` with no variadic args) writes {0, 0, 0}. */
|
|
{
|
|
int nfixed = 0;
|
|
Tparam *var_p = NULL;
|
|
for (Tparam *p = callee_params; p; p = p->next) {
|
|
if (p->variadic) { var_p = p; break; }
|
|
nfixed++;
|
|
}
|
|
if (var_p != NULL) {
|
|
int nvar = argcount - nfixed;
|
|
if (nvar < 0) nvar = 0;
|
|
int forwarding = (nvar == 1 && args[nfixed] &&
|
|
args[nfixed]->kind == N_SPREAD);
|
|
if (forwarding) {
|
|
args[nfixed] = args[nfixed]->lhs;
|
|
argcount = nfixed + 1;
|
|
} else {
|
|
Type *vst = var_p->type;
|
|
Type *vsu = (vst && vst->kind == TY_NAMED)
|
|
? vst->under : vst;
|
|
Type *velem = (vsu && vsu->kind == TY_SLICE)
|
|
? vsu->sub : NULL;
|
|
int esz = (velem && velem->size)
|
|
? (int)velem->size : 8;
|
|
const char *slname = mklabel(c, "vararg_sl");
|
|
int sloff = localoff(c, &locals,
|
|
slname, 24, cg_frame);
|
|
int doff = 0;
|
|
if (nvar > 0) {
|
|
const char *dname = mklabel(c, "vararg_d");
|
|
doff = localoff(c, &locals,
|
|
dname, nvar * esz, cg_frame);
|
|
int v_is_tagged = velem &&
|
|
tagged_arg_size(velem) > 0;
|
|
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);
|
|
int op = A_MOVQ;
|
|
if (esz == 1) op = A_MOVB;
|
|
else if (esz == 4) op = A_MOVL;
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, slot));
|
|
}
|
|
}
|
|
if (nvar > 0)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, doff),
|
|
areg(D_AX));
|
|
else
|
|
ins2(c, A_XORQ, areg(D_AX),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sloff + 0));
|
|
ins2(c, A_MOVQ, aimm(nvar),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sloff + 8));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sloff + 16));
|
|
Node *sn = newnode(c->a, N_IDENT, n->pos);
|
|
sn->str = slname;
|
|
sn->strlen = 0;
|
|
sn->type = vst;
|
|
args[nfixed] = sn;
|
|
argcount = nfixed + 1;
|
|
}
|
|
}
|
|
}
|
|
/* widen[i]: param is tagged and arg needs re-layout.
|
|
* - arg is a concrete variant (str/struct/scalar) — wrap
|
|
* in the param's slot shape.
|
|
* - arg is itself a tagged union of a subset/different
|
|
* variant set — copy the slot words and remap the tag.
|
|
* Identical types pass through unchanged. */
|
|
int widen[64] = {0};
|
|
int widen_sz[64] = {0};
|
|
Type *widen_param[64] = {0};
|
|
{
|
|
Tparam *p = callee_params;
|
|
for (int i = 0; i < argcount; i++) {
|
|
if (p == NULL) break;
|
|
Type *at = args[i] ? args[i]->type : NULL;
|
|
int psz = tagged_arg_size(p->type);
|
|
if (psz > 0) {
|
|
Type *pu = (p->type && p->type->kind == TY_NAMED)
|
|
? p->type->under : p->type;
|
|
Type *au = (at && at->kind == TY_NAMED)
|
|
? at->under : at;
|
|
int same = (pu == au) || type_eq(p->type, at);
|
|
if (!same) {
|
|
widen[i] = 1;
|
|
widen_sz[i] = psz;
|
|
widen_param[i] = p->type;
|
|
}
|
|
}
|
|
p = p->next;
|
|
}
|
|
}
|
|
/* eval right-to-left, push to stack. Each N_IDENT fast-path
|
|
* is guarded by !widen[i] so the tagged-union widening (which
|
|
* needs to synthesise tag + payload + pad) takes precedence
|
|
* over the verbatim slice/struct/tagged-ident loads below. */
|
|
for (int i = argcount - 1; i >= 0; i--) {
|
|
if (!widen[i] && node_isslice(args[i]) && args[i]->kind == N_IDENT) {
|
|
int off = localfind(locals, args[i]->str);
|
|
/* push cap, len, ptr (top) so pops give ptr,len,cap */
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
continue;
|
|
}
|
|
if (!widen[i] && args[i]->kind == N_SLICE) {
|
|
Node *base = args[i]->lhs;
|
|
Node *lo = args[i]->rhs;
|
|
Node *hi = args[i]->cond;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ?
|
|
bt->under : bt;
|
|
/* base addr → push */
|
|
if (base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal && bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ,
|
|
masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr(c, base, locals);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
/* hi (default base length) → push */
|
|
if (hi) cgexpr(c, hi, locals);
|
|
else if (bu && bu->kind == TY_ARRAY)
|
|
cgexpr_int(c, (long long)bu->alen);
|
|
else if (base->kind == N_IDENT && bu &&
|
|
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) &&
|
|
let_islet(base->str);
|
|
if (isglobal) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, base->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_CX, 8), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
/* lo (default 0) → AX */
|
|
if (lo) cgexpr(c, lo, locals);
|
|
else cgexpr_int(c, 0);
|
|
ins1(c, A_POPQ, areg(D_BX)); /* hi */
|
|
ins1(c, A_POPQ, areg(D_CX)); /* base */
|
|
/* len = hi - lo (DX) */
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_DX));
|
|
ins2(c, A_SUBQ, areg(D_AX), areg(D_DX));
|
|
/* ptr = base + lo */
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_CX));
|
|
/* push cap, len, ptr (top) */
|
|
ins1(c, A_PUSHQ, areg(D_DX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_DX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* ptr */
|
|
continue;
|
|
}
|
|
if (!widen[i] && node_isstructarg(args[i]) && args[i]->kind == N_IDENT) {
|
|
/* load qword(s) directly from the struct's slot */
|
|
int off = localfind(locals, args[i]->str);
|
|
int sz = struct_arg_size(args[i]->type);
|
|
if (sz > 8) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
continue;
|
|
}
|
|
if (!widen[i] && node_istaggedarg(args[i]) && args[i]->kind == N_IDENT) {
|
|
/* Tagged-union: push each 8B word from the slot.
|
|
* High word goes first so the popper drains them
|
|
* in low→high order into the arg-register class. */
|
|
int off = localfind(locals, args[i]->str);
|
|
int sz = tagged_arg_size(args[i]->type);
|
|
int nwords = sz / 8;
|
|
for (int k = nwords - 1; k >= 0; k--) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + k*8),
|
|
areg(D_AX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
continue;
|
|
}
|
|
if (widen[i]) {
|
|
/* Concrete → tagged-union widening at the call
|
|
* site. Mirrors the let/assign/return widening:
|
|
* lay out the value in the parameter's slot
|
|
* shape, then push high→low so pop drains tag
|
|
* first.
|
|
*
|
|
* Branches by source shape:
|
|
* - nullable (sz==8): pointer IS the disc.
|
|
* - str: tag@+0, ptr@+8, len@+16.
|
|
* - struct ident: copy struct words then
|
|
* prepend tag, zero-pad to slot size.
|
|
* - struct literal: materialise via a stack
|
|
* scratch slot — store each field at its
|
|
* struct-relative offset (with the +8 tag
|
|
* shift), zero-fill, then push from slot.
|
|
* - tagged source: load src slot words, remap
|
|
* the tag word via cg_widen_tag_remap, pad
|
|
* to wider dst slot, push.
|
|
* - scalar: tag@+0, value@+8, optional pad. */
|
|
cg_widen_tagged_push(c, &locals, widen_param[i],
|
|
args[i], widen_sz[i]);
|
|
continue;
|
|
}
|
|
cgexpr(c, args[i], locals);
|
|
if (node_isfloat(args[i])) {
|
|
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
|
|
ins2(c, A_MOVSD, areg(D_X0), amem(D_SP, 0));
|
|
} else if (node_isstr(args[i])) {
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
|
|
} else if (node_isslice(args[i])) {
|
|
/* Slice-typed arg without a fast path above
|
|
* (e.g. `s: []u8` cast): cgexpr left
|
|
* (AX=ptr, BX=len, CX=cap). Push the triple. */
|
|
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
|
|
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
|
|
} else if (node_istaggedarg(args[i])) {
|
|
/* Tagged-return ABI: AX=tag, DX=val0,
|
|
* CX=val1, R8=val2. Push high-to-low so pop
|
|
* drains tag first (into arg-reg[0]), then
|
|
* values into arg-reg[1..]. Nullable (sz=8):
|
|
* AX holds the pointer, no value-word
|
|
* registers — push just AX. */
|
|
int sz = tagged_arg_size(args[i]->type);
|
|
if (sz > 24)
|
|
ins1(c, A_PUSHQ, areg(D_R8));
|
|
if (sz > 16)
|
|
ins1(c, A_PUSHQ, areg(D_CX));
|
|
if (sz > 8)
|
|
ins1(c, A_PUSHQ, areg(D_DX));
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
} else {
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
}
|
|
}
|
|
/* 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 = 0, fi = 0, stackslots = 0;
|
|
for (int i = 0; i < argcount; i++) {
|
|
if (widen[i]) {
|
|
/* Pop widened tagged slot into arg-register
|
|
* class — sized by the parameter's tagged slot,
|
|
* not the arg's static type. */
|
|
int eb = widen_sz[i] / 8;
|
|
for (int k = 0; k < eb; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ,
|
|
areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
continue;
|
|
}
|
|
if (node_isfloat(args[i])) {
|
|
if (fi < 8) {
|
|
ins2(c, A_MOVSD, amem(D_SP, 0),
|
|
areg(sysv_fargregs[fi]));
|
|
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
|
|
fi++;
|
|
} else {
|
|
stackslots++; /* leave on stack */
|
|
}
|
|
} else if (node_isstr(args[i])) {
|
|
for (int k = 0; k < 2; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if (node_isslice(args[i])) {
|
|
for (int k = 0; k < 3; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if (node_isstructarg(args[i])) {
|
|
int sz = struct_arg_size(args[i]->type);
|
|
int eb = (sz > 8) ? 2 : 1;
|
|
for (int k = 0; k < eb; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else if (node_istaggedarg(args[i])) {
|
|
int sz = tagged_arg_size(args[i]->type);
|
|
int eb = sz / 8;
|
|
for (int k = 0; k < eb; k++) {
|
|
if (ii < 6)
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
|
|
else
|
|
stackslots++;
|
|
}
|
|
} else {
|
|
if (ii < 6) {
|
|
ins1(c, A_POPQ, areg(sysv_argregs[ii]));
|
|
ii++;
|
|
} else {
|
|
stackslots++;
|
|
}
|
|
}
|
|
}
|
|
/* 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 {
|
|
ins1(c, A_CALL,
|
|
masym(c, n->lhs->str));
|
|
}
|
|
} 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) {
|
|
ins1(c, A_CALL, masym(c, n->lhs->str));
|
|
} else {
|
|
cgexpr(c, n->lhs, locals); /* AX = fn ptr */
|
|
ins1(c, A_CALL, areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_CALL, areg(D_AX));
|
|
}
|
|
/* SysV: caller cleans stack args. */
|
|
if (stackslots > 0)
|
|
ins2(c, A_ADDQ, aimm(stackslots * 8), areg(D_SP));
|
|
/* If callee returns a str (16B → AX:DX per SysV), shuffle
|
|
* len from DX into BX so str values stay in (AX, BX). */
|
|
if (node_isstr(n)) ins2(c, A_MOVQ, areg(D_DX), areg(D_BX));
|
|
break;
|
|
}
|
|
case N_MATCH: {
|
|
/* match on a tagged-union scrutinee. Read tag and value from
|
|
* the slot. Dispatch by the resolved variant index of each
|
|
* case's type pattern — case order is independent of variant
|
|
* declaration order. A case with no pattern (`case =>`) is a
|
|
* default arm; its body always runs.
|
|
*
|
|
* Slot layout: [+0]=tag, [+8]=value0, [+16]=value1. The third
|
|
* word is only meaningful for variants whose payload is >8B
|
|
* (e.g. str). Bindings sized 16B (str) copy two words.
|
|
*
|
|
* Nullable folded `(*T | void)`: slot is one 8B word holding
|
|
* the pointer; null IS the void variant. Discriminator =
|
|
* value, not a separate tag. */
|
|
Node *s = n->lhs;
|
|
Type *st = s ? s->type : NULL;
|
|
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int is_nullable = type_isnullable(st);
|
|
int slot_size = (su && su->kind == TY_TAGGED) ? (int)su->size : 16;
|
|
int sl_off;
|
|
if (s->kind == N_IDENT) {
|
|
sl_off = localfind(locals, s->str);
|
|
} else if (s->kind == N_DOT && s->lhs && s->lhs->kind == N_IDENT
|
|
&& s->lhs->type) {
|
|
/* `match (p.field)` — point sl_off at the field's slot
|
|
* inside the parent struct. The slot layout (tag at +0,
|
|
* value words at +8/+16) is contiguous within the struct,
|
|
* so no spill is needed. */
|
|
Type *bt = s->lhs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
Tfield *f = NULL;
|
|
if (bu && bu->kind == TY_STRUCT) {
|
|
for (Tfield *fl = bu->fields; fl; fl = fl->next) {
|
|
if (strcmp(fl->name, s->str) == 0) {
|
|
f = fl; break;
|
|
}
|
|
}
|
|
}
|
|
if (f) {
|
|
int boff = localfind(locals, s->lhs->str);
|
|
sl_off = boff + (int)f->offset;
|
|
} else {
|
|
/* fall back to spill — `match (h.e)` where
|
|
* h is *struct. cgexpr → cgdot now leaves the
|
|
* AX=tag, DX=val0, CX=val1[, R8=val2] shape
|
|
* (task #28), so spill all words the variant
|
|
* may carry. Pre-#28 only AX landed and the
|
|
* dispatch fired on a stale slot. */
|
|
sl_off = localoff(c, &locals, "@match_spill",
|
|
slot_size, cg_frame);
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, sl_off + 0));
|
|
if (!is_nullable) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
if (slot_size > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, sl_off + 24));
|
|
}
|
|
}
|
|
} else {
|
|
/* Spill non-ident scrutinees (e.g. `match (foo()?)`) into
|
|
* a scratch slot so we can index out the tag/value. The
|
|
* call ABI for tagged returns is AX=tag, DX=value0,
|
|
* CX=value1, R8=value2 — copy each word into the slot.
|
|
* Nullable returns are single-word: AX is the pointer;
|
|
* spill only that. */
|
|
sl_off = localoff(c, &locals, "@match_spill", slot_size,
|
|
cg_frame);
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
|
if (!is_nullable) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
if (slot_size > 24)
|
|
ins2(c, A_MOVQ, areg(D_R8),
|
|
amem(D_BP, sl_off + 24));
|
|
}
|
|
}
|
|
char *end = mklabel(c, "match_end");
|
|
/* Push the end label as the yield target for arm bodies. */
|
|
if (nyields < YIELD_MAX) {
|
|
yield_target[nyields++] = end;
|
|
}
|
|
for (Node *cs = n->list; cs; cs = cs->next) {
|
|
char *next = mklabel(c, "match_next");
|
|
/* Per-arm scope: save the locals head, restore it
|
|
* after the body runs. Mirrors check.c's saved/restore
|
|
* around cstmt — the case bind (and any lets inside
|
|
* the arm) shouldn't leak past the arm, where a
|
|
* matching outer name would otherwise resolve to the
|
|
* shadow instead of the original. */
|
|
Local *arm_locals_saved = locals;
|
|
if (cs->type != NULL) {
|
|
int tag = cg_tag_for_variant(su, cs->type);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
if (is_nullable) {
|
|
/* discriminator = pointer-vs-null.
|
|
* *T variant: skip if ptr == 0.
|
|
* void variant: skip if ptr != 0. */
|
|
int ptr_tag = nullable_ptr_tag(su);
|
|
int want_ptr = (tag == ptr_tag);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
if (want_ptr)
|
|
ins1(c, A_JE, abranch(next));
|
|
else
|
|
ins1(c, A_JNE, abranch(next));
|
|
} else if (cs->list != NULL) {
|
|
/* Multi-pattern `case T1 | T2 | ... =>`:
|
|
* if the tag matches any of the alts,
|
|
* jump to body; otherwise to the next
|
|
* case. */
|
|
char *body = mklabel(c, "match_body");
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag),
|
|
areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
for (Node *alt = cs->list; alt;
|
|
alt = alt->next) {
|
|
int atag = cg_tag_for_variant(
|
|
su, alt->type);
|
|
ins2(c, A_CMPQ,
|
|
aimm(atag < 0 ? 0 : atag),
|
|
areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
}
|
|
ins1(c, A_JMP, abranch(next));
|
|
label(c, body);
|
|
} else {
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag),
|
|
areg(D_AX));
|
|
ins1(c, A_JNE, abranch(next));
|
|
}
|
|
}
|
|
if (cs->str && cs->str[0] && cs->type) {
|
|
Type *bt = cs->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
if (is_nullable) {
|
|
/* Bind *T or void to a local. The
|
|
* value IS the slot's pointer word; no
|
|
* payload to copy. void binding is
|
|
* unusable (size 0), so only emit for
|
|
* the *T variant. local_alloc (not
|
|
* localoff): the bind must NEVER reuse
|
|
* an outer same-named slot. */
|
|
if (bu && bu->kind == TY_PTR) {
|
|
int voff = local_alloc(c, &locals,
|
|
cs->str, 8, cg_frame);
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, sl_off + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, voff));
|
|
}
|
|
} else {
|
|
int bsz = 8;
|
|
if (bu && bu->kind == TY_STR) bsz = 16;
|
|
else if (bu && bu->kind == TY_SLICE) bsz = 24;
|
|
else if (bu) bsz = (int)bu->size;
|
|
if (bsz <= 0) bsz = 8;
|
|
/* local_alloc to dodge name-collision
|
|
* dedup — a 16B str bind shadowing an
|
|
* 8B outer would otherwise overflow
|
|
* into the saved BP. */
|
|
int voff = local_alloc(c, &locals, cs->str,
|
|
bsz, cg_frame);
|
|
int nwords = (bsz + 7) / 8;
|
|
for (int w = 0; w < nwords; w++) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, sl_off + 8 + 8*w),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, voff + 8*w));
|
|
}
|
|
}
|
|
}
|
|
cgstmt(c, cs->body, &locals, cg_frame);
|
|
locals = arm_locals_saved;
|
|
ins1(c, A_JMP, abranch(end));
|
|
label(c, next);
|
|
}
|
|
label(c, end);
|
|
if (nyields > 0) nyields--;
|
|
break;
|
|
}
|
|
case N_TRYPROP: {
|
|
/* Evaluate tagged value: AX=tag, DX=value0[, CX=value1].
|
|
* If the tag matches an error variant, propagate as the
|
|
* current function's return (with a tag remap to the
|
|
* enclosing fn's variant order). On success, unwrap to the
|
|
* success-variant ABI: ≤8B values in AX; str values in
|
|
* (AX=ptr, BX=len).
|
|
*
|
|
* Nullable: AX is the pointer; *T variant is the success
|
|
* (any non-null), void variant is the error (null). The
|
|
* enclosing fn's null encoding is the same — RET with AX=0
|
|
* if propagating; otherwise leave AX as-is on success. */
|
|
cgexpr(c, n->lhs, locals);
|
|
Type *u = n->lhs ? n->lhs->type : NULL;
|
|
if (u && u->kind == TY_NAMED) u = u->under;
|
|
Type *r = cg_ret_type;
|
|
if (r && r->kind == TY_NAMED) r = r->under;
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
char *cont = mklabel(c, "tryprop_ok");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(cont));
|
|
/* null = error: propagate. AX already 0; matches
|
|
* the enclosing nullable encoding if it has one. */
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
label(c, cont);
|
|
break;
|
|
}
|
|
int s_tag = cg_tagged_success_tag(u);
|
|
Type *succ_t = NULL;
|
|
if (u && u->kind == TY_TAGGED) {
|
|
int i = 0;
|
|
for (Tparam *p = u->params; p; p = p->next, i++)
|
|
if (i == s_tag) { succ_t = p->type; break; }
|
|
}
|
|
int success_is_str = type_isstr(succ_t);
|
|
char *cont = mklabel(c, "tryprop_ok");
|
|
ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX));
|
|
ins1(c, A_JE, abranch(cont));
|
|
if (u && r && r->kind == TY_TAGGED && u->params) {
|
|
char *propret = mklabel(c, "tryprop_ret");
|
|
int i = 0;
|
|
for (Tparam *p = u->params; p; p = p->next, i++) {
|
|
if (!cg_variant_is_error(u, i)) continue;
|
|
int j = cg_tag_for_variant(r, p->type);
|
|
if (j < 0) j = 0;
|
|
if (j == i) continue;
|
|
char *skip = mklabel(c, "tryprop_skip");
|
|
ins2(c, A_CMPQ, aimm(i), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(skip));
|
|
ins2(c, A_MOVQ, aimm(j), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(propret));
|
|
label(c, skip);
|
|
}
|
|
label(c, propret);
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
label(c, cont);
|
|
if (success_is_str)
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case N_TRYUNW: {
|
|
/* On error variant: exit(1) directly via the syscall.
|
|
* Nullable: null = error; non-null = success (AX is the
|
|
* pointer, ready to use). */
|
|
cgexpr(c, n->lhs, locals);
|
|
Type *u = n->lhs ? n->lhs->type : NULL;
|
|
if (u && u->kind == TY_NAMED) u = u->under;
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
char *cont = mklabel(c, "tryunw_ok");
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(cont));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, cont);
|
|
break;
|
|
}
|
|
int s_tag = cg_tagged_success_tag(u);
|
|
Type *succ_t = NULL;
|
|
if (u && u->kind == TY_TAGGED) {
|
|
int i = 0;
|
|
for (Tparam *p = u->params; p; p = p->next, i++)
|
|
if (i == s_tag) { succ_t = p->type; break; }
|
|
}
|
|
int success_is_str = type_isstr(succ_t);
|
|
char *cont = mklabel(c, "tryunw_ok");
|
|
ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX));
|
|
ins1(c, A_JE, abranch(cont));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, cont);
|
|
if (success_is_str)
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
|
|
break;
|
|
}
|
|
case N_TYPETEST: {
|
|
/* `e is T` — Compare scrutinee tag against T's variant index.
|
|
* Result is bool (0/1) in AX. Nullable: discriminator is
|
|
* pointer-vs-null, not a tag. */
|
|
cgexpr(c, n->lhs, locals);
|
|
Type *u = n->lhs ? n->lhs->type : NULL;
|
|
if (u && u->kind == TY_NAMED) u = u->under;
|
|
Type *vt = n->rhs ? n->rhs->type : NULL;
|
|
char *ne = mklabel(c, "is_ne");
|
|
char *done = mklabel(c, "is_done");
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
int ptr_tag = nullable_ptr_tag(u);
|
|
int want_ptr = (tag == ptr_tag);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
if (want_ptr)
|
|
ins1(c, A_JE, abranch(ne));
|
|
else
|
|
ins1(c, A_JNE, abranch(ne));
|
|
} else {
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
|
|
ins1(c, A_JNE, abranch(ne));
|
|
}
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_AX));
|
|
ins1(c, A_JMP, abranch(done));
|
|
label(c, ne);
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
label(c, done);
|
|
break;
|
|
}
|
|
case N_TYPEASSERT: {
|
|
/* `e as T` — abort if tag != T's variant index; otherwise
|
|
* unwrap value to T's ABI: scalar/ptr variants land in AX;
|
|
* 16B str variants in AX:BX.
|
|
*
|
|
* We need both tag *and* value words. For an N_IDENT local
|
|
* the value lives at slot+8/+16 — cgexpr's single-MOVQ path
|
|
* does not load it. Mirror match's pattern: resolve a slot
|
|
* offset (existing local or a fresh @asrt_spill) and index
|
|
* out tag/value from memory.
|
|
*
|
|
* Nullable: the slot's word IS the pointer. *T variant
|
|
* asserts non-null; void variant asserts null. The value
|
|
* left in AX after the check is the pointer itself. */
|
|
Node *s = n->lhs;
|
|
Type *st = s ? s->type : NULL;
|
|
Type *u = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
Type *vt = n->type;
|
|
/* Enum ↔ integer: reinterpret-only. The value already lives
|
|
* in AX after evaluating the LHS; no tag/unwrap needed. */
|
|
{
|
|
Type *vu = (vt && vt->kind == TY_NAMED) ? vt->under : vt;
|
|
if ((u && u->kind == TY_ENUM) ||
|
|
(vu && vu->kind == TY_ENUM)) {
|
|
cgexpr(c, s, locals);
|
|
break;
|
|
}
|
|
}
|
|
int slot_size = (u && u->kind == TY_TAGGED) ? (int)u->size : 16;
|
|
int sl_off = 0;
|
|
if (s && s->kind == N_IDENT && s->str) {
|
|
sl_off = localfind(locals, s->str);
|
|
}
|
|
if (sl_off == 0) {
|
|
sl_off = localoff(c, &locals, "@asrt_spill",
|
|
slot_size, cg_frame);
|
|
cgexpr(c, s, locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0));
|
|
if (!(u && u->kind == TY_TAGGED && u->nullable)) {
|
|
ins2(c, A_MOVQ, areg(D_DX),
|
|
amem(D_BP, sl_off + 8));
|
|
if (slot_size > 16)
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
amem(D_BP, sl_off + 16));
|
|
}
|
|
}
|
|
char *ok = mklabel(c, "asrt_ok");
|
|
if (u && u->kind == TY_TAGGED && u->nullable) {
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
int ptr_tag = nullable_ptr_tag(u);
|
|
int want_ptr = (tag == ptr_tag);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
if (want_ptr)
|
|
ins1(c, A_JNE, abranch(ok));
|
|
else
|
|
ins1(c, A_JE, abranch(ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, ok);
|
|
/* AX already holds the pointer (or 0 for void
|
|
* variant, where the result type has size 0 and
|
|
* no consumer reads it). */
|
|
break;
|
|
}
|
|
int tag = cg_tag_for_variant(u, vt);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX));
|
|
ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX));
|
|
ins1(c, A_JE, abranch(ok));
|
|
ins2(c, A_MOVQ, aimm(1), areg(D_DI));
|
|
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
|
|
ins0(c, A_SYSCALL);
|
|
label(c, ok);
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 8), areg(D_AX));
|
|
if (type_isstr(vt))
|
|
ins2(c, A_MOVQ, amem(D_BP, sl_off + 16), areg(D_BX));
|
|
break;
|
|
}
|
|
case N_CAST: {
|
|
int from_f = node_isfloat(n->lhs);
|
|
int to_f = cg_isfloat(n->type);
|
|
int from_f32 = node_isf32(n->lhs);
|
|
int to_f32 = type_isf32(n->type);
|
|
cgexpr(c, n->lhs, locals); /* AX or X0 depending */
|
|
if (from_f && !to_f) {
|
|
int op = from_f32 ? A_CVTTSS2SI : A_CVTTSD2SI;
|
|
ins2(c, op, areg(D_X0), areg(D_AX));
|
|
} else if (!from_f && to_f) {
|
|
int op = to_f32 ? A_CVTSI2SS : A_CVTSI2SD;
|
|
ins2(c, op, areg(D_AX), areg(D_X0));
|
|
} else if (from_f && to_f && from_f32 != to_f32) {
|
|
int op = to_f32 ? A_CVTSD2SS : A_CVTSS2SD;
|
|
ins2(c, op, areg(D_X0), areg(D_X0));
|
|
}
|
|
/* str → []u8 (or any []T): cgexpr left (AX=ptr, BX=len).
|
|
* Slice register convention is (AX=ptr, BX=len, CX=cap);
|
|
* synthesise cap = len so downstream arg-push / let-init
|
|
* paths see the canonical triple. Without this, the cap
|
|
* register stays whatever cgexpr happened to leave there
|
|
* and the receiver reads a stale value. */
|
|
{
|
|
Type *tt = n->type;
|
|
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
|
|
Type *ft = n->lhs ? n->lhs->type : NULL;
|
|
Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft;
|
|
if (tu && tu->kind == TY_SLICE
|
|
&& fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
}
|
|
}
|
|
/* Narrowing integer cast: clamp AX to the target width so
|
|
* downstream 64-bit ops see a value within the declared
|
|
* range. Hare semantics: `expr: T` truncates to T's bit
|
|
* width (mod 2^n). Without this, `(big_u64): u32` left the
|
|
* upper 32 bits intact and CMPQ/DIVQ misread the value.
|
|
*
|
|
* Unsigned targets use MOVL/ANDQ to clear the high bits.
|
|
* Signed-narrow targets (i8/i16/i32) sign-extend via
|
|
* MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates;
|
|
* this is what lets `(0xFF80i64): i8` compare equal to
|
|
* -128i64 after a widening read-back. Symmetric on signed
|
|
* vs unsigned: both branches gate on `type_isint(tu) &&
|
|
* size<8`, then dispatch on type_isunsigned(tu). The
|
|
* recursion through TY_ENUM in type_isunsigned (task #5)
|
|
* is what lets an enum-aliased narrow (`type myflag = i8`)
|
|
* pick up the right MOVS*Q. Wwstage's cgcast keys off the
|
|
* resolved type-name through the same shape. TY_RUNE is
|
|
* unsigned (Unicode scalar) and lands on the MOVL path. */
|
|
if (!from_f && !to_f && n->type) {
|
|
Type *tt = n->type;
|
|
Type *tu = (tt && tt->kind == TY_NAMED) ? tt->under : tt;
|
|
if (tu && type_isint(tu) && tu->size > 0
|
|
&& tu->size < 8) {
|
|
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. */
|
|
Type *bt = n->lhs ? n->lhs->type : NULL;
|
|
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
/* Module-qualified value reference: `mod.name`. The checker
|
|
* leaves SK_USE idents untyped (NULL/ty_err); detect that and
|
|
* look up the leaf in the flat (driver-concatenated) sym/def
|
|
* maps the same way a bare N_IDENT would. */
|
|
if (n->lhs && n->lhs->kind == N_IDENT
|
|
&& (bt == NULL || bt == ty_err)) {
|
|
Type *t = n->type;
|
|
Type *tu = (t && t->kind == TY_NAMED) ? t->under : t;
|
|
if (tu && tu->kind == TY_FN) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, n->str), areg(D_AX));
|
|
break;
|
|
}
|
|
for (Sdef *s = sdefs; s; s = s->next) {
|
|
if (strcmp(s->name, n->str) != 0) continue;
|
|
const char *lab = intern_strlit(c, s->bytes,
|
|
s->len);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
ins2(c, A_MOVQ, aimm((long long)s->len),
|
|
areg(D_BX));
|
|
goto dot_done;
|
|
}
|
|
/* Same gating as the bare-ident catch-all: lets route
|
|
* through localloadop (their slot can be the target of
|
|
* a narrow deref-store via `&letname: *iN`); defs and
|
|
* unresolved symbols stay on MOVQ so wwstage's defent-
|
|
* registry-without-tnode shape agrees byte-for-byte. */
|
|
int mqop = let_islet(n->str)
|
|
? localloadop(n->type) : A_MOVQ;
|
|
if (mqop == A_MOVQ) {
|
|
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX));
|
|
ins2(c, mqop, amem(D_CX, 0), areg(D_AX));
|
|
}
|
|
goto dot_done;
|
|
}
|
|
/* Chained N_DOT spine through value-struct fields. Handles any
|
|
* depth `root.f0.f1.…leaf` where every intermediate field is a
|
|
* value struct, plus the slice/str pseudo-field tail (`s.buf.len`)
|
|
* where the innermost field is a slice/str header. Walks inward
|
|
* collecting (parent_struct, field_name); reverses to sum field
|
|
* offsets; emits one load at (base + total_off). Placed BEFORE
|
|
* the slice/str pseudo-field branch so its else-arm (cgexpr lhs
|
|
* + shuffle BX→AX) doesn't mis-handle `b.buf.len` — cgexpr on a
|
|
* value-struct→slice chain only loads .ptr into AX, leaving BX
|
|
* stale. Sibling of the pointer-chain branch further down. */
|
|
if (n->lhs && n->lhs->kind == N_DOT) {
|
|
Type *lt0 = n->lhs->type;
|
|
Type *lu0 = (lt0 && lt0->kind == TY_NAMED) ? lt0->under : lt0;
|
|
int leaf_is_pseudo = lu0 && n->str
|
|
&& (lu0->kind == TY_SLICE || lu0->kind == TY_STR)
|
|
&& (strcmp(n->str, "ptr") == 0
|
|
|| strcmp(n->str, "len") == 0
|
|
|| strcmp(n->str, "cap") == 0);
|
|
int leaf_in_struct = lu0 && lu0->kind == TY_STRUCT;
|
|
if (leaf_is_pseudo || leaf_in_struct) {
|
|
struct { Type *pu; const char *name; } steps[16];
|
|
int nsteps = 0;
|
|
int ptr_root = 0;
|
|
Node *cur = n;
|
|
int abort = 0;
|
|
while (cur && cur->kind == N_DOT && cur->lhs) {
|
|
Type *pt = cur->lhs->type;
|
|
Type *pu = (pt && pt->kind == TY_NAMED)
|
|
? pt->under : pt;
|
|
if (!pu) { abort = 1; break; }
|
|
if (cur == n && (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR)) {
|
|
/* leaf pseudo on slice/str header */
|
|
} else if (pu->kind == TY_STRUCT) {
|
|
/* value-struct hop */
|
|
} else if (pu->kind == TY_PTR && pu->sub
|
|
&& cur->lhs->kind == N_IDENT) {
|
|
/* `*T` root: dereference once at emit
|
|
* time, then walk offsets through the
|
|
* pointee. Only at the last hop (root
|
|
* is a bare ident) — `*T`-field mid-
|
|
* chain keeps its cgexpr-based pointer-
|
|
* field branch further down. */
|
|
Type *sub = (pu->sub->kind == TY_NAMED)
|
|
? pu->sub->under : pu->sub;
|
|
if (sub && sub->kind == TY_STRUCT) {
|
|
pu = sub;
|
|
ptr_root = 1;
|
|
} else {
|
|
abort = 1;
|
|
break;
|
|
}
|
|
} else {
|
|
abort = 1;
|
|
break;
|
|
}
|
|
if (nsteps >= 16) { abort = 1; break; }
|
|
steps[nsteps].pu = pu;
|
|
steps[nsteps].name = cur->str;
|
|
nsteps++;
|
|
cur = cur->lhs;
|
|
}
|
|
if (!abort && cur && cur->kind == N_IDENT
|
|
&& nsteps > 0) {
|
|
int total_off = 0;
|
|
Type *leaf_type = NULL;
|
|
int slice_delta = -1;
|
|
int ok = 1;
|
|
for (int i = nsteps - 1; i >= 0; i--) {
|
|
Type *pu = steps[i].pu;
|
|
if (pu->kind == TY_SLICE
|
|
|| pu->kind == TY_STR) {
|
|
if (strcmp(steps[i].name, "ptr") == 0)
|
|
slice_delta = 0;
|
|
else if (strcmp(steps[i].name, "len") == 0)
|
|
slice_delta = 8;
|
|
else if (strcmp(steps[i].name, "cap") == 0)
|
|
slice_delta = 16;
|
|
else { ok = 0; break; }
|
|
} else {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = pu->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, steps[i].name) == 0)
|
|
{ f = fl; break; }
|
|
if (!f) { ok = 0; break; }
|
|
total_off += (int)f->offset;
|
|
leaf_type = f->type;
|
|
}
|
|
}
|
|
if (ok) {
|
|
int root_off = localfind(locals, cur->str);
|
|
int base_reg = D_BP;
|
|
int base_disp = root_off;
|
|
int root_resolved = (root_off != 0);
|
|
if (!root_resolved && let_islet(cur->str)) {
|
|
ins2(c, A_LEAQ,
|
|
masym(c, cur->str), areg(D_CX));
|
|
base_reg = D_CX;
|
|
base_disp = 0;
|
|
root_resolved = 1;
|
|
}
|
|
if (root_resolved && ptr_root) {
|
|
/* `*T` root: load the pointer value
|
|
* once; field accesses then index at
|
|
* total_off off the pointer. */
|
|
if (base_reg == D_BP) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, base_disp),
|
|
areg(D_CX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_CX, 0), areg(D_CX));
|
|
}
|
|
base_reg = D_CX;
|
|
base_disp = 0;
|
|
}
|
|
if (root_resolved) {
|
|
if (slice_delta >= 0) {
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + slice_delta),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
Type *fu = (leaf_type
|
|
&& leaf_type->kind == TY_NAMED)
|
|
? leaf_type->under : leaf_type;
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 8),
|
|
areg(D_BX));
|
|
goto dot_done;
|
|
}
|
|
if (fu && fu->kind == TY_SLICE) {
|
|
/* Slice leaf: load all three header
|
|
* words into (AX=ptr, BX=len, CX=cap)
|
|
* so the value follows the canonical
|
|
* slice-rhs convention. base_reg may
|
|
* be CX (global / `*T` root); load
|
|
* .cap LAST so the base survives the
|
|
* earlier reads. */
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg,
|
|
base_disp + total_off + 16),
|
|
areg(D_CX));
|
|
goto dot_done;
|
|
}
|
|
int g_isf32 = 0;
|
|
if (fld_isfloat(leaf_type, &g_isf32)) {
|
|
int mov = g_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(base_reg,
|
|
base_disp + total_off),
|
|
areg(D_X0));
|
|
goto dot_done;
|
|
}
|
|
int fsz = (int)(leaf_type
|
|
? leaf_type->size : 8);
|
|
int op = fldloadop(leaf_type, fsz);
|
|
ins2(c, op,
|
|
amem(base_reg,
|
|
base_disp + total_off),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
int lenfld = (n->str && strcmp(n->str, "len") == 0);
|
|
int capfld = (n->str && strcmp(n->str, "cap") == 0);
|
|
int ptrfld = (n->str && strcmp(n->str, "ptr") == 0);
|
|
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
|
|
&& (lenfld || capfld || ptrfld)) {
|
|
if (n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (off == 0) {
|
|
/* Not a local — could be `def NAME: str
|
|
* = "lit"`. Sdef-backed strs aren't laid
|
|
* out in memory; emit .ptr/.len from the
|
|
* literal directly, mirroring the bare
|
|
* N_IDENT branch above. Without this we'd
|
|
* load BP+8 (return-address slot) as the
|
|
* "len". */
|
|
for (Sdef *s = sdefs; s; s = s->next) {
|
|
if (strcmp(s->name, n->lhs->str) != 0)
|
|
continue;
|
|
if (ptrfld) {
|
|
const char *lab = intern_strlit(c,
|
|
s->bytes, s->len);
|
|
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
aimm((long long)s->len),
|
|
areg(D_AX));
|
|
}
|
|
goto dot_done;
|
|
}
|
|
/* Top-level str/slice `let` — load
|
|
* the field through &name(SB). Same
|
|
* pattern as the bare N_IDENT load. */
|
|
if (let_islet(n->lhs->str)) {
|
|
int delta = ptrfld ? 0
|
|
: (lenfld ? 8 : 16);
|
|
ins2(c, A_LEAQ,
|
|
masym(c, n->lhs->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_CX, delta),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
int delta = ptrfld ? 0 : (lenfld ? 8 : 16);
|
|
ins2(c, A_MOVQ, amem(D_BP, off + delta),
|
|
areg(D_AX));
|
|
} else {
|
|
/* Evaluate the str/slice expression — leaves
|
|
* (AX=ptr, BX=len) for str. .ptr returns AX,
|
|
* .len shuffles BX→AX. */
|
|
cgexpr(c, n->lhs, locals);
|
|
if (lenfld)
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
if (u && u->kind == TY_ARRAY && n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
if (lenfld) {
|
|
ins2(c, A_MOVQ, aimm((long long)u->alen), areg(D_AX));
|
|
break;
|
|
}
|
|
if (ptrfld) {
|
|
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
|
|
break;
|
|
}
|
|
}
|
|
/* tuple positional field access: t.0, t.1, ... */
|
|
if (u && u->kind == TY_TUPLE && n->lhs->kind == N_IDENT && n->str) {
|
|
int idx = 0;
|
|
for (const char *q = n->str; *q; q++) idx = idx * 10 + (*q - '0');
|
|
Tparam *tp = u->params;
|
|
int foff = 0;
|
|
while (idx > 0 && tp) {
|
|
if (tp->type) foff += (int)tp->type->size;
|
|
tp = tp->next;
|
|
idx--;
|
|
}
|
|
if (tp != NULL) {
|
|
int fsz = (int)(tp->type ? tp->type->size : 8);
|
|
Type *fu = (tp->type && tp->type->kind == TY_NAMED)
|
|
? tp->type->under : tp->type;
|
|
int op = fldloadop(tp->type, fsz);
|
|
int off = localfind(locals, n->lhs->str);
|
|
/* str element: load (ptr, len) into (AX, BX) so chains
|
|
* like `t.1.len` propagate through the str-rhs
|
|
* convention. Without this we'd MOVQ 8B and the .len
|
|
* shuffle (BX→AX) would surface garbage. */
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ, amem(D_BP, off + foff + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, off + foff + 8), areg(D_BX));
|
|
break;
|
|
}
|
|
ins2(c, op, amem(D_BP, off + foff), areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
/* real struct field: load at struct_base + field_off.
|
|
* Base is either a local frame slot (off(BP)) or a top-
|
|
* level let global (&name(SB) into CX); we resolve which
|
|
* once and then share the field-walk code. */
|
|
if (u && u->kind == TY_STRUCT && n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
int is_global = 0;
|
|
int base_reg = D_BP;
|
|
int base_disp = off;
|
|
if (off == 0 && let_islet(n->lhs->str)) {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX));
|
|
is_global = 1;
|
|
base_reg = D_CX;
|
|
base_disp = 0;
|
|
}
|
|
for (Tfield *f = u->fields; f; f = f->next) {
|
|
if (strcmp(f->name, n->str) != 0) continue;
|
|
/* tagged-union field: load AX=tag, DX=val0,
|
|
* CX=val1, R8=val2 (CX last, since for globals
|
|
* CX is also the base addr; load R8 before CX
|
|
* so the base address survives the +24 read).
|
|
* Mirrors the tagged-return ABI so the let-init
|
|
* / match dispatch shapes just work. The val2
|
|
* word fires for slice-variant tagged-unions
|
|
* (slot = 8 tag + 24 slice header = 32B). */
|
|
Type *tag_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (tag_fu && tag_fu->kind == TY_TAGGED) {
|
|
int fo = base_disp + (int)f->offset;
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, fo + 0), areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, fo + 8), areg(D_DX));
|
|
if (tag_fu->size > 24)
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, fo + 24),
|
|
areg(D_R8));
|
|
if (tag_fu->size > 16)
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, fo + 16),
|
|
areg(D_CX));
|
|
(void)is_global;
|
|
break;
|
|
}
|
|
/* str field: load (ptr, len) into (AX, BX) so the
|
|
* value flows through the str-rhs convention. */
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (str_fu && str_fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 8),
|
|
areg(D_BX));
|
|
break;
|
|
}
|
|
/* slice field: load (ptr, len, cap) into (AX, BX, CX)
|
|
* so the value flows through the slice-rhs convention.
|
|
* base_reg may be CX for globals; load .cap LAST so
|
|
* the base survives the earlier reads. */
|
|
if (str_fu && str_fu->kind == TY_SLICE) {
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(base_reg, base_disp + (int)f->offset + 16),
|
|
areg(D_CX));
|
|
break;
|
|
}
|
|
/* f64/f32 field: route through X0 (MOVSD/MOVSS).
|
|
* Loading via MOVQ AX would put the bits in the
|
|
* integer reg, and any downstream consumer that
|
|
* reads X0 (arg pass, return, arithmetic) would see
|
|
* stale data. */
|
|
int e_isf32 = 0;
|
|
if (fld_isfloat(f->type, &e_isf32)) {
|
|
int mov = e_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(base_reg, base_disp + (int)f->offset),
|
|
areg(D_X0));
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int op = fldloadop(f->type, fsz);
|
|
ins2(c, op,
|
|
amem(base_reg, base_disp + (int)f->offset),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
/* pointer-to-slice/str field: deref and read pseudo-field.
|
|
* Used by helpers like rt_appendu8(s: *[]u8, v: u8). */
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *inner = u->sub;
|
|
if (inner->kind == TY_NAMED) inner = inner->under;
|
|
if (inner && (inner->kind == TY_SLICE || inner->kind == TY_STR)
|
|
&& (lenfld || capfld || ptrfld)
|
|
&& n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->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; ... } */
|
|
if (u && u->kind == TY_PTR && u->sub) {
|
|
Type *inner = u->sub;
|
|
if (inner->kind == TY_NAMED) inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT
|
|
&& n->lhs->kind == N_IDENT) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
|
|
for (Tfield *f = inner->fields; f; f = f->next) {
|
|
if (strcmp(f->name, n->str) != 0) continue;
|
|
/* tagged-union field through *struct: BX
|
|
* already holds the *struct pointer. Load
|
|
* the four payload regs from (BX, f->offset)
|
|
* — BX is not a target (AX/DX/CX/R8), so
|
|
* load order is harmless. Mirrors the direct-
|
|
* struct branch above so consumers see the
|
|
* same tagged-return register shape
|
|
* regardless of pointer rooting. Pre-#28 fell
|
|
* through to fldloadop and dropped the
|
|
* payload words. */
|
|
Type *ptag_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (ptag_fu && ptag_fu->kind == TY_TAGGED) {
|
|
int fo = (int)f->offset;
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 8),
|
|
areg(D_DX));
|
|
if (ptag_fu->size > 16)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 16),
|
|
areg(D_CX));
|
|
if (ptag_fu->size > 24)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, fo + 24),
|
|
areg(D_R8));
|
|
break;
|
|
}
|
|
/* str field through *struct: read len into a
|
|
* scratch first (it's at +8) so loading ptr
|
|
* into AX last leaves (AX=ptr, BX=len). We
|
|
* use CX as the scratch, then move CX→BX. */
|
|
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
|
|
? f->type->under : f->type;
|
|
if (str_fu && str_fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 8),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
|
|
break;
|
|
}
|
|
/* slice field through *struct: load (ptr, len,
|
|
* cap) into (AX, BX, CX). BX holds the *struct
|
|
* pointer, so load .len LAST — the earlier loads
|
|
* still index off the original base. */
|
|
if (str_fu && str_fu->kind == TY_SLICE) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, (int)f->offset + 8),
|
|
areg(D_BX));
|
|
break;
|
|
}
|
|
/* f64/f32 field via *struct: load into X0.
|
|
* BX already holds the struct pointer from
|
|
* the MOVQ amem(D_BP,off) above. */
|
|
int f_isf32 = 0;
|
|
if (fld_isfloat(f->type, &f_isf32)) {
|
|
int mov = f_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_BX, (int)f->offset),
|
|
areg(D_X0));
|
|
break;
|
|
}
|
|
int fsz = (int)(f->type ? f->type->size : 8);
|
|
int op = fldloadop(f->type, fsz);
|
|
ins2(c, op,
|
|
amem(D_BX, (int)f->offset),
|
|
areg(D_AX));
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* Chained N_DOT through a *struct field. cgexpr lhs leaves
|
|
* AX = the inner *struct pointer; load the requested field
|
|
* with a single MOVQ. Without this, returning `o.p.val`
|
|
* silently leaves AX = o.p (the pointer) and the outer
|
|
* cast/use sees the pointer instead of the dereferenced
|
|
* field. (Surfaced building ww-w6l.) */
|
|
if (n->lhs->kind == N_DOT) {
|
|
Type *lt = n->lhs->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
if (lu && lu->kind == TY_PTR && lu->sub) {
|
|
Type *inner = lu->sub;
|
|
if (inner->kind == TY_NAMED) inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
for (Tfield *f = inner->fields; f; f = f->next) {
|
|
if (strcmp(f->name, n->str) != 0) continue;
|
|
cgexpr(c, n->lhs, locals); /* AX = inner ptr */
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
/* str field: load (ptr, len) into (AX, BX). */
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 0),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
/* slice field: load (ptr, len, cap) into
|
|
* (AX, BX, CX). AX is the *struct base, so
|
|
* load .ptr (which targets AX) LAST. */
|
|
if (fu && fu->kind == TY_SLICE) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, (int)f->offset + 0),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
/* f64/f32 chained field: read into X0. */
|
|
int g_isf32 = 0;
|
|
if (fld_isfloat(ft, &g_isf32)) {
|
|
int mov = g_isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_AX, (int)f->offset),
|
|
areg(D_X0));
|
|
goto dot_done;
|
|
}
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int op = fldloadop(ft, fsz);
|
|
ins2(c, op, amem(D_AX, (int)f->offset),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* `arr[i].field` — element-then-field through a `[N]*S` /
|
|
* `[N]S` (and slice/`*[N]S`) base. One branch covers both
|
|
* shapes: compute `&arr[i]` into BX, then either deref
|
|
* (`*Struct` element) or move-to-AX (value `Struct` element),
|
|
* so the leaf load is `(field.offset)(AX)` either way.
|
|
* Bypasses cgindex deliberately — cgindex's final MOVQ
|
|
* would truncate a value-struct element to 8 bytes. Mirrors
|
|
* selfhost/cmd/wcc/cgenexpr.ww's cgdot N_INDEX-lhs branch. */
|
|
if (n->lhs && n->lhs->kind == N_INDEX && n->lhs->lhs
|
|
&& n->lhs->lhs->kind == N_IDENT) {
|
|
Node *idxbase = n->lhs->lhs;
|
|
Type *elemt = n->lhs->type;
|
|
Type *elemu = (elemt && elemt->kind == TY_NAMED)
|
|
? elemt->under : elemt;
|
|
Type *struct_t = NULL;
|
|
int viaptr = 0;
|
|
if (elemu && elemu->kind == TY_PTR) {
|
|
Type *inner = elemu->sub;
|
|
if (inner && inner->kind == TY_NAMED)
|
|
inner = inner->under;
|
|
if (inner && inner->kind == TY_STRUCT) {
|
|
struct_t = inner;
|
|
viaptr = 1;
|
|
}
|
|
} else if (elemu && elemu->kind == TY_STRUCT) {
|
|
struct_t = elemu;
|
|
}
|
|
if (struct_t) {
|
|
Tfield *f = NULL;
|
|
for (Tfield *fl = struct_t->fields; fl; fl = fl->next)
|
|
if (strcmp(fl->name, n->str) == 0)
|
|
{ f = fl; break; }
|
|
Type *bt = idxbase->type;
|
|
Type *bu = (bt && bt->kind == TY_NAMED)
|
|
? bt->under : bt;
|
|
int is_arr = bu && bu->kind == TY_ARRAY;
|
|
int is_sl = bu && bu->kind == TY_SLICE;
|
|
int is_ptr = bu && bu->kind == TY_PTR;
|
|
int off = localfind(locals, idxbase->str);
|
|
if (f != NULL && (is_arr || is_sl || is_ptr)
|
|
&& off != 0) {
|
|
int esz = (int)elemt->size;
|
|
cgexpr(c, n->lhs->rhs, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz),
|
|
areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX),
|
|
areg(D_AX));
|
|
}
|
|
if (is_arr)
|
|
ins2(c, A_LEAQ,
|
|
amem(D_BP, off), areg(D_BX));
|
|
else
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, off), areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
if (viaptr)
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BX, 0), areg(D_AX));
|
|
else
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_AX));
|
|
int foff = (int)f->offset;
|
|
Type *ft = f->type;
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
if (fu && fu->kind == TY_STR) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, foff + 8),
|
|
areg(D_BX));
|
|
ins2(c, A_MOVQ,
|
|
amem(D_AX, foff + 0),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
int g_isf32 = 0;
|
|
if (fld_isfloat(ft, &g_isf32)) {
|
|
int mov = g_isf32
|
|
? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
amem(D_AX, foff),
|
|
areg(D_X0));
|
|
goto dot_done;
|
|
}
|
|
int fsz = (int)(ft ? ft->size : 8);
|
|
int op = fldloadop(ft, fsz);
|
|
ins2(c, op, amem(D_AX, foff),
|
|
areg(D_AX));
|
|
goto dot_done;
|
|
}
|
|
}
|
|
}
|
|
/* Nested module-qualified field where the chain didn't fold to
|
|
* a known shape (typical when w6c runs on a single file with
|
|
* `use mod;` but no driver concatenation — the body's enum /
|
|
* struct hasn't been seen). Emit `MOVQ <leaf>(SB), AX` so the
|
|
* linker surfaces a clean undefined-symbol error on the leaf
|
|
* — mirrors the bare-N_IDENT unresolved fallback used by
|
|
* single-segment N_DOTs. Keeps cstage / wwstage byte-aligned
|
|
* on the cgen-match isolation probes. */
|
|
if (n->lhs && n->lhs->kind == N_DOT && n->str) {
|
|
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
|
|
break;
|
|
}
|
|
/* fall through to base evaluation; result placeholder */
|
|
cgexpr(c, n->lhs, locals);
|
|
dot_done:
|
|
break;
|
|
}
|
|
case N_INDEX: {
|
|
/* Scaled indexing for slice/array/str/ptr-to-T.
|
|
* Element size is 1 for u8/str, otherwise type's natural size.
|
|
* For `*[N]T` drill through to the array so esz/esub reflect
|
|
* T, not sizeof(array). */
|
|
Type *bt = n->lhs ? n->lhs->type : NULL;
|
|
Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
Type *eff = idx_eff(bt);
|
|
int esz = 1;
|
|
if (eff && eff->sub) esz = (int)eff->sub->size;
|
|
if (u && u->kind == TY_STR) esz = 1;
|
|
Type *esub = eff ? eff->sub : NULL;
|
|
Type *esubu = (esub && esub->kind == TY_NAMED)
|
|
? esub->under : esub;
|
|
int elem_tagged = esubu && esubu->kind == TY_TAGGED;
|
|
|
|
if (n->lhs->kind == N_IDENT && u) {
|
|
int off = localfind(locals, n->lhs->str);
|
|
int isglobal = (off == 0) && let_islet(n->lhs->str);
|
|
cgexpr(c, n->rhs, locals); /* idx → AX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
/* base address into BX. Top-level array → LEAQ
|
|
* name(SB); top-level ptr → MOVQ name(SB) (the symbol
|
|
* holds the pointer); locals route off BP. */
|
|
if (isglobal && u->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, masym(c, n->lhs->str),
|
|
areg(D_BX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ, masym(c, n->lhs->str),
|
|
areg(D_BX));
|
|
} else if (u->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX));
|
|
} else {
|
|
/* slice/str/ptr: ptr field is at off+0 */
|
|
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
/* str element: load (ptr, len) into (AX, BX) so the
|
|
* value flows through the str-rhs convention. */
|
|
if (u->sub && type_isstr(u->sub)) {
|
|
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
|
|
break;
|
|
}
|
|
/* tagged element: load slot words into (AX=tag,
|
|
* DX=val0, CX=val1, R8=val2) — matches the
|
|
* tagged-return ABI so let-init / match / call-arg
|
|
* paths consume it without spilling. Nullable folded
|
|
* element is one word in AX (caller treats it as a
|
|
* pointer). */
|
|
if (elem_tagged) {
|
|
int ssz = (int)esubu->size;
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, amem(D_BX, 24),
|
|
areg(D_R8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, amem(D_BX, 16),
|
|
areg(D_CX));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, amem(D_BX, 8),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
int load_op = fldloadop(esub, esz);
|
|
ins2(c, load_op, amem(D_BX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
/* Fallback: evaluate base (treat as plain pointer) and
|
|
* dereference at base+idx. Pick the load opcode by element
|
|
* size — `b.data[i]` on a *u8 must read 1 byte, not 8.
|
|
*
|
|
* Scale the index in a register before pushing, because
|
|
* IMULQ on a memory operand isn't currently encoded by w6a
|
|
* (modrm bits use mod=3 register form). */
|
|
cgexpr(c, n->rhs, locals);
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, n->lhs, locals);
|
|
ins1(c, A_POPQ, areg(D_BX));
|
|
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
|
|
/* str element: load (ptr, len) into (AX, BX). */
|
|
if (u && u->sub && type_isstr(u->sub)) {
|
|
ins2(c, A_MOVQ, amem(D_AX, 8), areg(D_BX));
|
|
ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
/* tagged element via fallback base: AX holds the element
|
|
* address — copy to BX (the load into AX clobbers it), then
|
|
* load slot words. */
|
|
if (elem_tagged) {
|
|
int ssz = (int)esubu->size;
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
if (ssz > 24)
|
|
ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8));
|
|
if (ssz > 16)
|
|
ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX));
|
|
if (ssz > 8)
|
|
ins2(c, A_MOVQ, amem(D_BX, 8), areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BX, 0), areg(D_AX));
|
|
break;
|
|
}
|
|
{
|
|
int load_op = fldloadop(esub, esz);
|
|
ins2(c, load_op, amem(D_AX, 0), areg(D_AX));
|
|
}
|
|
break;
|
|
}
|
|
case N_SLICE: {
|
|
/* base[lo:hi] as a slice value. Leaves the triple in
|
|
* (AX=base+lo, BX=hi-lo, CX=hi-lo) so callers can route
|
|
* to a slice slot, return, or arg with the same ABI. Cap
|
|
* defaults to the new length — there's no syntax for a
|
|
* larger cap yet. Element scaling on the ptr isn't wired
|
|
* (matches the let-init path), so non-u8 slices need a
|
|
* follow-up audit when fixtures exercise them. */
|
|
Node *base = n->lhs;
|
|
Node *lo = n->rhs;
|
|
Node *hi = n->cond;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
if (base && base->kind == N_IDENT) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) && let_islet(base->str);
|
|
if (isglobal && bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (isglobal) {
|
|
ins2(c, A_MOVQ, masym(c, base->str),
|
|
areg(D_AX));
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
|
|
}
|
|
} else if (base) {
|
|
cgexpr(c, base, locals);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (lo) cgexpr(c, lo, locals);
|
|
else cgexpr_int(c, 0);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
if (hi) {
|
|
cgexpr(c, hi, locals);
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
cgexpr_int(c, (long long)bu->alen);
|
|
} else if (base && base->kind == N_IDENT && bu &&
|
|
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(locals, base->str);
|
|
int isglobal = (boff == 0) && let_islet(base->str);
|
|
if (isglobal) {
|
|
ins2(c, A_LEAQ, masym(c, base->str),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_AX));
|
|
} else {
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8),
|
|
areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
|
|
ins1(c, A_POPQ, areg(D_CX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX));
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_BX));
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
break;
|
|
}
|
|
default:
|
|
cgexpr_int(c, 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
|
{
|
|
if (n == NULL) return;
|
|
switch (n->kind) {
|
|
case N_BLOCK:
|
|
for (Node *s = n->list; s; s = s->next)
|
|
cgstmt(c, s, locals, frame);
|
|
break;
|
|
case N_EXPRSTMT:
|
|
cgexpr(c, n->lhs, *locals);
|
|
break;
|
|
case N_LET: {
|
|
Type *lt = n->type;
|
|
Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt;
|
|
int sz = 8;
|
|
if (lu && lu->kind == TY_ARRAY) sz = (int)lu->size;
|
|
else if (lu && lu->kind == TY_SLICE) sz = 24;
|
|
else if (lu && lu->kind == TY_STR) sz = 16;
|
|
else if (lu && lu->kind == TY_STRUCT) sz = (int)lu->size;
|
|
else if (lu && lu->kind == TY_TUPLE) sz = (int)lu->size;
|
|
else if (lu && lu->kind == TY_TAGGED) sz = (int)lu->size;
|
|
int off = localoff(c, locals, n->str, sz, frame);
|
|
int isf = cg_isfloat(lt);
|
|
int isf32 = type_isf32(lt);
|
|
/* alloc([], n) initialiser for a slice local: allocate
|
|
* n*esize bytes, build the {ptr, 0, n} header in the slot.
|
|
* Element size comes from the declared slice type. */
|
|
if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24
|
|
&& n->rhs->kind == N_CALL && n->rhs->lhs
|
|
&& n->rhs->lhs->kind == N_IDENT
|
|
&& strcmp(n->rhs->lhs->str, "alloc") == 0
|
|
&& n->rhs->list && n->rhs->list->kind == N_ARRLIT
|
|
&& n->rhs->list->list == NULL
|
|
&& n->rhs->list->next && n->rhs->list->next->next == NULL) {
|
|
Node *count = n->rhs->list->next;
|
|
int esz = (lu->sub) ? (int)lu->sub->size : 1;
|
|
cgexpr(c, count, *locals); /* AX = n */
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* save count */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_BX));
|
|
ins2(c, A_IMULQ, areg(D_BX), areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DI));
|
|
ins1(c, A_CALL, asym(ffi_resolve("alloc")));
|
|
ins1(c, A_POPQ, areg(D_BX)); /* count */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, aimm(0), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
/* str initialiser: cgexpr produces (AX=ptr, BX=len). */
|
|
if (n->rhs && type_isstr(lt) && sz == 16) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
break;
|
|
}
|
|
/* 2-tuple initialiser from a function call: SysV returns
|
|
* a 16-byte aggregate in (AX, DX). Store both into the
|
|
* tuple slot. */
|
|
if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 16) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8));
|
|
break;
|
|
}
|
|
/* 24B tuple initialiser for `(scalar, str)` / `(str, scalar)`.
|
|
* Per the AX:DX:CX return convention: AX = scalar elem,
|
|
* DX = str.ptr, CX = str.len. The slot is laid out positionally
|
|
* (e0 at +0, e1 at +8 for scalars; str takes 16B starting at
|
|
* its position), so we route each register to the slot dictated
|
|
* by the element's type, not by AX/DX position. */
|
|
if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 24) {
|
|
Tparam *p0 = lu->params;
|
|
Tparam *p1 = p0 ? p0->next : NULL;
|
|
Type *t0 = p0 ? p0->type : NULL;
|
|
Type *t1 = p1 ? p1->type : NULL;
|
|
Type *u0 = (t0 && t0->kind == TY_NAMED) ? t0->under : t0;
|
|
Type *u1 = (t1 && t1->kind == TY_NAMED) ? t1->under : t1;
|
|
int e0_str = u0 && u0->kind == TY_STR;
|
|
int e1_str = u1 && u1->kind == TY_STR;
|
|
if (e0_str ^ e1_str) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
if (e0_str) {
|
|
/* layout: str@+0 (16B), scalar@+16. */
|
|
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 16));
|
|
} else {
|
|
/* layout: scalar@+0 (8B), str@+8 (16B). */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
/* Tagged-union initialiser. Delegates to cg_widen_tagged_store,
|
|
* which handles nullable fold, tagged→tagged (with tag remap
|
|
* when variant indices differ), struct payload (ident or
|
|
* literal — field-by-field at slot+8+field_off), str payload,
|
|
* and scalar payload (with zero-pad to the slot size). */
|
|
if (n->rhs && lu && lu->kind == TY_TAGGED) {
|
|
cg_widen_tagged_store(c, locals, lu, n->rhs, D_BP, off, sz);
|
|
break;
|
|
}
|
|
/* slice expression initialiser: build a {ptr, len, cap} header
|
|
* referring to the source. Element size assumed to be 1
|
|
* (u8) for now; real element-size scaling is a future TODO. */
|
|
if (n->rhs && n->rhs->kind == N_SLICE && lu
|
|
&& lu->kind == TY_SLICE) {
|
|
Node *base = n->rhs->lhs;
|
|
Node *lo = n->rhs->rhs;
|
|
Node *hi = n->rhs->cond;
|
|
Type *bt = base ? base->type : NULL;
|
|
Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt;
|
|
/* load base address */
|
|
if (base->kind == N_IDENT) {
|
|
int boff = localfind(*locals, base->str);
|
|
if (bu && bu->kind == TY_ARRAY) {
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_AX));
|
|
} else {
|
|
/* slice/str/ptr: load .ptr */
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_AX));
|
|
}
|
|
} else {
|
|
cgexpr(c, base, *locals);
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* save base addr */
|
|
/* lo (default 0) */
|
|
if (lo) cgexpr(c, lo, *locals);
|
|
else cgexpr_int(c, 0);
|
|
ins1(c, A_PUSHQ, areg(D_AX)); /* save lo */
|
|
/* hi (default base length) */
|
|
if (hi) {
|
|
cgexpr(c, hi, *locals);
|
|
} else if (bu && bu->kind == TY_ARRAY) {
|
|
cgexpr_int(c, (long long)bu->alen);
|
|
} else if (base->kind == N_IDENT && bu &&
|
|
(bu->kind == TY_SLICE || bu->kind == TY_STR)) {
|
|
int boff = localfind(*locals, base->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX));
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); /* BX = hi */
|
|
ins1(c, A_POPQ, areg(D_CX)); /* CX = lo */
|
|
ins1(c, A_POPQ, areg(D_AX)); /* AX = base */
|
|
ins2(c, A_ADDQ, areg(D_CX), areg(D_AX)); /* AX = base+lo */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_SUBQ, areg(D_CX), areg(D_BX)); /* BX = hi-lo */
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
/* Generic slice rhs (e.g. fn returning []u8, slice ident,
|
|
* slice-typed param). cgexpr leaves (AX=ptr, BX=len, CX=
|
|
* cap); store all three into the local slot. Runs after
|
|
* the alloc and N_SLICE specialisations above so they keep
|
|
* their direct-store shape. */
|
|
if (n->rhs && lu && lu->kind == TY_SLICE && sz == 24) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
|
|
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
|
|
break;
|
|
}
|
|
/* struct literal initialiser: field-by-field store. The
|
|
* literal carries op == TK_ELLIPSIS when the source ends in
|
|
* `..., ...` — in that case zero-fill the entire slot first,
|
|
* so unmentioned fields read as 0. */
|
|
if (n->rhs && n->rhs->kind == N_STRUCTLIT && lu
|
|
&& lu->kind == TY_STRUCT) {
|
|
if (n->rhs->op == TK_ELLIPSIS) {
|
|
u64 sz = lu->size;
|
|
/* AX = 0 once, then store from AX. w6a doesn't
|
|
* accept MOVB imm,mem — use register stores. */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
u64 i = 0;
|
|
while (i + 8 <= sz) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + (int)i));
|
|
i += 8;
|
|
}
|
|
while (i + 4 <= sz) {
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BP, off + (int)i));
|
|
i += 4;
|
|
}
|
|
while (i < sz) {
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BP, off + (int)i));
|
|
i += 1;
|
|
}
|
|
}
|
|
for (Node *f = n->rhs->list; f; f = f->next) {
|
|
/* find offset of this field */
|
|
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;
|
|
}
|
|
}
|
|
/* Tagged-union field: delegate to the shared
|
|
* widening writer. Handles str, scalar, struct
|
|
* literal/ident payload, and tagged-subset
|
|
* forwarding (with tag remap). The field's slot
|
|
* starts at off+foff inside the struct slot. */
|
|
Type *fu = (ft && ft->kind == TY_NAMED)
|
|
? ft->under : ft;
|
|
if (fu && fu->kind == TY_TAGGED) {
|
|
cg_widen_tagged_store(c, locals, fu,
|
|
f->lhs, D_BP, off + (int)foff,
|
|
(int)fu->size);
|
|
continue;
|
|
}
|
|
cgexpr(c, f->lhs, *locals);
|
|
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(D_BP, off + (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(D_BP, off + (int)foff));
|
|
}
|
|
break;
|
|
}
|
|
/* Whole-struct receive for sizes <=24B (call-result rhs).
|
|
* Counterpart of #4's cgreturn ABI: cgexpr leaves
|
|
* AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23], zero-
|
|
* padded to 24B by the producer.
|
|
*
|
|
* ASYMMETRY (do NOT mirror the sender): producer emits three
|
|
* uniform MOVQs into a zero-padded 24B scratch slot; the
|
|
* receiver must write only `sz` bytes — MOVQ for full 8B
|
|
* chunks plus a sized tail (MOVL/MOVW/MOVB) by the *declared*
|
|
* struct size. Otherwise a trailing 1..7-byte chunk would
|
|
* overrun into the next local slot.
|
|
*
|
|
* Tail chunks in {3,5,6,7} (would need shift-and-store from
|
|
* the register) are unreachable under WW struct alignment
|
|
* rules (field aligns force size%align==0); the guard
|
|
* excludes them so they fall through to the existing scalar
|
|
* path rather than emit a stomping MOVQ tail. Sizes >24B also
|
|
* fall through (sret deferred, same constraint as #4). */
|
|
if (n->rhs && n->rhs->kind == N_CALL && lu
|
|
&& lu->kind == TY_STRUCT && sz <= 24
|
|
&& (sz % 8 == 0 || sz % 8 == 1
|
|
|| sz % 8 == 2 || sz % 8 == 4)) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
int regs[3] = { D_AX, D_DX, D_CX };
|
|
int full = sz / 8;
|
|
int tail = sz % 8;
|
|
for (int i = 0; i < full; i++)
|
|
ins2(c, A_MOVQ, areg(regs[i]),
|
|
amem(D_BP, off + i * 8));
|
|
if (tail > 0) {
|
|
int op = (tail == 4) ? A_MOVL
|
|
: (tail == 2) ? A_MOVW : A_MOVB;
|
|
ins2(c, op, areg(regs[full]),
|
|
amem(D_BP, off + full * 8));
|
|
}
|
|
break;
|
|
}
|
|
/* array literal initialiser: `let xs: [N]T = [a, b, c];`.
|
|
* Walk elements in declaration order, store each at off + i*esz
|
|
* using the right width for the element type. The trailing
|
|
* `...` repeat marker (an N_FIELD with str=="...") fills the
|
|
* remaining slots with the last value. */
|
|
if (n->rhs && n->rhs->kind == N_ARRLIT && lu
|
|
&& lu->kind == TY_ARRAY) {
|
|
int esz = lu->sub ? (int)lu->sub->size : 1;
|
|
int op = A_MOVQ;
|
|
if (esz == 1) op = A_MOVB;
|
|
else if (esz == 4) op = A_MOVL;
|
|
/* esz == 2 (i16/u16) falls through to MOVQ — over-writes
|
|
* by 6B; the next element store rewrites the high half.
|
|
* For the last element this trails 6 bytes into the next
|
|
* stack slot. Add MOVW to w6a if real i16 arrays land. */
|
|
int idx = 0;
|
|
Node *last = NULL;
|
|
int repeat = 0;
|
|
for (Node *e = n->rhs->list; e; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str &&
|
|
strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
cgexpr(c, e, *locals);
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, off + idx * esz));
|
|
last = e;
|
|
idx++;
|
|
}
|
|
if (repeat && last) {
|
|
/* fill remaining slots with the value already in AX. */
|
|
while (idx < (int)lu->alen) {
|
|
ins2(c, op, areg(D_AX),
|
|
amem(D_BP, off + idx * esz));
|
|
idx++;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (n->rhs && sz == 8) {
|
|
cgexpr(c, n->rhs, *locals);
|
|
if (isf) {
|
|
int mov = isf32 ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov, areg(D_X0), amem(D_BP, off));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
|
|
}
|
|
} else if (sz == 8) {
|
|
ins2(c, A_MOVQ, aimm(0), amem(D_BP, off));
|
|
} else if (!n->rhs && sz > 8 && lu && lu->kind != TY_ARRAY) {
|
|
/* `let x: T;` with no rhs for a multi-word composite
|
|
* (str/slice/tuple/struct/tagged). Zero the slot so
|
|
* reads after the bare let see {0...} rather than
|
|
* whatever the stack already held. Arrays keep the
|
|
* per-index-write contract — leave them uninit. */
|
|
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
|
|
int zi = 0;
|
|
while (zi + 8 <= sz) {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + zi));
|
|
zi += 8;
|
|
}
|
|
while (zi + 4 <= sz) {
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BP, off + zi));
|
|
zi += 4;
|
|
}
|
|
while (zi < sz) {
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BP, off + zi));
|
|
zi += 1;
|
|
}
|
|
}
|
|
/* arrays left uninitialised — caller writes via index */
|
|
break;
|
|
}
|
|
case N_RETURN:
|
|
/* run all defers in reverse before the actual return */
|
|
for (int di = ndefers - 1; di >= 0; di--)
|
|
cgexpr(c, defers[di], *locals);
|
|
/* If the function returns a tagged union and the value is
|
|
* one of the variant types, wrap into (tag, value). If rhs
|
|
* already produces a tagged union (e.g. forwarding another
|
|
* fallible call), pass it through unchanged.
|
|
*
|
|
* Tagged-return ABI: AX=tag, DX=value0[, CX=value1]. CX is
|
|
* only meaningful when the union has a >8B variant (e.g.
|
|
* str, where ptr→DX and len→CX).
|
|
*
|
|
* Bare `return;` from a tagged-union-returning function: this
|
|
* is producing the void variant. Emit its tag; the payload is
|
|
* undefined (void has size 0). */
|
|
if (n->lhs == NULL && cg_ret_type) {
|
|
Type *rt = cg_ret_type;
|
|
if (rt->kind == TY_NAMED) rt = rt->under;
|
|
if (rt && rt->kind == TY_TAGGED) {
|
|
if (rt->nullable) {
|
|
/* bare `return;` is the void/null
|
|
* variant: emit AX = 0. */
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
} else {
|
|
int tag = cg_tag_for_variant(rt, ty_void);
|
|
if (tag < 0) tag = 0;
|
|
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
if (n->lhs && cg_ret_type) {
|
|
Type *rt = cg_ret_type;
|
|
if (rt->kind == TY_NAMED) rt = rt->under;
|
|
if (rt && rt->kind == TY_TAGGED) {
|
|
Type *vt = n->lhs->type;
|
|
Type *vu = (vt && vt->kind == TY_NAMED)
|
|
? vt->under : vt;
|
|
int istagged = vu && vu->kind == TY_TAGGED;
|
|
int passthrough = istagged && (vu == rt ||
|
|
type_eq(vt, cg_ret_type));
|
|
int isstruct = vu && vu->kind == TY_STRUCT;
|
|
if (rt->nullable) {
|
|
cgexpr(c, n->lhs, *locals);
|
|
} else if (passthrough) {
|
|
/* same tagged type: forward AX/DX/CX. */
|
|
cgexpr(c, n->lhs, *locals);
|
|
} else if (!istagged && !isstruct) {
|
|
/* str / slice / scalar variant: synthesise
|
|
* the tag in AX and shuffle the value into
|
|
* DX[/CX[/R8]]. Direct register path keeps
|
|
* the asm short — no scratch slot.
|
|
* Tagged-return ABI: AX=tag, DX=word0,
|
|
* CX=word1, R8=word2. Slice payload uses
|
|
* all four; str uses three; scalar uses
|
|
* two. */
|
|
int tag = cg_tag_for_variant(rt, vt);
|
|
cgexpr(c, n->lhs, *locals);
|
|
if (type_isslice(vt)) {
|
|
/* cgexpr leaves (AX=ptr, BX=len,
|
|
* CX=cap). Move into the return
|
|
* shuffle: DX=ptr, CX=len, R8=cap. */
|
|
ins2(c, A_MOVQ, areg(D_CX),
|
|
areg(D_R8));
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
} else if (type_isstr(vt)) {
|
|
ins2(c, A_MOVQ, areg(D_BX),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
} else {
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
areg(D_DX));
|
|
}
|
|
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). */
|
|
int sz = (int)rt->size;
|
|
const char *scrn = mklabel(c, "retscr");
|
|
int scr = local_alloc(c, locals, scrn,
|
|
sz, cg_frame);
|
|
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;
|
|
}
|
|
}
|
|
/* Whole-struct return for sizes ≤24B. ABI: AX=bytes[0..7],
|
|
* DX=bytes[8..15], CX=bytes[16..23]. Sizes >24B are not
|
|
* wired (sret deferred); they fall through to the scalar
|
|
* path below and return only AX. Materialise rhs into a
|
|
* zero-padded 24B scratch slot, then emit AX/DX/CX loads
|
|
* unconditionally so the instruction shape is constant
|
|
* regardless of declared struct size. The receive side
|
|
* masks via the dst slot's declared size. Two rhs shapes
|
|
* are wired: N_IDENT (word-copy from rhs local slot) and
|
|
* N_STRUCTLIT (field-by-field store at scratch+foff). Call-
|
|
* result chain return is deferred to #5's receive side. */
|
|
if (n->lhs && cg_ret_type) {
|
|
Type *rt = cg_ret_type;
|
|
if (rt->kind == TY_NAMED) rt = rt->under;
|
|
if (rt && rt->kind == TY_STRUCT && rt->size <= 24
|
|
&& (n->lhs->kind == N_IDENT
|
|
|| n->lhs->kind == N_STRUCTLIT)) {
|
|
int sz = (int)rt->size;
|
|
const char *scrn = mklabel(c, "retscr");
|
|
int scr = local_alloc(c, locals, scrn, 24,
|
|
cg_frame);
|
|
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) {
|
|
for (Node *f = n->lhs->list; f;
|
|
f = f->next) {
|
|
u64 foff = 0;
|
|
int fsz = 8;
|
|
Type *ft = NULL;
|
|
for (Tfield *fl = rt->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) {
|
|
cg_widen_tagged_store(c,
|
|
locals, fu, f->lhs,
|
|
D_BP,
|
|
scr + (int)foff,
|
|
(int)fu->size);
|
|
continue;
|
|
}
|
|
cgexpr(c, f->lhs, *locals);
|
|
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(D_BP,
|
|
scr + (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(D_BP,
|
|
scr + (int)foff));
|
|
}
|
|
} else {
|
|
/* N_IDENT: word-copy rhs slot into
|
|
* scratch. Whole 8B words via MOVQ;
|
|
* trailing partial word via MOVL/MOVB
|
|
* so we read no further than the
|
|
* source slot's declared size. */
|
|
int rhsoff = localfind(*locals,
|
|
n->lhs->str);
|
|
int k = 0;
|
|
while (k + 8 <= sz) {
|
|
ins2(c, A_MOVQ,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 8;
|
|
}
|
|
while (k + 4 <= sz) {
|
|
ins2(c, A_MOVL,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVL, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 4;
|
|
}
|
|
while (k < sz) {
|
|
ins2(c, A_MOVB,
|
|
amem(D_BP, rhsoff + k),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVB, areg(D_AX),
|
|
amem(D_BP, scr + k));
|
|
k += 1;
|
|
}
|
|
}
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 0),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 8),
|
|
areg(D_DX));
|
|
ins2(c, A_MOVQ, amem(D_BP, scr + 16),
|
|
areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
}
|
|
if (n->lhs && node_isstr(n->lhs)) {
|
|
cgexpr(c, n->lhs, *locals); /* AX=ptr, BX=len */
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_DX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
}
|
|
if (n->lhs && n->lhs->kind == N_TUPLE) {
|
|
/* 2-tuple ABI:
|
|
* (scalar, scalar) — AX = e0, DX = e1. (16B, fits SysV.)
|
|
* (scalar, str) — AX = scalar elem,
|
|
* DX = str.ptr, CX = str.len. (24B custom.)
|
|
* (str, scalar) — same regs, type-keyed not position-keyed.
|
|
*
|
|
* The 24B convention mirrors the existing tagged-union return
|
|
* (AX:DX:CX); receive sites destructure off the same regs. */
|
|
Node *e0 = n->lhs->list;
|
|
Node *e1 = e0 ? e0->next : NULL;
|
|
if (e1 && e1->next == NULL) {
|
|
int e0_is_str = node_isstr(e0);
|
|
int e1_is_str = node_isstr(e1);
|
|
if (e0_is_str ^ e1_is_str) {
|
|
Node *strn = e0_is_str ? e0 : e1;
|
|
Node *scaln = e0_is_str ? e1 : e0;
|
|
cgexpr(c, scaln, *locals); /* AX = scalar */
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, strn, *locals); /* AX=ptr, BX=len */
|
|
ins2(c, A_MOVQ, areg(D_BX), areg(D_CX));
|
|
ins2(c, A_MOVQ, areg(D_AX), areg(D_DX));
|
|
ins1(c, A_POPQ, areg(D_AX));
|
|
} else {
|
|
cgexpr(c, e1, *locals);
|
|
ins1(c, A_PUSHQ, areg(D_AX));
|
|
cgexpr(c, e0, *locals);
|
|
ins1(c, A_POPQ, areg(D_DX));
|
|
}
|
|
} else {
|
|
/* >2-tuple not yet implemented; fall back to first elem */
|
|
if (e0) cgexpr(c, e0, *locals);
|
|
else cgexpr_int(c, 0);
|
|
}
|
|
} else if (n->lhs) {
|
|
cgexpr(c, n->lhs, *locals);
|
|
} else {
|
|
cgexpr_int(c, 0);
|
|
}
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
break;
|
|
case N_IF: {
|
|
char *els = mklabel(c, "else");
|
|
char *end = mklabel(c, "end");
|
|
cgexpr(c, n->cond, *locals);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JE, abranch(n->els ? els : end));
|
|
cgstmt(c, n->body, locals, frame);
|
|
if (n->els) {
|
|
ins1(c, A_JMP, abranch(end));
|
|
label(c, els);
|
|
cgstmt(c, n->els, locals, frame);
|
|
}
|
|
label(c, end);
|
|
break;
|
|
}
|
|
case N_FORRANGE: {
|
|
/* Lower `for (let x .. s) body` (and its tuple-destructure
|
|
* cousin `for (let (a, b) .. s)`). The body is wrapped in a
|
|
* counted loop driven by stack-spilled `_i`/`_len`. Each
|
|
* iteration computes the element address `s.ptr + i*esz`
|
|
* and either loads the whole element into the named local
|
|
* or pulls each tuple field into its respective local. */
|
|
Node *slc = n->lhs;
|
|
Type *st = slc ? slc->type : NULL;
|
|
Type *u = (st && st->kind == TY_NAMED) ? st->under : st;
|
|
int esz = (u && u->sub) ? (int)u->sub->size : 1;
|
|
Type *etu = (u && u->sub && u->sub->kind == TY_NAMED)
|
|
? u->sub->under : (u ? u->sub : NULL);
|
|
int destruct = (n->list != NULL);
|
|
|
|
/* allocate temp slots: _i (8B), _len (8B) */
|
|
char *iname = aprintf(c->a, ".rgi_%d", c->labelseq++);
|
|
char *lname = aprintf(c->a, ".rgl_%d", c->labelseq++);
|
|
int ioff = localoff(c, locals, iname, 8, frame);
|
|
int loff = localoff(c, locals, lname, 8, frame);
|
|
|
|
/* allocate per-name slots */
|
|
struct { int off, sz, foff; Type *ftype; } binds[8] = {0};
|
|
int nbinds = 0;
|
|
if (destruct) {
|
|
Tparam *tp = (etu && etu->kind == TY_TUPLE) ?
|
|
etu->params : NULL;
|
|
int field_off = 0;
|
|
for (Node *nm = n->list; nm && nbinds < 8; nm = nm->next) {
|
|
int fsz = tp && tp->type ? (int)tp->type->size : 8;
|
|
int slot_sz = (fsz < 8) ? 8 : fsz;
|
|
binds[nbinds].sz = fsz;
|
|
binds[nbinds].foff = field_off;
|
|
binds[nbinds].ftype = tp ? tp->type : NULL;
|
|
binds[nbinds].off = localoff(c, locals,
|
|
nm->str, slot_sz, frame);
|
|
field_off += fsz;
|
|
nbinds++;
|
|
if (tp) tp = tp->next;
|
|
}
|
|
} else {
|
|
int slot_sz = (esz < 8) ? 8 : esz;
|
|
binds[0].off = localoff(c, locals, n->str, slot_sz, frame);
|
|
binds[0].sz = esz;
|
|
binds[0].foff = 0;
|
|
binds[0].ftype = u ? u->sub : NULL;
|
|
nbinds = 1;
|
|
}
|
|
|
|
ins2(c, A_MOVQ, aimm(0), amem(D_BP, ioff));
|
|
if (u && (u->kind == TY_SLICE || u->kind == TY_STR)
|
|
&& slc->kind == N_IDENT) {
|
|
int boff = localfind(*locals, slc->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, boff + 8), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff));
|
|
} else if (u && u->kind == TY_ARRAY) {
|
|
ins2(c, A_MOVQ, aimm((long long)u->alen),
|
|
amem(D_BP, loff));
|
|
} else {
|
|
cgexpr(c, slc, *locals);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, loff));
|
|
}
|
|
|
|
char *loop = mklabel(c, "rloop");
|
|
char *end = mklabel(c, "rend");
|
|
char *natural_exit = end;
|
|
if (n->els) natural_exit = mklabel(c, "relseloop");
|
|
if (nloops < LOOP_MAX) {
|
|
loop_cont[nloops] = loop;
|
|
loop_brk[nloops] = end;
|
|
nloops++;
|
|
}
|
|
label(c, loop);
|
|
ins2(c, A_MOVQ, amem(D_BP, ioff), areg(D_AX));
|
|
ins2(c, A_MOVQ, amem(D_BP, loff), areg(D_BX));
|
|
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
|
|
ins1(c, A_JGE, abranch(natural_exit));
|
|
/* compute element base: s.ptr + i*esz → BX */
|
|
if (esz > 1) {
|
|
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
|
|
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
|
|
}
|
|
if (slc->kind == N_IDENT && u && u->kind == TY_ARRAY) {
|
|
int boff = localfind(*locals, slc->str);
|
|
ins2(c, A_LEAQ, amem(D_BP, boff), areg(D_BX));
|
|
} else if (slc->kind == N_IDENT) {
|
|
int boff = localfind(*locals, slc->str);
|
|
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
|
|
}
|
|
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
|
|
/* load each binding from BX + foff into its slot */
|
|
for (int b = 0; b < nbinds; b++) {
|
|
int op = fldloadop(binds[b].ftype, binds[b].sz);
|
|
ins2(c, op, amem(D_BX, binds[b].foff), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, binds[b].off));
|
|
}
|
|
cgstmt(c, n->body, locals, frame);
|
|
ins2(c, A_ADDQ, aimm(1), amem(D_BP, ioff));
|
|
ins1(c, A_JMP, abranch(loop));
|
|
if (n->els) {
|
|
label(c, natural_exit);
|
|
cgstmt(c, n->els, locals, frame);
|
|
}
|
|
label(c, end);
|
|
if (nloops > 0) nloops--;
|
|
break;
|
|
}
|
|
case N_FOR: {
|
|
char *loop = mklabel(c, "loop");
|
|
char *end = mklabel(c, "endloop");
|
|
/* `else` runs at normal cond-false exit; break skips it.
|
|
* Separate the natural exit label from the break target so
|
|
* the else block sits between them. */
|
|
char *natural_exit = end;
|
|
if (n->els) natural_exit = mklabel(c, "elseloop");
|
|
if (n->lhs) cgstmt(c, n->lhs, locals, frame);
|
|
label(c, loop);
|
|
if (n->cond) {
|
|
cgexpr(c, n->cond, *locals);
|
|
ins2(c, A_CMPQ, aimm(0), areg(D_AX));
|
|
ins1(c, A_JE, abranch(natural_exit));
|
|
}
|
|
if (nloops < LOOP_MAX) {
|
|
loop_cont[nloops] = loop;
|
|
loop_brk[nloops] = end;
|
|
nloops++;
|
|
}
|
|
cgstmt(c, n->body, locals, frame);
|
|
if (nloops > 0) nloops--;
|
|
if (n->rhs) cgexpr(c, n->rhs, *locals);
|
|
ins1(c, A_JMP, abranch(loop));
|
|
if (n->els) {
|
|
label(c, natural_exit);
|
|
cgstmt(c, n->els, locals, frame);
|
|
}
|
|
label(c, end);
|
|
break;
|
|
}
|
|
case N_MLET: {
|
|
/* eval rhs; consume the per-type return-ABI registers.
|
|
* (scalar, scalar) — AX → l0, DX → l1.
|
|
* (scalar, str) — AX → scalar slot, (DX, CX) → str slot
|
|
* as (.ptr, .len). Position-agnostic.
|
|
* Local sizing comes from each l->type so the str slot gets
|
|
* the full 16B; without this, only DX would land and the
|
|
* len half (CX) would have nowhere to go. */
|
|
cgexpr(c, n->rhs, *locals);
|
|
Node *l0 = n->list;
|
|
Node *l1 = l0 ? l0->next : NULL;
|
|
Type *t0 = l0 ? l0->type : NULL;
|
|
Type *t1 = l1 ? l1->type : NULL;
|
|
Type *u0 = (t0 && t0->kind == TY_NAMED) ? t0->under : t0;
|
|
Type *u1 = (t1 && t1->kind == TY_NAMED) ? t1->under : t1;
|
|
int s0_is_str = u0 && u0->kind == TY_STR;
|
|
int s1_is_str = u1 && u1->kind == TY_STR;
|
|
if (l0 && l1 && (s0_is_str ^ s1_is_str)) {
|
|
int sz0 = s0_is_str ? 16 : 8;
|
|
int sz1 = s1_is_str ? 16 : 8;
|
|
int off0 = localoff(c, locals, l0->str, sz0, frame);
|
|
int off1 = localoff(c, locals, l1->str, sz1, frame);
|
|
if (s0_is_str) {
|
|
/* l0 is str: ptr=DX, len=CX. l1 is scalar: l1 = AX. */
|
|
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off0 + 0));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off0 + 8));
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off1));
|
|
} else {
|
|
/* l0 is scalar; l1 is str. */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off0));
|
|
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off1 + 0));
|
|
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off1 + 8));
|
|
}
|
|
break;
|
|
}
|
|
ins1(c, A_PUSHQ, areg(D_DX)); /* save 2nd while we store 1st */
|
|
if (l0) {
|
|
int off = localoff(c, locals, l0->str, 8, frame);
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_DX));
|
|
if (l1) {
|
|
int off = localoff(c, locals, l1->str, 8, frame);
|
|
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off));
|
|
}
|
|
break;
|
|
}
|
|
case N_MASSIGN: {
|
|
cgexpr(c, n->rhs, *locals);
|
|
ins1(c, A_PUSHQ, areg(D_DX));
|
|
Node *l0 = n->list;
|
|
Node *l1 = l0 ? l0->next : NULL;
|
|
if (l0 && l0->kind == N_IDENT) {
|
|
int off = localfind(*locals, l0->str);
|
|
if (off != 0)
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));
|
|
}
|
|
ins1(c, A_POPQ, areg(D_DX));
|
|
if (l1 && l1->kind == N_IDENT) {
|
|
int off = localfind(*locals, l1->str);
|
|
if (off != 0)
|
|
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off));
|
|
}
|
|
break;
|
|
}
|
|
case N_DEFER:
|
|
if (ndefers < DEFER_MAX) {
|
|
defers[ndefers++] = n->lhs;
|
|
}
|
|
break;
|
|
case N_YIELD:
|
|
/* Evaluate the value into AX, then jump to the enclosing
|
|
* match's end label. str-typed yields land in (AX, BX);
|
|
* the consumer's let-init or call-arg site reads both. */
|
|
if (n->lhs) cgexpr(c, n->lhs, *locals);
|
|
if (nyields > 0)
|
|
ins1(c, A_JMP, abranch(yield_target[nyields - 1]));
|
|
break;
|
|
case N_BREAK:
|
|
if (nloops > 0)
|
|
ins1(c, A_JMP, abranch(loop_brk[nloops - 1]));
|
|
break;
|
|
case N_CONTINUE:
|
|
if (nloops > 0)
|
|
ins1(c, A_JMP, abranch(loop_cont[nloops - 1]));
|
|
break;
|
|
case N_SWITCH: {
|
|
/* Lower to a chain of compares. Scrutinee lands in a fresh
|
|
* local slot so case bodies can spill through SP without
|
|
* losing it. Cases are tried top-to-bottom; a `case:` arm
|
|
* with no exprs is the default and runs after all named
|
|
* arms fail. */
|
|
char *swname = aprintf(c->a, ".sw_%d", c->labelseq++);
|
|
int sloff = localoff(c, locals, swname, 8, frame);
|
|
cgexpr(c, n->lhs, *locals); /* AX = scrutinee */
|
|
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sloff));
|
|
char *end = mklabel(c, "swend");
|
|
Node *defcase = NULL;
|
|
for (Node *cs = n->list; cs; cs = cs->next) {
|
|
if (cs->list == NULL) {
|
|
defcase = cs; /* save for last */
|
|
continue;
|
|
}
|
|
char *body = mklabel(c, "swcase");
|
|
char *next = mklabel(c, "swnext");
|
|
for (Node *e = cs->list; e; e = e->next) {
|
|
cgexpr(c, e, *locals); /* AX = case-expr */
|
|
ins2(c, A_MOVQ, amem(D_BP, sloff), areg(D_BX));
|
|
ins2(c, A_CMPQ, areg(D_BX), areg(D_AX));
|
|
ins1(c, A_JE, abranch(body));
|
|
}
|
|
ins1(c, A_JMP, abranch(next));
|
|
label(c, body);
|
|
cgstmt(c, cs->body, locals, frame);
|
|
ins1(c, A_JMP, abranch(end));
|
|
label(c, next);
|
|
}
|
|
if (defcase)
|
|
cgstmt(c, defcase->body, locals, frame);
|
|
label(c, end);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
cgfn(Cg *c, FILE *out, Node *fn)
|
|
{
|
|
if (fn->body == NULL) return; /* extern decl, no body */
|
|
|
|
/* fresh per-fn state */
|
|
c->head = c->tail = NULL;
|
|
c->fnname = fn->str;
|
|
c->labelseq = 0;
|
|
cg_stack_arg_cursor = 0;
|
|
ndefers = 0;
|
|
nloops = 0;
|
|
cg_ret_type = fn->type ? fn->type->ret : NULL;
|
|
|
|
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 for non-exported, non-FFI decls; mod_mangle
|
|
* does the FFI/module lookup in one step. */
|
|
text->to = masym(c, fn->str);
|
|
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);
|
|
|
|
/* 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 = 0, fargi = 0;
|
|
Tparam *tp = fn->type ? fn->type->params : NULL;
|
|
for (Node *p = fn->list; p; p = p->next) {
|
|
if (p->str == NULL || strcmp(p->str, "...") == 0) {
|
|
if (tp) tp = tp->next;
|
|
continue;
|
|
}
|
|
Type *pt = tp ? tp->type : NULL;
|
|
Type *pu = (pt && pt->kind == TY_NAMED) ? pt->under : pt;
|
|
int slice = (pu && pu->kind == TY_SLICE);
|
|
int is_str = type_isstr(pt);
|
|
int is_struct = pu && pu->kind == TY_STRUCT && pu->size <= 16;
|
|
int tagged_sz = tagged_arg_size(pt);
|
|
int is_tagged = tagged_sz > 0;
|
|
int isf = cg_isfloat(pt);
|
|
|
|
/* Args overflowing register classes live at positive offsets
|
|
* from BP (16 + i*8). We register them as Locals at those
|
|
* offsets, no spill needed. */
|
|
int struct_eb = is_struct ? ((pu->size > 8) ? 2 : 1) : 0;
|
|
int tagged_eb = is_tagged ? (tagged_sz / 8) : 0;
|
|
int eightbytes = slice ? 3 :
|
|
(is_str ? 2 :
|
|
(is_struct ? struct_eb :
|
|
(is_tagged ? tagged_eb : 1)));
|
|
int regs_left = isf ? (8 - fargi) : (6 - argi);
|
|
if (regs_left >= eightbytes) {
|
|
int sz = slice ? 24 : (is_str ? 16 :
|
|
(is_struct ? (int)pu->size :
|
|
(is_tagged ? tagged_sz : 8)));
|
|
int off = localoff(c, &locals, p->str, sz, &frame);
|
|
if (slice || is_str || is_struct || is_tagged) {
|
|
for (int k = 0; k < eightbytes; k++, argi++)
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off + k * 8));
|
|
} else if (isf) {
|
|
int mov = type_isf32(pt) ? A_MOVSS : A_MOVSD;
|
|
ins2(c, mov,
|
|
areg(sysv_fargregs[fargi]),
|
|
amem(D_BP, off));
|
|
fargi++;
|
|
} else {
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off));
|
|
argi++;
|
|
}
|
|
} else if (eightbytes > 1 && regs_left > 0 &&
|
|
(slice || is_str || is_struct || is_tagged)) {
|
|
/* Multi-word arg that partially fits in regs: caller
|
|
* filled (regs_left) registers greedily, the rest spilled
|
|
* to stack at positive BP offsets. Stitch a single local
|
|
* slot from both sources so the body sees a contiguous
|
|
* value. Mirrors the SysV greedy reg fill the caller
|
|
* does. */
|
|
int sz = slice ? 24 : (is_str ? 16 :
|
|
(is_struct ? (int)pu->size :
|
|
(is_tagged ? tagged_sz : 8)));
|
|
int off = localoff(c, &locals, p->str, sz, &frame);
|
|
extern int cg_stack_arg_cursor;
|
|
int k = 0;
|
|
for (; k < regs_left; k++, argi++)
|
|
ins2(c, A_MOVQ,
|
|
areg(sysv_argregs[argi]),
|
|
amem(D_BP, off + k * 8));
|
|
for (; k < eightbytes; k++) {
|
|
int stack_off = 16 +
|
|
cg_stack_arg_cursor * 8;
|
|
cg_stack_arg_cursor++;
|
|
ins2(c, A_MOVQ, amem(D_BP, stack_off),
|
|
areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_AX),
|
|
amem(D_BP, off + k * 8));
|
|
}
|
|
} else {
|
|
/* stack-spilled. Access in place via positive BP offset. */
|
|
static int stack_arg_off;
|
|
(void)stack_arg_off;
|
|
Local *l = amalloc(c->a, sizeof *l);
|
|
l->name = p->str;
|
|
/* spilled args layout: each takes 8B (ptr/len/etc); we
|
|
* only support the simple case of plain int/float here. */
|
|
extern int cg_stack_arg_cursor;
|
|
l->off = 16 + cg_stack_arg_cursor * 8;
|
|
cg_stack_arg_cursor += eightbytes;
|
|
l->next = locals;
|
|
locals = l;
|
|
}
|
|
if (tp) tp = tp->next;
|
|
}
|
|
|
|
cgstmt(c, fn->body, &locals, &frame);
|
|
|
|
/* implicit return for void functions */
|
|
if (c->tail->as != A_RET) {
|
|
for (int di = ndefers - 1; di >= 0; di--)
|
|
cgexpr(c, defers[di], locals);
|
|
ins2(c, A_MOVQ, aimm(0), areg(D_AX));
|
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
|
ins1(c, A_POPQ, areg(D_BP));
|
|
ins0(c, A_RET);
|
|
}
|
|
|
|
/* round frame to 16; patch SUBQ */
|
|
if (frame & 15) frame = (frame + 15) & ~15;
|
|
subsp->from.offset = frame;
|
|
text->from.offset = frame;
|
|
|
|
txt_emit(out, c->head);
|
|
}
|
|
|
|
/* Escape one byte for an asm string literal — the same rules
|
|
* emit_data and emit_defs already use. */
|
|
static void
|
|
emit_data_byte(FILE *out, u8 b)
|
|
{
|
|
if (b == '"' || b == '\\')
|
|
fprintf(out, "\\%c", b);
|
|
else if (b < 0x20 || b >= 0x7f)
|
|
fprintf(out, "\\x%02x", b);
|
|
else
|
|
fputc(b, out);
|
|
}
|
|
|
|
/* Emit `DIR NAME(SB),"<8 LE bytes of v>"`. Used for scalar `def`
|
|
* constants (DATA) and scalar `let` globals (DATAW). */
|
|
static void
|
|
emit_data_row(FILE *out, const char *dir, const char *name, u64 v)
|
|
{
|
|
fprintf(out, "%s %s(SB),\"", dir, name);
|
|
for (int i = 0; i < 8; i++)
|
|
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
|
|
fputs("\"\n", out);
|
|
}
|
|
|
|
/* Emit `DIR NAME(SB),"<sz zero bytes>"`. Used for top-level str/
|
|
* slice/struct lets without a baked-in initialiser — the slot is
|
|
* pre-zeroed and the program writes the real value at runtime. */
|
|
static void
|
|
emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz)
|
|
{
|
|
fprintf(out, "%s %s(SB),\"", dir, name);
|
|
for (int i = 0; i < sz; i++)
|
|
emit_data_byte(out, 0);
|
|
fputs("\"\n", out);
|
|
}
|
|
|
|
/* Emit DATAW directives for top-level mutable `let` decls.
|
|
*
|
|
* Scalar lets (8B): emit the literal value, or 0 if no init.
|
|
* Non-literal init: skip — undefined symbol surfaces at link time.
|
|
*
|
|
* str lets (16B): three init shapes are wired:
|
|
* - no rhs / `nil` / `""` → 16 zero bytes
|
|
* - `"literal"` (non-empty) → 8 zero placeholder + 8 LE len,
|
|
* plus DATAR patching the ptr
|
|
* half with the interned strlit's
|
|
* runtime VA at link time.
|
|
*
|
|
* Slice lets (24B): no-init only — the slot is zero. There's no
|
|
* literal slice syntax to honour, so this is the natural shape.
|
|
*
|
|
* Struct lets (size from Type.size): no-init only. */
|
|
static void
|
|
emit_lets(Cg *c, FILE *out, Node *file)
|
|
{
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_LET) continue;
|
|
if (d->str == NULL || d->str[0] == '\0') continue;
|
|
int sz = let_emit_size(d->type);
|
|
if (sz == 0) continue;
|
|
if (let_isfloat(d->type)) {
|
|
/* sz is 4 (f32) or 8 (f64). FLOATLIT init or zero. */
|
|
int isf32 = type_isf32(d->type);
|
|
u64 v = 0;
|
|
if (d->rhs != NULL) {
|
|
Node *r = d->rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r == NULL) continue;
|
|
if (r->kind != N_FLOATLIT) continue;
|
|
if (isf32) {
|
|
union { float f; u32 u; } x;
|
|
x.f = (float)r->fval;
|
|
v = (u64)x.u;
|
|
} else {
|
|
union { double d; u64 u; } x;
|
|
x.d = r->fval;
|
|
v = x.u;
|
|
}
|
|
}
|
|
fprintf(out, "DATAW %s(SB),\"", mod_mangle(c, d->str));
|
|
for (int i = 0; i < sz; i++)
|
|
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
|
|
fputs("\"\n", out);
|
|
continue;
|
|
}
|
|
if (sz == 8 && !let_isarray(d->type)) {
|
|
u64 v = 0;
|
|
if (d->rhs != NULL) {
|
|
Node *r = d->rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r == NULL) continue;
|
|
if (r->kind == N_INTLIT) v = r->uval;
|
|
else if (r->kind == N_RUNELIT) v = r->uval;
|
|
else if (r->kind == N_TRUE) v = 1;
|
|
else if (r->kind == N_FALSE) v = 0;
|
|
else if (r->kind == N_NIL) v = 0;
|
|
else continue;
|
|
}
|
|
emit_data_row(out, "DATAW", mod_mangle(c, d->str), v);
|
|
continue;
|
|
}
|
|
/* Strip leading casts on the rhs so a `nil: str` etc.
|
|
* reads the same as a bare nil. */
|
|
Node *r = NULL;
|
|
if (d->rhs != NULL) {
|
|
r = d->rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r == NULL) continue;
|
|
}
|
|
/* str literal init: bake the interned label's address
|
|
* into the ptr half via a DATAR reloc, set the len half
|
|
* inline. */
|
|
if (sz == 16 && r != NULL && r->kind == N_STRLIT
|
|
&& r->strlen > 0) {
|
|
const char *lab = intern_strlit(c, r->str, r->strlen);
|
|
const char *sym = mod_mangle(c, d->str);
|
|
u64 v = r->strlen;
|
|
/* 16-byte payload: 8 zero placeholder + LE len. */
|
|
fprintf(out, "DATAW %s(SB),\"", sym);
|
|
for (int i = 0; i < 8; i++) emit_data_byte(out, 0);
|
|
for (int i = 0; i < 8; i++)
|
|
emit_data_byte(out, (u8)((v >> (i * 8)) & 0xff));
|
|
fputs("\"\n", out);
|
|
fprintf(out, "DATAR %s+0(SB),%s(SB)\n", sym, lab);
|
|
continue;
|
|
}
|
|
/* Array literal init: `let xs: [N]T = [v0, v1, ...];`. Walk
|
|
* elements in declaration order; each must reduce to an
|
|
* integer literal (casts are stripped). The trailing `...`
|
|
* repeat marker fills remaining slots with the last value.
|
|
* Falls through to zero-init if any element isn't a
|
|
* constant we can evaluate at emit time. */
|
|
if (r != NULL && r->kind == N_ARRLIT && let_isarray(d->type)) {
|
|
Type *u = type_unwrap(d->type);
|
|
int esz = (u && u->sub) ? (int)u->sub->size : 1;
|
|
int alen = (u) ? (int)u->alen : 0;
|
|
u64 *vals = amalloc(c->a, sizeof(u64) * (size_t)alen);
|
|
int idx = 0;
|
|
int ok = 1;
|
|
u64 last = 0;
|
|
int repeat = 0;
|
|
for (Node *e = r->list; e && idx < alen; e = e->next) {
|
|
if (e->kind == N_FIELD && e->str &&
|
|
strcmp(e->str, "...") == 0) {
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
Node *ev = e;
|
|
while (ev && ev->kind == N_CAST) ev = ev->lhs;
|
|
if (ev == NULL) { ok = 0; break; }
|
|
if (ev->kind == N_INTLIT || ev->kind == N_RUNELIT) {
|
|
last = ev->uval;
|
|
} else if (ev->kind == N_TRUE) {
|
|
last = 1;
|
|
} else if (ev->kind == N_FALSE) {
|
|
last = 0;
|
|
} else if (ev->kind == N_NIL) {
|
|
last = 0;
|
|
} else {
|
|
ok = 0;
|
|
break;
|
|
}
|
|
vals[idx++] = last;
|
|
}
|
|
if (ok) {
|
|
if (repeat) {
|
|
while (idx < alen) vals[idx++] = last;
|
|
} else {
|
|
while (idx < alen) vals[idx++] = 0;
|
|
}
|
|
fprintf(out, "DATAW %s(SB),\"",
|
|
mod_mangle(c, d->str));
|
|
for (int i = 0; i < alen; i++) {
|
|
u64 v = vals[i];
|
|
for (int b = 0; b < esz; b++) {
|
|
emit_data_byte(out,
|
|
(u8)((v >> (b * 8)) & 0xff));
|
|
}
|
|
}
|
|
fputs("\"\n", out);
|
|
continue;
|
|
}
|
|
/* fall through to zero-init */
|
|
}
|
|
/* Otherwise: zero-init. str accepts nil / ""; struct
|
|
* accepts no rhs at all; slice accepts nil; array with no
|
|
* literal init (or a non-constant one) zero-fills. */
|
|
if (r != NULL) {
|
|
int is_struct = let_isstruct(d->type);
|
|
int is_array = let_isarray(d->type);
|
|
int empty_str = (r->kind == N_STRLIT && r->strlen == 0);
|
|
if (is_struct) continue;
|
|
if (is_array) continue;
|
|
if (r->kind != N_NIL && !empty_str) continue;
|
|
}
|
|
emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz);
|
|
}
|
|
}
|
|
|
|
/* Emit DATA directives for top-level `def` constants whose value is
|
|
* an integer/rune literal. The w6a side stores the bytes inside .text
|
|
* and accesses are RIP-relative.
|
|
*/
|
|
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 = 0;
|
|
if (d->rhs->kind == N_INTLIT || d->rhs->kind == N_RUNELIT) {
|
|
v = d->rhs->uval;
|
|
} else if (d->rhs->kind == N_TRUE) {
|
|
v = 1;
|
|
} else {
|
|
continue; /* skip non-integer-literal defs */
|
|
}
|
|
fprintf(out, "DATA %s(SB),\"", mod_mangle(c, d->str));
|
|
for (int i = 0; i < 8; i++) {
|
|
unsigned b = (unsigned)((v >> (i * 8)) & 0xff);
|
|
if (b == '"' || b == '\\')
|
|
fprintf(out, "\\%c", b);
|
|
else if (b < 0x20 || b >= 0x7f)
|
|
fprintf(out, "\\x%02x", b);
|
|
else
|
|
fputc(b, out);
|
|
}
|
|
fputs("\"\n", out);
|
|
}
|
|
(void)c;
|
|
}
|
|
|
|
/* Collect str-typed `def`s so cgexpr N_IDENT can splice them inline.
|
|
* Walks past any leading cast on the rhs (e.g. `def x: error = "x": error;`
|
|
* shows up as N_CAST wrapping an N_STRLIT). */
|
|
static void
|
|
sdef_collect(Cg *c, Node *file)
|
|
{
|
|
(void)c;
|
|
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->bytes = r->str;
|
|
s->len = r->strlen;
|
|
s->next = sdefs;
|
|
sdefs = s;
|
|
}
|
|
}
|
|
|
|
/* Pre-intern strlits referenced from top-level `let` initialisers
|
|
* (e.g. `let g: str = "hello";`). Interning has to happen before
|
|
* emit_data walks the strlit list, but we don't want to reorder
|
|
* emit_data after emit_lets (the (DATA strlits, DATAW lets) section
|
|
* order is part of the byte-identity contract with the selfhost
|
|
* cgen). So this pass populates the strlit table; emit_lets later
|
|
* just looks up the label. */
|
|
static void
|
|
let_pre_intern(Cg *c, Node *file)
|
|
{
|
|
if (file == NULL) return;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_LET) continue;
|
|
if (let_emit_size(d->type) != 16) continue;
|
|
Node *r = d->rhs;
|
|
while (r != NULL && r->kind == N_CAST) r = r->lhs;
|
|
if (r == NULL || r->kind != N_STRLIT) continue;
|
|
if (r->strlen == 0) continue;
|
|
(void)intern_strlit(c, r->str, r->strlen);
|
|
}
|
|
}
|
|
|
|
void
|
|
cg_file(Cg *c, FILE *out, Node *file)
|
|
{
|
|
if (file == NULL || file->kind != N_FILE) return;
|
|
ffi_collect(c, file);
|
|
mod_collect(c, file);
|
|
sdef_collect(c, file);
|
|
let_collect(c, file);
|
|
strlits = NULL;
|
|
strlit_seq = 0;
|
|
for (Node *d = file->list; d; d = d->next) {
|
|
if (d->kind != N_FNDECL) continue;
|
|
cgfn(c, out, d);
|
|
}
|
|
let_pre_intern(c, file);
|
|
emit_data(c, out);
|
|
emit_defs(c, out, file);
|
|
emit_lets(c, out, file);
|
|
}
|
|
|
|
void peephole(Cg *c) { (void)c; }
|
|
void regalloc_init(Cg *c) { (void)c; }
|