/* * cgen.c — typed AST → Prog list, expressed as Plan 9-flavoured * amd64 assembly text. This is the simplest thing that works: * * - Every function gets a stack frame sized for spilled locals + a * 16-byte alignment pad. * - Expressions are evaluated stack-machine style: result in AX, * intermediate stuff pushed on the hardware stack via PUSHQ AX. * - The first six integer args go in DI, SI, DX, CX, R8, R9 * (SysV amd64 ABI). We don't yet handle struct-by-value or * floats; floats and slices are deferred. * * Calling our own functions: emit CALL (SB), let w6a/w6l resolve. * Calling C externs: same — extern symbols are just unresolved CALLs. */ #include "gc.h" #include #include static const int sysv_argregs[] = { D_DI, D_SI, D_DX, D_CX, D_R8, D_R9 }; static const int sysv_fargregs[] = { D_X0, D_X1, D_X2, D_X3, D_X4, D_X5, D_X6, D_X7 }; /* per-fn cursor, reset before each cgfn: counts how many 8-byte * stack-arg slots above BP have been claimed. */ int cg_stack_arg_cursor; /* return type of the current function, set by cgfn before walking * the body. Drives tagged-union return construction and the `?` / * `!` propagation paths. */ static Type *cg_ret_type; /* Pointer to the current function's frame size accumulator. cgexpr * needs this to allocate scratch slots (e.g. match bindings) without * threading it through every signature. */ static int *cg_frame; /* Per-fn 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; 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); } /* 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). * Both fit in our 3-register classification. 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; if (t->size > 24) return 0; return (int)t->size; } 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); } /* 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 . * 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; 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; } /* Mangle an AST identifier into its asm linker symbol: * - @symbol("...") binding wins (return mapped name). * - module-private decl → .. * - else → name unchanged. * Used at every CALL/MOVQ/LEAQ site that targets an AST name. Plain * `asym(s)` still emits `s` verbatim — use it for strlit labels and * hard-coded runtime symbols like "rt_streq". */ static const char * mod_mangle(Cg *c, const char *ident) { const char *resolved = ffi_resolve(ident); if (resolved != ident) return resolved; const char *mod = mod_lookup(ident); if (mod == NULL) return ident; 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; } 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 cgexpr_int(Cg *c, long long v) { ins2(c, A_MOVQ, aimm(v), 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: 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 { ins2(c, A_MOVQ, 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; } ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); } ident_done: break; } case N_UN: 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: ins1(c, A_NOTQ, 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: { /* address-of for an N_IDENT local */ if (n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX)); } break; } case TK_STAR: /* deref */ ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX)); break; default: break; } break; case N_BIN: { /* 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.u64toa 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.u64toa 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; } case TK_AND: case TK_OR: { /* short-circuit not yet — eager evaluation. */ if (n->op == TK_AND) ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); else ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break; } 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. */ Type *ft = f->type; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; if (fu && fu->kind == TY_TAGGED && n->op == TK_ASSIGN) { int boff = localfind(locals, base->str); int foff = (int)f->offset; Type *vt = n->rhs ? n->rhs->type : NULL; int tag = cg_tag_for_variant(fu, vt); /* Resolve the slot's base address * (either struct local or *struct). */ int addr_in_bx = via_ptr; if (via_ptr) ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX)); cgexpr(c, n->rhs, locals); if (type_isstr(vt)) { if (addr_in_bx) { ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + 8)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BX, foff + 16)); } else { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 8)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 16)); } } else { if (addr_in_bx) ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, foff + 8)); else ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 8)); } if (addr_in_bx) ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BX, foff + 0)); else ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, boff + foff + 0)); break; } int fsz = (int)(f->type ? f->type->size : 8); int signed_field = f->type && ( f->type->kind == TY_I8 || f->type->kind == TY_I16 || f->type->kind == TY_I32); int load_op = A_MOVQ, store_op = A_MOVQ; if (fsz == 1) { load_op = A_MOVZBQ; store_op = A_MOVB; } else if (fsz == 4) { load_op = signed_field ? A_MOVSXD : A_MOVL; store_op = A_MOVL; } int boff = localfind(locals, 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 { 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; } /* 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 { 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 */ } } /* 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 { ins2(c, store_op, areg(D_AX), amem(D_BP, boff + foff)); } break; } } /* Chained `.field = v` where evaluates to a *struct. * cgexpr on the inner expression already returns the pointer; * we then store at (ptr + field.offset). Without this, only the * single-level N_IDENT base above is wired and shapes like * `r.sym.flag = 1` (where r.sym: *T) silently emit no store — * the read still works because the chained-N_DOT read path is * wired below. (This was trap 1 of the cgen miscompilations.) */ if (n->lhs && n->lhs->kind == N_DOT && n->lhs->lhs && n->lhs->lhs->kind != N_IDENT) { Type *bt = n->lhs->lhs->type; Type *bu = (bt && bt->kind == TY_NAMED) ? bt->under : bt; if (bu && bu->kind == TY_PTR && bu->sub) { Type *inner = bu->sub; if (inner->kind == TY_NAMED) inner = inner->under; if (inner && inner->kind == TY_STRUCT) { Tfield *f = NULL; for (Tfield *fl = inner->fields; fl; fl = fl->next) if (strcmp(fl->name, n->lhs->str) == 0) { f = fl; break; } if (f != NULL) { Type *ft = f->type; Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; int fsz = (int)(ft ? ft->size : 8); int signed_field = ft && ( ft->kind == TY_I8 || ft->kind == TY_I16 || ft->kind == TY_I32); int store_op = A_MOVQ; if (fsz == 1) store_op = A_MOVB; else if (fsz == 4) store_op = A_MOVL; int foff = (int)f->offset; if (n->op == TK_ASSIGN) { 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 = A_MOVQ; if (fsz == 1) load_op = A_MOVZBQ; else if (fsz == 4) load_op = signed_field ? A_MOVSXD : A_MOVL; 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; } } } } /* float assignment to a local */ if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) { cgexpr(c, n->rhs, locals); /* X0 */ int off = localfind(locals, n->lhs->str); if (off != 0) { int op = op_for(n, A_MOVSD, A_MOVSS); ins2(c, op, areg(D_X0), amem(D_BP, off)); } 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; int esz = (u && u->sub) ? (int)u->sub->size : 1; int elem_is_str = u && u->sub && type_isstr(u->sub); 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 */ if (base->kind == N_IDENT && is_arr) { int off = localfind(locals, base->str); ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX)); } else if (base->kind == N_IDENT) { int off = localfind(locals, base->str); 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 = A_MOVQ; if (esz == 1) store_op = A_MOVB; else if (esz == 4) store_op = A_MOVL; ins2(c, store_op, areg(D_AX), amem(D_BX, 0)); break; } } /* Plain `r = expr;` where r is a tagged-union local. Mirrors * the let-init path: forward AX:DX[:CX] when rhs is itself * tagged, else synthesise the tag from rhs's static type. */ 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; Type *rt = n->rhs ? n->rhs->type : NULL; if (type_istagged(rt)) { 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)); if (lu->size > 16) ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); } else { int tag = cg_tag_for_variant(lu, rt); cgexpr(c, n->rhs, locals); if (type_isstr(rt)) { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); } else { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 8)); } ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, off + 0)); } 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; 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 signed_field = vt && (vt->kind == TY_I8 || vt->kind == TY_I16 || vt->kind == TY_I32); (void)signed_field; int store_op = A_MOVQ; if (sz == 1) store_op = A_MOVB; else if (sz == 4) store_op = A_MOVL; 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. */ 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) break; 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 (n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); /* No local match: drop the whole assignment, including * the RHS evaluation. Top-level let mutation isn't * supported (no writable .data) and we don't want to * leak a dead `MOVQ $rhs, AX` into the output. */ if (off == 0) 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. */ if (n->op == TK_PLUSEQ) { ins2(c, A_ADDQ, areg(D_AX), amem(D_BP, off)); break; } if (n->op == TK_MINUSEQ) { ins2(c, A_SUBQ, areg(D_AX), amem(D_BP, off)); break; } ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX)); switch (n->op) { 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 * * 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; 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); break; } } cgexpr(c, f->lhs, locals); /* AX = field val */ 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; int sn_off = (sn->kind == N_IDENT) ? localfind(locals, sn->str) : 0; int store_op = (esz == 1) ? A_MOVB : A_MOVQ; 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 = (esz == 1) ? A_MOVZBQ : A_MOVQ; /* 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). */ int argcount = 0; Node *args[16] = {0}; for (Node *a = n->list; a; a = a->next) if (argcount < 16) args[argcount++] = a; /* eval right-to-left, push to stack */ for (int i = argcount - 1; i >= 0; i--) { if (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 (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); 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); 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 (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 (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; } 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 { 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 (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. */ Type *callee_t = n->lhs ? n->lhs->type : NULL; Type *cu = (callee_t && callee_t->kind == TY_NAMED) ? callee_t->under : callee_t; 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. */ Node *s = n->lhs; Type *st = s ? s->type : NULL; Type *su = (st && st->kind == TY_NAMED) ? st->under : 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 */ 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)); } } 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 — copy each word into the 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)); 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 *end = mklabel(c, "match_end"); for (Node *cs = n->list; cs; cs = cs->next) { char *next = mklabel(c, "match_next"); 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 (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; int bsz = (bu && bu->kind == TY_STR) ? 16 : 8; int voff = localoff(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); ins1(c, A_JMP, abranch(end)); label(c, next); } label(c, end); break; } case N_TRYPROP: { /* Evaluate tagged value: AX=tag, DX=value0[, CX=value1]. * If tag != 0, propagate as the current function's return. * On success, unwrap to the success-variant ABI: ≤8B values * land in AX; str values land in (AX=ptr, BX=len). */ cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; Type *first = (u && u->kind == TY_TAGGED && u->params) ? u->params->type : NULL; int success_is_str = type_isstr(first); char *cont = mklabel(c, "tryprop_ok"); ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins1(c, A_JE, abranch(cont)); 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. */ cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; Type *first = (u && u->kind == TY_TAGGED && u->params) ? u->params->type : NULL; int success_is_str = type_isstr(first); char *cont = mklabel(c, "tryunw_ok"); ins2(c, A_CMPQ, aimm(0), 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_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)); } 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; } ins2(c, A_MOVQ, masym(c, n->str), 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; } } 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 signed_field = tp->type && ( tp->type->kind == TY_I8 || tp->type->kind == TY_I16 || tp->type->kind == TY_I32); int op = A_MOVQ; if (fsz == 1) op = A_MOVZBQ; else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; 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_off + field_off */ if (u && u->kind == TY_STRUCT && n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); for (Tfield *f = u->fields; f; f = f->next) { if (strcmp(f->name, n->str) != 0) continue; /* 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(D_BP, off + (int)f->offset + 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_BP, off + (int)f->offset + 8), areg(D_BX)); break; } int fsz = (int)(f->type ? f->type->size : 8); int signed_field = f->type && ( f->type->kind == TY_I8 || f->type->kind == TY_I16 || f->type->kind == TY_I32); int op = A_MOVQ; if (fsz == 1) op = A_MOVZBQ; else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; ins2(c, op, amem(D_BP, off + (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; /* 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; } int fsz = (int)(f->type ? f->type->size : 8); int signed_field = f->type && ( f->type->kind == TY_I8 || f->type->kind == TY_I16 || f->type->kind == TY_I32); int op = A_MOVQ; if (fsz == 1) op = A_MOVZBQ; else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; 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; } int fsz = (int)(ft ? ft->size : 8); int signed_field = ft && ( ft->kind == TY_I8 || ft->kind == TY_I16 || ft->kind == TY_I32); int op = A_MOVQ; if (fsz == 1) op = A_MOVZBQ; else if (fsz == 4) op = signed_field ? A_MOVSXD : A_MOVL; ins2(c, op, amem(D_AX, (int)f->offset), areg(D_AX)); goto dot_done; } } } } /* 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. */ Type *bt = n->lhs ? n->lhs->type : NULL; Type *u = (bt && bt->kind == TY_NAMED) ? bt->under : bt; int esz = 1; if (u && u->sub) esz = (int)u->sub->size; if (u && u->kind == TY_STR) esz = 1; if (n->lhs->kind == N_IDENT && u) { int off = localfind(locals, 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 */ 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; } int signed_elem = u && u->sub && ( u->sub->kind == TY_I8 || u->sub->kind == TY_I16 || u->sub->kind == TY_I32); int load_op = A_MOVQ; if (esz == 1) load_op = A_MOVZBQ; else if (esz == 4) load_op = signed_elem ? A_MOVSXD : A_MOVL; 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; } { int signed_elem = u && u->sub && ( u->sub->kind == TY_I8 || u->sub->kind == TY_I16 || u->sub->kind == TY_I32); int load_op = A_MOVQ; if (esz == 1) load_op = A_MOVZBQ; else if (esz == 4) load_op = signed_elem ? A_MOVSXD : A_MOVL; ins2(c, load_op, amem(D_AX, 0), areg(D_AX)); } 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. Two shapes: * 1) rhs already produces a tagged-union value (e.g. a fn * call returning (T | E)). cgexpr leaves AX=tag, * DX=value0[, CX=value1] — copy each into the slot. * 2) rhs is a bare variant value (e.g. `let r: (i64|i32) = 7`). * Synthesise the tag from rhs's static type and store it * alongside the value. str-typed rhs flows as * (AX=ptr, BX=len) so we store both halves. */ if (n->rhs && lu && lu->kind == TY_TAGGED) { Type *rt = n->rhs->type; if (type_istagged(rt)) { 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)); if (lu->size > 16) ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); } else { int tag = cg_tag_for_variant(lu, rt); cgexpr(c, n->rhs, *locals); if (type_isstr(rt)) { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 8)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 16)); } else { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 8)); } ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, off + 0)); } 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; } /* 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: synthesise tag from * f->lhs's static type and store value bytes * (1 or 2 words for ≤8B/str variants). */ Type *fu = (ft && ft->kind == TY_NAMED) ? ft->under : ft; if (fu && fu->kind == TY_TAGGED) { Type *vt = f->lhs ? f->lhs->type : NULL; int tag = cg_tag_for_variant(fu, vt); cgexpr(c, f->lhs, *locals); if (type_isstr(vt)) { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + (int)foff + 8)); ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + (int)foff + 16)); } else if (type_istagged(vt)) { /* forwarding a tagged value */ ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + (int)foff + 8)); if (fu->size > 16) ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + (int)foff + 16)); /* AX already holds the tag */ ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + (int)foff + 0)); continue; } else { ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + (int)foff + 8)); } ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, off + (int)foff + 0)); continue; } cgexpr(c, f->lhs, *locals); 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; } /* 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)); } /* arrays/slices 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). */ 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; cgexpr(c, n->lhs, *locals); if (!type_istagged(vt)) { int tag = cg_tag_for_variant(rt, vt); if (type_isstr(vt)) { /* AX=ptr, BX=len → DX=ptr, * CX=len, AX=tag. Move BX * before AX since the tag * MOVQ trashes AX. */ 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)); } 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; int signed_field; } 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].signed_field = tp && tp->type && ( tp->type->kind == TY_I8 || tp->type->kind == TY_I16 || tp->type->kind == TY_I32); 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].signed_field = u && u->sub && ( u->sub->kind == TY_I8 || u->sub->kind == TY_I16 || u->sub->kind == TY_I32); 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 = A_MOVQ; if (binds[b].sz == 1) op = A_MOVZBQ; else if (binds[b].sz == 4) op = binds[b].signed_field ? A_MOVSXD : A_MOVL; 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_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 { /* 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); } /* 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; } } 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); strlits = NULL; strlit_seq = 0; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_FNDECL) continue; cgfn(c, out, d); } emit_data(c, out); emit_defs(c, out, file); } void peephole(Cg *c) { (void)c; } void regalloc_init(Cg *c) { (void)c; }