aboutsummaryrefslogtreecommitdiff
path: root/src/backend/rv64
diff options
context:
space:
mode:
Diffstat (limited to 'src/backend/rv64')
-rw-r--r--src/backend/rv64/abi.cc690
-rw-r--r--src/backend/rv64/emit.cc949
-rw-r--r--src/backend/rv64/isel.cc518
-rw-r--r--src/backend/rv64/targ.cc142
4 files changed, 2299 insertions, 0 deletions
diff --git a/src/backend/rv64/abi.cc b/src/backend/rv64/abi.cc
new file mode 100644
index 0000000..6456c21
--- /dev/null
+++ b/src/backend/rv64/abi.cc
@@ -0,0 +1,690 @@
+/* Source code for RCC-Legacy.
+ Copyright (C) 2026 Richard Qin
+
+This file is part of RCC.
+
+RCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+RCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with RCC; see the file LICENSE. If not see
+<http://www.gnu.org/licenses/>. */
+
+/// @file abi.cc
+/// @brief RISC-V lp64d ABI lowering.
+
+#include "rcc/rv64.h"
+
+namespace rcc {
+
+namespace {
+
+// ABI classification flags (OR-able).
+enum : int {
+ kCptr = 1, // Pointer to large struct.
+ kCstk1 = 2, // First XLEN on stack.
+ kCstk2 = 4, // Second XLEN on stack.
+ kCstk = kCstk1 | kCstk2,
+ kCfpint = 8, // Float in GP reg.
+};
+
+// Argument class.
+struct ArgClass {
+ int flags; // OR of kCptr / kCstk1 / kCstk2 / kCfpint
+ Typ* type;
+ int reg[2];
+ int cls[2];
+ int off[2];
+ int ngp; // Written by typclass
+ int nfp;
+ int nreg;
+};
+
+struct Insl {
+ Insl* link;
+ Ins i;
+};
+
+struct Params {
+ int ngp;
+ int nfp;
+ int stk;
+};
+
+// Registers. Sentinels of 0 are used to prevent out-of-bounds reads.
+constexpr int kGpReg[10] = {A0, A1, A2, A3, A4, A5, A6, A7, 0, 0};
+constexpr int kFpReg[10] = {FA0, FA1, FA2, FA3, FA4, FA5, FA6, FA7, 0, 0};
+
+} // namespace
+
+// RCall encoding.
+
+bits rv64_retregs(Ref r, int p[2]) {
+ assert(rtype(r) == RCall);
+ int ngp = static_cast<int>(r.val & 3);
+ int nfp = static_cast<int>((r.val >> 2) & 3);
+ if (p) {
+ p[0] = ngp;
+ p[1] = nfp;
+ }
+ bits b = 0;
+ for (int i = 0; i < ngp; ++i)
+ b |= BIT(A0 + i);
+ for (int i = 0; i < nfp; ++i)
+ b |= BIT(FA0 + i);
+ return b;
+}
+
+bits rv64_argregs(Ref r, int p[2]) {
+ assert(rtype(r) == RCall);
+ int ngp = static_cast<int>((r.val >> 4) & 15);
+ int nfp = static_cast<int>((r.val >> 8) & 15);
+ int t5 = static_cast<int>((r.val >> 12) & 1);
+ if (p) {
+ p[0] = ngp + t5;
+ p[1] = nfp;
+ }
+ bits b = 0;
+ for (int i = 0; i < ngp; ++i)
+ b |= BIT(A0 + i);
+ for (int i = 0; i < nfp; ++i)
+ b |= BIT(FA0 + i);
+ if (t5)
+ b |= BIT(T5);
+ return b;
+}
+
+namespace {
+
+// HFA-style floating-point struct classification.
+
+int classify_fpstruct(Typ* t, int off, ArgClass* c) {
+ if (t->isunion)
+ return -1;
+
+ for (Field* f = *t->fields; f->type != Field::FEnd; ++f) {
+ if (f->type == Field::FPad) {
+ off += static_cast<int>(f->len);
+ continue;
+ }
+ if (f->type == Field::FTyp) {
+ if (classify_fpstruct(&typ[f->len], off, c) == -1)
+ return -1;
+ continue;
+ }
+
+ int slot = c->nfp + c->ngp;
+ if (slot == 2)
+ return -1;
+
+ switch (f->type) {
+ case Field::Fb:
+ case Field::Fh:
+ case Field::Fw:
+ c->cls[slot] = Kw;
+ ++c->ngp;
+ break;
+ case Field::Fl:
+ c->cls[slot] = Kl;
+ ++c->ngp;
+ break;
+ case Field::Fs:
+ c->cls[slot] = Ks;
+ ++c->nfp;
+ break;
+ case Field::Fd:
+ c->cls[slot] = Kd;
+ ++c->nfp;
+ break;
+ default:
+ die("classify_fpstruct: unexpected field type");
+ }
+ c->off[slot] = off;
+ off += static_cast<int>(f->len);
+ }
+ return c->nfp;
+}
+
+// Determine ABI class for struct t.
+void typclass(ArgClass* c, Typ* t, int fpabi, const int* gp, const int* fp) {
+ c->type = t;
+ c->flags = 0;
+ c->ngp = 0;
+ c->nfp = 0;
+
+ if (t->align > 4)
+ err(const_cast<char*>("alignments larger than 16 are not supported"));
+
+ if (t->isdark || t->size > 16 || t->size == 0) {
+ // Large or dark: pass by pointer.
+ c->flags |= kCptr;
+ c->cls[0] = Kl;
+ c->off[0] = 0;
+ c->ngp = 1;
+ } else if (!fpabi || classify_fpstruct(t, 0, c) <= 0) {
+ // GP-only word packing.
+ uint n = 0;
+ while (8 * n < t->size) {
+ c->cls[n] = Kl;
+ c->off[n] = static_cast<int>(8 * n);
+ ++n;
+ }
+ c->nfp = 0;
+ c->ngp = static_cast<int>(n);
+ }
+
+ c->nreg = c->nfp + c->ngp;
+ for (int i = 0; i < c->nreg; ++i) {
+ if (KBASE(c->cls[i]) == 0)
+ c->reg[i] = *gp++;
+ else
+ c->reg[i] = *fp++;
+ }
+}
+
+// Store tmp to mem+off.
+void sttmps(Ctx* ctx, Ref tmp[], int ntmp, ArgClass* c, Ref mem, Fn* fn) {
+ static constexpr auto kStOp = [] {
+ std::array<int, 4> a{};
+ a[Kw] = Ostorew;
+ a[Kl] = Ostorel;
+ a[Ks] = Ostores;
+ a[Kd] = Ostored;
+ return a;
+ }();
+ assert(ntmp > 0 && ntmp <= 2);
+ for (int i = 0; i < ntmp; ++i) {
+ tmp[i] = newtmp(ctx, "abi", c->cls[i], fn);
+ Ref addr = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, kStOp[static_cast<size_t>(c->cls[i])], 0, Ref::none(), tmp[i], addr);
+ emit(ctx, Oadd, Kl, addr, mem, getcon(ctx, c->off[i], fn));
+ }
+}
+
+// Load mem+off to regs.
+void ldregs(Ctx* ctx, ArgClass* c, Ref mem, Fn* fn) {
+ for (int i = 0; i < c->nreg; ++i) {
+ Ref addr = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Oload, c->cls[i], Ref::tmp(static_cast<uint>(c->reg[i])), addr, Ref::none());
+ emit(ctx, Oadd, Kl, addr, mem, getcon(ctx, c->off[i], fn));
+ }
+}
+
+// Return value lowering.
+
+void selret(Ctx* ctx, Blk* b, Fn* fn) {
+ int j = b->jmp.type;
+ if (!isret(j) || j == Jret0)
+ return;
+
+ Ref r = b->jmp.arg;
+ b->jmp.type = Jret0;
+
+ int cty;
+ if (j == Jretc) {
+ ArgClass cr{};
+ typclass(&cr, &typ[fn->retty], 1, kGpReg, kFpReg);
+ if (cr.flags & kCptr) {
+ assert(rtype(fn->retr) == RTmp);
+ emit(ctx, Oblit1, 0, Ref::none(), Ref::integer(static_cast<int>(cr.type->size)),
+ Ref::none());
+ emit(ctx, Oblit0, 0, Ref::none(), r, fn->retr);
+ cty = 0;
+ } else {
+ ldregs(ctx, &cr, r, fn);
+ cty = (cr.nfp << 2) | cr.ngp;
+ }
+ } else {
+ int k = j - Jretw;
+ if (KBASE(k) == 0) {
+ emit(ctx, Ocopy, k, Ref::tmp(A0), r, Ref::none());
+ cty = 1;
+ } else {
+ emit(ctx, Ocopy, k, Ref::tmp(FA0), r, Ref::none());
+ cty = 1 << 2;
+ }
+ }
+
+ b->jmp.arg = Ref::call(static_cast<uint>(cty));
+}
+
+// Arg/par classification.
+
+int argsclass(Ins* i0, Ins* i1, ArgClass* carg, int retptr) {
+ const int* gp = kGpReg;
+ const int* fp = kFpReg;
+ int ngp = 8, nfp = 8;
+ bool vararg = false, envc = false;
+
+ if (retptr) {
+ ++gp;
+ --ngp;
+ }
+
+ ArgClass* c = carg;
+ for (Ins* i = i0; i < i1; ++i, ++c) {
+ switch (i->op) {
+ case Opar:
+ case Oarg:
+ c->cls[0] = static_cast<int>(i->cls);
+ if (!vararg && KBASE(i->cls) == 1 && nfp > 0) {
+ --nfp;
+ c->reg[0] = *fp++;
+ } else if (ngp > 0) {
+ if (KBASE(i->cls) == 1)
+ c->flags |= kCfpint;
+ --ngp;
+ c->reg[0] = *gp++;
+ } else {
+ c->flags |= kCstk1;
+ }
+ break;
+
+ case Oargv:
+ vararg = true;
+ break;
+
+ case Oparc:
+ case Oargc: {
+ Typ* t = &typ[i->arg[0].val];
+ typclass(c, t, 1, gp, fp);
+ if (c->nfp > 0 && (c->nfp >= nfp || c->ngp >= ngp)) {
+ // Downgrade to GP-only.
+ typclass(c, t, 0, gp, fp);
+ }
+ assert(c->nfp <= nfp);
+ if (c->ngp <= ngp) {
+ ngp -= c->ngp;
+ nfp -= c->nfp;
+ gp += c->ngp;
+ fp += c->nfp;
+ } else if (ngp > 0) {
+ // First word in GP, second on stack.
+ assert(c->ngp == 2);
+ assert(c->flags == 0);
+ c->flags |= kCstk2;
+ c->nreg = 1;
+ --ngp;
+ ++gp;
+ } else {
+ c->flags |= kCstk1;
+ if (c->nreg > 1)
+ c->flags |= kCstk2;
+ c->nreg = 0;
+ }
+ break;
+ }
+
+ case Opare:
+ case Oarge:
+ c->reg[0] = T5;
+ c->cls[0] = Kl;
+ envc = true;
+ break;
+ }
+ }
+
+ int passgp = static_cast<int>(gp - kGpReg);
+ int passfp = static_cast<int>(fp - kFpReg);
+ return ((envc ? 1 : 0) << 12) | (passgp << 4) | (passfp << 8);
+}
+
+// Prepend Oalloc to start block.
+void stkblob(Ctx* ctx, Ref r, Typ* t, Fn* fn, Insl** ilp) {
+ Insl* il = static_cast<Insl*>(alloc(ctx, sizeof(*il)));
+ int al = t->align - 2; // alignment logic.
+ if (al < 0)
+ al = 0;
+ uint64_t sz = (t->size + 7) & ~static_cast<uint64_t>(7);
+
+ Ins ins = {};
+ ins.op = static_cast<uint>(Oalloc + al);
+ ins.cls = Kl;
+ ins.to = r;
+ ins.arg[0] = getcon(ctx, static_cast<int64_t>(sz), fn);
+ il->i = ins;
+ il->link = *ilp;
+ *ilp = il;
+}
+
+// Call lowering.
+
+void selcall(Ctx* ctx, Fn* fn, Ins* i0, Ins* i1, Insl** ilp) {
+ ArgClass* ca =
+ static_cast<ArgClass*>(alloc(ctx, static_cast<size_t>(i1 - i0) * sizeof(ArgClass)));
+ for (Ins* z = i0; z < i1; ++z)
+ ca[z - i0] = {};
+ ArgClass cr{};
+
+ if (!req(i1->arg[1], Ref::none())) {
+ typclass(&cr, &typ[i1->arg[1].val], 1, kGpReg, kFpReg);
+ }
+
+ int cty = argsclass(i0, i1, ca, cr.flags & kCptr);
+
+ // compute stk and replace large struct args with pointer.
+ uint64_t stk = 0;
+ {
+ ArgClass* c = ca;
+ for (Ins* i = i0; i < i1; ++i, ++c) {
+ if (i->op == Oargv)
+ continue;
+ if (c->flags & kCptr) {
+ i->arg[0] = newtmp(ctx, "abi", Kl, fn);
+ stkblob(ctx, i->arg[0], c->type, fn, ilp);
+ i->op = Oarg;
+ }
+ if (c->flags & kCstk1)
+ stk += 8;
+ if (c->flags & kCstk2)
+ stk += 8;
+ }
+ stk += stk & 15; // 16B align tail
+ }
+ if (stk)
+ emit(ctx, Osalloc, Kl, Ref::none(), getcon(ctx, -static_cast<int64_t>(stk), fn),
+ Ref::none());
+
+ // Emit instructions backwards.
+ Ref tmp[2];
+ if (!req(i1->arg[1], Ref::none())) {
+ stkblob(ctx, i1->to, cr.type, fn, ilp);
+ cty |= (cr.nfp << 2) | cr.ngp;
+ if (cr.flags & kCptr) {
+ // Dummy Ocopy.
+ emit(ctx, Ocopy, Kw, Ref::none(), Ref::tmp(A0), Ref::none());
+ } else {
+ sttmps(ctx, tmp, cr.nreg, &cr, i1->to, fn);
+ for (int j = 0; j < cr.nreg; ++j) {
+ Ref rreg = Ref::tmp(static_cast<uint>(cr.reg[j]));
+ emit(ctx, Ocopy, cr.cls[j], tmp[j], rreg, Ref::none());
+ }
+ }
+ } else if (KBASE(i1->cls) == 0) {
+ emit(ctx, Ocopy, i1->cls, i1->to, Ref::tmp(A0), Ref::none());
+ cty |= 1;
+ } else {
+ emit(ctx, Ocopy, i1->cls, i1->to, Ref::tmp(FA0), Ref::none());
+ cty |= 1 << 2;
+ }
+
+ emit(ctx, Ocall, 0, Ref::none(), i1->arg[0], Ref::call(static_cast<uint>(cty)));
+
+ if (cr.flags & kCptr) {
+ // Cptr return logic.
+ emit(ctx, Ocopy, Kl, Ref::tmp(A0), i1->to, Ref::none());
+ }
+
+ // move args to regs.
+ {
+ ArgClass* c = ca;
+ for (Ins* i = i0; i < i1; ++i, ++c) {
+ if (i->op == Oargv || (c->flags & kCstk1))
+ continue;
+ if (i->op == Oargc) {
+ ldregs(ctx, c, i->arg[1], fn);
+ } else if (c->flags & kCfpint) {
+ int k = KWIDE(c->cls[0]) ? Kl : Kw;
+ Ref r = newtmp(ctx, "abi", k, fn);
+ emit(ctx, Ocopy, k, Ref::tmp(static_cast<uint>(c->reg[0])), r, Ref::none());
+ c->reg[0] = static_cast<int>(r.val);
+ } else {
+ emit(ctx, Ocopy, c->cls[0], Ref::tmp(static_cast<uint>(c->reg[0])), i->arg[0],
+ Ref::none());
+ }
+ }
+ }
+
+ // casts and blits.
+ {
+ ArgClass* c = ca;
+ for (Ins* i = i0; i < i1; ++i, ++c) {
+ if (c->flags & kCfpint) {
+ int k = KWIDE(c->cls[0]) ? Kl : Kw;
+ emit(ctx, Ocast, k, Ref::tmp(static_cast<uint>(c->reg[0])), i->arg[0], Ref::none());
+ }
+ if (c->flags & kCptr) {
+ emit(ctx, Oblit1, 0, Ref::none(), Ref::integer(static_cast<int>(c->type->size)),
+ Ref::none());
+ emit(ctx, Oblit0, 0, Ref::none(), i->arg[1], i->arg[0]);
+ }
+ }
+ }
+
+ if (!stk)
+ return;
+
+ // move stack args to slot.
+ uint64_t off = 0;
+ Ref sp = newtmp(ctx, "abi", Kl, fn);
+ {
+ ArgClass* c = ca;
+ for (Ins* i = i0; i < i1; ++i, ++c) {
+ if (i->op == Oargv || !(c->flags & kCstk))
+ continue;
+ if (i->op == Oarg) {
+ Ref addr = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Ostorew + i->cls, Kw, Ref::none(), i->arg[0], addr);
+ if (i->cls == Kw) {
+ // TODO: check if sext is needed.
+ ctx->curi->op = Ostorel;
+ ctx->curi->arg[0] = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Oextsw, Kl, ctx->curi->arg[0], i->arg[0], Ref::none());
+ }
+ emit(ctx, Oadd, Kl, addr, sp, getcon(ctx, static_cast<int64_t>(off), fn));
+ off += 8;
+ }
+ if (i->op == Oargc) {
+ if (c->flags & kCstk1) {
+ Ref addr = newtmp(ctx, "abi", Kl, fn);
+ Ref val = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Ostorel, 0, Ref::none(), val, addr);
+ emit(ctx, Oadd, Kl, addr, sp, getcon(ctx, static_cast<int64_t>(off), fn));
+ emit(ctx, Oload, Kl, val, i->arg[1], Ref::none());
+ off += 8;
+ }
+ if (c->flags & kCstk2) {
+ Ref addr = newtmp(ctx, "abi", Kl, fn);
+ Ref val = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Ostorel, 0, Ref::none(), val, addr);
+ emit(ctx, Oadd, Kl, addr, sp, getcon(ctx, static_cast<int64_t>(off), fn));
+ Ref addr2 = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Oload, Kl, val, addr2, Ref::none());
+ emit(ctx, Oadd, Kl, addr2, i->arg[1], getcon(ctx, 8, fn));
+ off += 8;
+ }
+ }
+ }
+ }
+ emit(ctx, Osalloc, Kl, sp, getcon(ctx, static_cast<int64_t>(stk), fn), Ref::none());
+}
+
+// Par lowering.
+
+Params selpar(Ctx* ctx, Fn* fn, Ins* i0, Ins* i1) {
+ ArgClass* ca =
+ static_cast<ArgClass*>(alloc(ctx, static_cast<size_t>(i1 - i0) * sizeof(ArgClass)));
+ for (Ins* z = i0; z < i1; ++z)
+ ca[z - i0] = {};
+ ArgClass cr{};
+ ctx->curi = &ctx->insb[NIns];
+
+ if (fn->retty >= 0) {
+ typclass(&cr, &typ[fn->retty], 1, kGpReg, kFpReg);
+ if (cr.flags & kCptr) {
+ fn->retr = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Ocopy, Kl, fn->retr, Ref::tmp(A0), Ref::none());
+ }
+ }
+
+ int cty = argsclass(i0, i1, ca, cr.flags & kCptr);
+ fn->reg = rv64_argregs(Ref::call(static_cast<uint>(cty)), nullptr);
+
+ // slot allocations and Ocast.
+ Insl* il = nullptr;
+ Ref tmp[17];
+ Ref* t = tmp;
+ {
+ ArgClass* c = ca;
+ for (Ins* i = i0; i < i1; ++i, ++c) {
+ if (c->flags & kCfpint) {
+ Ref to_orig = i->to;
+ int k = c->cls[0];
+ c->cls[0] = KWIDE(k) ? Kl : Kw;
+ i->to = newtmp(ctx, "abi", k, fn);
+ emit(ctx, Ocast, k, to_orig, i->to, Ref::none());
+ }
+ if (i->op == Oparc && !(c->flags & kCptr) && c->nreg != 0) {
+ int nt = c->nreg;
+ if (c->flags & kCstk2) {
+ c->cls[1] = Kl;
+ c->off[1] = 8;
+ assert(nt == 1);
+ nt = 2;
+ }
+ sttmps(ctx, t, nt, c, i->to, fn);
+ stkblob(ctx, i->to, c->type, fn, &il);
+ t += nt;
+ }
+ }
+ }
+ for (; il; il = il->link)
+ emiti(ctx, il->i);
+
+ // copy from regs or load from slot.
+ t = tmp;
+ int s = 2 + 8 * fn->vararg;
+ {
+ ArgClass* c = ca;
+ for (Ins* i = i0; i < i1; ++i, ++c) {
+ if (i->op == Oparc && !(c->flags & kCptr)) {
+ if (c->nreg == 0) {
+ fn->tmp[i->to.val].slot = -s;
+ s += (c->flags & kCstk2) ? 2 : 1;
+ continue;
+ }
+ for (int j = 0; j < c->nreg; ++j) {
+ Ref rreg = Ref::tmp(static_cast<uint>(c->reg[j]));
+ emit(ctx, Ocopy, c->cls[j], *t++, rreg, Ref::none());
+ }
+ if (c->flags & kCstk2) {
+ emit(ctx, Oload, Kl, *t, Ref::slot(-s), Ref::none());
+ ++t;
+ ++s;
+ }
+ } else if (c->flags & kCstk1) {
+ emit(ctx, Oload, c->cls[0], i->to, Ref::slot(-s), Ref::none());
+ ++s;
+ } else {
+ emit(ctx, Ocopy, c->cls[0], i->to, Ref::tmp(static_cast<uint>(c->reg[0])),
+ Ref::none());
+ }
+ }
+ }
+
+ Params p;
+ p.stk = s;
+ p.ngp = (cty >> 4) & 15;
+ p.nfp = (cty >> 8) & 15;
+ return p;
+}
+
+// Varargs.
+
+void selvaarg(Ctx* ctx, Fn* fn, Ins* i) {
+ Ref loc = newtmp(ctx, "abi", Kl, fn);
+ Ref newloc = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Ostorel, Kw, Ref::none(), newloc, i->arg[0]);
+ emit(ctx, Oadd, Kl, newloc, loc, getcon(ctx, 8, fn));
+ emit(ctx, Oload, i->cls, i->to, loc, Ref::none());
+ emit(ctx, Oload, Kl, loc, i->arg[0], Ref::none());
+}
+
+void selvastart(Ctx* ctx, Fn* fn, Params p, Ref ap) {
+ Ref rsave = newtmp(ctx, "abi", Kl, fn);
+ emit(ctx, Ostorel, Kw, Ref::none(), rsave, ap);
+ int s = p.stk > 2 + 8 * fn->vararg ? p.stk : 2 + p.ngp;
+ emit(ctx, Oaddr, Kl, rsave, Ref::slot(-s), Ref::none());
+}
+
+} // namespace
+
+// Entry point.
+
+void rv64_abi(Ctx* ctx, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = 0;
+
+ // Lower parameters.
+ Blk* b = fn->start;
+ Ins* end = &b->ins[b->nins];
+ Ins* i = b->ins;
+ while (i < end && ispar(i->op))
+ ++i;
+ Params p = selpar(ctx, fn, b->ins, i);
+
+ int n0 = static_cast<int>(&ctx->insb[NIns] - ctx->curi);
+ int ioff = static_cast<int>(i - b->ins);
+ int n1 = static_cast<int>(b->nins) - ioff;
+ vgrow(ctx, &b->ins, static_cast<ulong>(n0 + n1));
+ icpy(b->ins + n0, b->ins + ioff, static_cast<ulong>(n1));
+ icpy(b->ins, ctx->curi, static_cast<ulong>(n0));
+ b->nins = static_cast<uint>(n0 + n1);
+
+ // Lower calls/returns/varargs.
+ Insl* il = nullptr;
+ b = fn->start;
+ do {
+ b = b->link ? b->link : fn->start;
+ if (b->visit)
+ continue;
+ ctx->curi = &ctx->insb[NIns];
+ selret(ctx, b, fn);
+
+ for (Ins* it = &b->ins[b->nins]; it != b->ins;) {
+ --it;
+ switch (it->op) {
+ default:
+ emiti(ctx, *it);
+ break;
+ case Ocall: {
+ Ins* lo = it;
+ while (lo > b->ins && isarg((lo - 1)->op))
+ --lo;
+ selcall(ctx, fn, lo, it, &il);
+ it = lo;
+ break;
+ }
+ case Ovastart:
+ selvastart(ctx, fn, p, it->arg[0]);
+ break;
+ case Ovaarg:
+ selvaarg(ctx, fn, it);
+ break;
+ case Oarg:
+ case Oargc:
+ die("rv64_abi: Oarg/Oargc unreachable");
+ }
+ }
+
+ if (b == fn->start) {
+ for (; il; il = il->link)
+ emiti(ctx, il->i);
+ }
+ idup(ctx, b, ctx->curi, static_cast<ulong>(&ctx->insb[NIns] - ctx->curi));
+ } while (b != fn->start);
+
+ if (debug['A']) {
+ fprintf(ctx->dbg_buf, "\n> After ABI lowering:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+} // namespace rcc
diff --git a/src/backend/rv64/emit.cc b/src/backend/rv64/emit.cc
new file mode 100644
index 0000000..4a0c87e
--- /dev/null
+++ b/src/backend/rv64/emit.cc
@@ -0,0 +1,949 @@
+/* Source code for RCC-Legacy.
+ Copyright (C) 2026 Richard Qin
+
+This file is part of RCC.
+
+RCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+RCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with RCC; see the file LICENSE. If not see
+<http://www.gnu.org/licenses/>. */
+
+/// @file backend/rv64/emit.cc
+/// @brief RISC-V 64 (lp64d) assembly emission.
+
+#include "rcc/rv64.h"
+
+#include <cinttypes>
+#include <cstdio>
+
+namespace rcc {
+
+// Pseudo K values for omap wildcards.
+
+namespace {
+
+enum : short {
+ Ki = -1, // Matches Kw / Kl
+ Ka = -2, // Matches all cls
+};
+
+// Single instruction format templates.
+struct AsmFmt {
+ short op;
+ short cls;
+ const char* fmt;
+};
+
+constexpr AsmFmt kAsmFmt[] = {
+ {Oadd, Ki, "add%k %=, %0, %1"},
+ {Oadd, Ka, "fadd.%k %=, %0, %1"},
+ {Osub, Ki, "sub%k %=, %0, %1"},
+ {Osub, Ka, "fsub.%k %=, %0, %1"},
+ {Oneg, Ki, "neg%k %=, %0"},
+ {Oneg, Ka, "fneg.%k %=, %0"},
+ {Odiv, Ki, "div%k %=, %0, %1"},
+ {Odiv, Ka, "fdiv.%k %=, %0, %1"},
+ {Orem, Ki, "rem%k %=, %0, %1"},
+ {Orem, Kl, "rem %=, %0, %1"},
+ {Oudiv, Ki, "divu%k %=, %0, %1"},
+ {Ourem, Ki, "remu%k %=, %0, %1"},
+ {Omul, Ki, "mul%k %=, %0, %1"},
+ {Omul, Ka, "fmul.%k %=, %0, %1"},
+ {Oand, Ki, "and %=, %0, %1"},
+ {Oor, Ki, "or %=, %0, %1"},
+ {Oxor, Ki, "xor %=, %0, %1"},
+ {Osar, Ki, "sra%k %=, %0, %1"},
+ {Oshr, Ki, "srl%k %=, %0, %1"},
+ {Oshl, Ki, "sll%k %=, %0, %1"},
+ {Ocsltl, Ki, "slt %=, %0, %1"},
+ {Ocultl, Ki, "sltu %=, %0, %1"},
+ {Oceqs, Ki, "feq.s %=, %0, %1"},
+ {Ocges, Ki, "fge.s %=, %0, %1"},
+ {Ocgts, Ki, "fgt.s %=, %0, %1"},
+ {Ocles, Ki, "fle.s %=, %0, %1"},
+ {Oclts, Ki, "flt.s %=, %0, %1"},
+ {Oceqd, Ki, "feq.d %=, %0, %1"},
+ {Ocged, Ki, "fge.d %=, %0, %1"},
+ {Ocgtd, Ki, "fgt.d %=, %0, %1"},
+ {Ocled, Ki, "fle.d %=, %0, %1"},
+ {Ocltd, Ki, "flt.d %=, %0, %1"},
+ {Ostoreb, Kw, "sb %0, %M1"},
+ {Ostoreh, Kw, "sh %0, %M1"},
+ {Ostorew, Kw, "sw %0, %M1"},
+ {Ostorel, Ki, "sd %0, %M1"},
+ {Ostores, Kw, "fsw %0, %M1"},
+ {Ostored, Kw, "fsd %0, %M1"},
+ {Oloadsb, Ki, "lb %=, %M0"},
+ {Oloadub, Ki, "lbu %=, %M0"},
+ {Oloadsh, Ki, "lh %=, %M0"},
+ {Oloaduh, Ki, "lhu %=, %M0"},
+ {Oloadsw, Ki, "lw %=, %M0"},
+ // rv64 sign-extends 32-bit values into 64-bit registers.
+ {Oloaduw, Kw, "lw %=, %M0"},
+ {Oloaduw, Kl, "lwu %=, %M0"},
+ {Oload, Kw, "lw %=, %M0"},
+ {Oload, Kl, "ld %=, %M0"},
+ {Oload, Ks, "flw %=, %M0"},
+ {Oload, Kd, "fld %=, %M0"},
+ {Oextsb, Ki, "sext.b %=, %0"},
+ {Oextub, Ki, "zext.b %=, %0"},
+ {Oextsh, Ki, "sext.h %=, %0"},
+ {Oextuh, Ki, "zext.h %=, %0"},
+ {Oextsw, Kl, "sext.w %=, %0"},
+ {Oextuw, Kl, "zext.w %=, %0"},
+ {Otruncd, Ks, "fcvt.s.d %=, %0"},
+ {Oexts, Kd, "fcvt.d.s %=, %0"},
+ {Ostosi, Kw, "fcvt.w.s %=, %0, rtz"},
+ {Ostosi, Kl, "fcvt.l.s %=, %0, rtz"},
+ {Ostoui, Kw, "fcvt.wu.s %=, %0, rtz"},
+ {Ostoui, Kl, "fcvt.lu.s %=, %0, rtz"},
+ {Odtosi, Kw, "fcvt.w.d %=, %0, rtz"},
+ {Odtosi, Kl, "fcvt.l.d %=, %0, rtz"},
+ {Odtoui, Kw, "fcvt.wu.d %=, %0, rtz"},
+ {Odtoui, Kl, "fcvt.lu.d %=, %0, rtz"},
+ {Oswtof, Ka, "fcvt.%k.w %=, %0"},
+ {Ouwtof, Ka, "fcvt.%k.wu %=, %0"},
+ {Osltof, Ka, "fcvt.%k.l %=, %0"},
+ {Oultof, Ka, "fcvt.%k.lu %=, %0"},
+ {Ocast, Kw, "fmv.x.w %=, %0"},
+ {Ocast, Kl, "fmv.x.d %=, %0"},
+ {Ocast, Ks, "fmv.w.x %=, %0"},
+ {Ocast, Kd, "fmv.d.x %=, %0"},
+ {Ocopy, Ki, "mv %=, %0"},
+ {Ocopy, Ka, "fmv.%k %=, %0"},
+ {Oswap, Ki, "mv %?, %0\n\tmv %0, %1\n\tmv %1, %?"},
+ {Oswap, Ka, "fmv.%k %?, %0\n\tfmv.%k %0, %1\n\tfmv.%k %1, %?"},
+ {Oreqz, Ki, "seqz %=, %0"},
+ {Ornez, Ki, "snez %=, %0"},
+ {Ocall, Kw, "jalr %0"},
+ {NOp, 0, nullptr},
+};
+
+// Register names.
+
+constexpr auto kRegName = [] {
+ std::array<const char*, FT11 + 1> a{};
+ a[T0] = "t0";
+ a[T1] = "t1";
+ a[T2] = "t2";
+ a[T3] = "t3";
+ a[T4] = "t4";
+ a[T5] = "t5";
+ a[A0] = "a0";
+ a[A1] = "a1";
+ a[A2] = "a2";
+ a[A3] = "a3";
+ a[A4] = "a4";
+ a[A5] = "a5";
+ a[A6] = "a6";
+ a[A7] = "a7";
+ a[S1] = "s1";
+ a[S2] = "s2";
+ a[S3] = "s3";
+ a[S4] = "s4";
+ a[S5] = "s5";
+ a[S6] = "s6";
+ a[S7] = "s7";
+ a[S8] = "s8";
+ a[S9] = "s9";
+ a[S10] = "s10";
+ a[S11] = "s11";
+ a[FP] = "fp";
+ a[SP] = "sp";
+ a[GP] = "gp";
+ a[TP] = "tp";
+ a[RA] = "ra";
+ a[FT0] = "ft0";
+ a[FT1] = "ft1";
+ a[FT2] = "ft2";
+ a[FT3] = "ft3";
+ a[FT4] = "ft4";
+ a[FT5] = "ft5";
+ a[FT6] = "ft6";
+ a[FT7] = "ft7";
+ a[FT8] = "ft8";
+ a[FT9] = "ft9";
+ a[FT10] = "ft10";
+ a[FA0] = "fa0";
+ a[FA1] = "fa1";
+ a[FA2] = "fa2";
+ a[FA3] = "fa3";
+ a[FA4] = "fa4";
+ a[FA5] = "fa5";
+ a[FA6] = "fa6";
+ a[FA7] = "fa7";
+ a[FS0] = "fs0";
+ a[FS1] = "fs1";
+ a[FS2] = "fs2";
+ a[FS3] = "fs3";
+ a[FS4] = "fs4";
+ a[FS5] = "fs5";
+ a[FS6] = "fs6";
+ a[FS7] = "fs7";
+ a[FS8] = "fs8";
+ a[FS9] = "fs9";
+ a[FS10] = "fs10";
+ a[FS11] = "fs11";
+ a[T6] = "t6";
+ a[FT11] = "ft11";
+ return a;
+}();
+
+// Register name access helper.
+static inline const char* regname(int r) {
+ return kRegName[static_cast<size_t>(r)];
+}
+
+constexpr char kClsChar[] = {'w', 'l', 's', 'd'};
+
+// Slot offsets.
+int64_t slot_offset(Ref r, Fn* fn) {
+ int s = rsval(r);
+ assert(s <= fn->slot);
+ if (s < 0)
+ return 8 * -s;
+ return -4 * (fn->slot - s);
+}
+
+// CAddr symbol literals.
+
+void emit_addr_sym(Con* c, FILE* f) {
+ assert((c->sym.type & ~Sym::SExt) == Sym::SGlo);
+ fputs(str(c->sym.id), f);
+ if (c->bits.i)
+ fprintf(f, "+%" PRIi64, c->bits.i);
+}
+
+// Load into specific register.
+
+void load_sym_into(Con* c, const char* rn, FILE* f) {
+ char off[32];
+ switch (c->sym.type) {
+ case Sym::SGlo:
+ case Sym::SExt:
+ // medany compatibility.
+ fprintf(f, "\tla %s, ", rn);
+ emit_addr_sym(c, f);
+ fputc('\n', f);
+ return;
+ case Sym::SThr:
+ if (c->bits.i)
+ snprintf(off, sizeof(off), "+%" PRIi64, c->bits.i);
+ else
+ off[0] = 0;
+ fprintf(f, "\tlui %s, %%tprel_hi(%s)%s\n", rn, str(c->sym.id), off);
+ fprintf(f, "\tadd %s, %s, tp, %%tprel_add(%s)%s\n", rn, rn, str(c->sym.id), off);
+ fprintf(f, "\taddi %s, %s, %%tprel_lo(%s)%s\n", rn, rn, str(c->sym.id), off);
+ return;
+ case Sym::SExtThr:
+ die("extern thread unavailable on rv64");
+ }
+}
+
+void load_con_into(Con* c, int r, int k, FILE* f) {
+ const char* rn = regname(r);
+ switch (c->type) {
+ case Con::CAddr:
+ load_sym_into(c, rn, f);
+ return;
+ case Con::CBits: {
+ int64_t n = c->bits.i;
+ if (!KWIDE(k))
+ n = static_cast<int32_t>(n);
+ fprintf(f, "\tli %s, %" PRIi64 "\n", rn, n);
+ return;
+ }
+ default:
+ die("invalid constant");
+ }
+}
+
+// Materialize memory operand into t6.
+
+void materialize_mem(Ref* pr, Fn* fn, FILE* f) {
+ Ref r = *pr;
+ if (rtype(r) == RCon) {
+ Con* c = &fn->con[r.val];
+ if (c->type == Con::CAddr && c->sym.type != Sym::SGlo) {
+ load_con_into(c, T6, Kl, f);
+ *pr = Ref::tmp(static_cast<uint>(T6));
+ }
+ }
+ if (rtype(r) == RSlot) {
+ int64_t s = slot_offset(r, fn);
+ if (s < -2048 || s > 2047) {
+ fprintf(f, "\tli t6, %" PRId64 "\n", s);
+ fputs("\tadd t6, fp, t6\n", f);
+ *pr = Ref::tmp(static_cast<uint>(T6));
+ }
+ }
+}
+
+// Format string expansion.
+
+void emit_reg_arg(Ref r, FILE* f) {
+ assert(isreg(r));
+ fputs(kRegName[r.val], f);
+}
+
+void emit_imm_or_reg(Ref r, Ins* /*i*/, Fn* fn, FILE* f) {
+ switch (rtype(r)) {
+ case RTmp:
+ emit_reg_arg(r, f);
+ return;
+ case RCon: {
+ Con* pc = &fn->con[r.val];
+ assert(pc->type == Con::CBits);
+ assert(pc->bits.i >= -2048 && pc->bits.i < 2048);
+ fprintf(f, "%d", static_cast<int>(pc->bits.i));
+ return;
+ }
+ default:
+ die("invalid second argument");
+ }
+}
+
+void emit_mem_arg(Ref r, Ins* i, Fn* fn, FILE* f) {
+ switch (rtype(r)) {
+ case RTmp:
+ fprintf(f, "0(%s)", kRegName[r.val]);
+ return;
+ case RCon: {
+ Con* pc = &fn->con[r.val];
+ assert(pc->type == Con::CAddr);
+ emit_addr_sym(pc, f);
+ // Store / float load pseudo-instructions need a scratch register.
+ if (isstore(i->op) || (isload(i->op) && KBASE(i->cls) == 1))
+ fputs(", t6", f);
+ return;
+ }
+ case RSlot: {
+ int64_t offset = slot_offset(r, fn);
+ assert(offset >= -2048 && offset <= 2047);
+ fprintf(f, "%d(fp)", static_cast<int>(offset));
+ return;
+ }
+ case RMem: {
+ // Base+const addressing from isel.
+ Mem* m = &fn->mem[r.val];
+ assert(isreg(m->base));
+ assert(req(m->index, Ref::none()));
+ int64_t off = m->offset.type == Con::CBits ? m->offset.bits.i : 0;
+ assert(off >= -2048 && off <= 2047);
+ fprintf(f, "%d(%s)", static_cast<int>(off), kRegName[m->base.val]);
+ return;
+ }
+ default:
+ die("invalid address argument");
+ }
+}
+
+void emit_fmt(const char* s, Ins* i, Fn* fn, FILE* f) {
+ int k = i->cls;
+ fputc('\t', f);
+ for (;;) {
+ // Process characters or escape sequences.
+ char c;
+ while ((c = *s++) != '%') {
+ if (!c) {
+ fputc('\n', f);
+ return;
+ }
+ fputc(c, f);
+ }
+ char e = *s++;
+ switch (e) {
+ case '?':
+ fputs(KBASE(k) == 0 ? "t6" : "ft11", f);
+ break;
+ case 'k':
+ if (k != Kl)
+ fputc(kClsChar[k], f);
+ break;
+ case '=':
+ emit_reg_arg(i->to, f);
+ break;
+ case '0':
+ emit_reg_arg(i->arg[0], f);
+ break;
+ case '1':
+ emit_imm_or_reg(i->arg[1], i, fn, f);
+ break;
+ case 'M': {
+ char sel = *s++;
+ assert(sel == '0' || sel == '1');
+ emit_mem_arg(i->arg[sel - '0'], i, fn, f);
+ break;
+ }
+ default:
+ die("invalid escape");
+ }
+ }
+}
+
+// Table-driven emit dispatch.
+
+void emit_from_table(Ins* i, Fn* fn, FILE* f) {
+ for (int o = 0;; ++o) {
+ if (kAsmFmt[o].op == NOp)
+ die("no match for %s(%c) to=t%d/v%u a0=t%d/v%u a1=t%d/v%u || %s", optab[i->op].name,
+ "wlsd"[i->cls], rtype(i->to), i -> to.val, rtype(i->arg[0]), i->arg[0].val,
+ rtype(i->arg[1]), i->arg[1].val, die_ctx_str(nullptr, i));
+ if (kAsmFmt[o].op != static_cast<short>(i->op))
+ continue;
+ short tc = kAsmFmt[o].cls;
+ if (tc == static_cast<short>(i->cls) || tc == Ka || (tc == Ki && KBASE(i->cls) == 0)) {
+ emit_fmt(kAsmFmt[o].fmt, i, fn, f);
+ return;
+ }
+ }
+}
+
+// Single instruction dispatch.
+
+void emit_ins(Ins* i, Fn* fn, FILE* f) {
+ // Store with constant 0.
+ if (INRANGE(i->op, Ostoreb, Ostorel) && rtype(i->arg[0]) == RCon) {
+ Con* c = &fn->con[i->arg[0].val];
+ if (c->type == Con::CBits && c->bits.i == 0) {
+ materialize_mem(&i->arg[1], fn, f);
+ const char* mn;
+ switch (i->op) {
+ case Ostoreb:
+ mn = "sb";
+ break;
+ case Ostoreh:
+ mn = "sh";
+ break;
+ case Ostorew:
+ mn = "sw";
+ break;
+ case Ostorel:
+ mn = "sd";
+ break;
+ default:
+ die("unreachable");
+ }
+ fprintf(f, "\t%s zero, ", mn);
+ emit_mem_arg(i->arg[1], i, fn, f);
+ fputc('\n', f);
+ return;
+ }
+ }
+
+ switch (i->op) {
+ default:
+ if (isload(i->op))
+ materialize_mem(&i->arg[0], fn, f);
+ else if (isstore(i->op))
+ materialize_mem(&i->arg[1], fn, f);
+ emit_from_table(i, fn, f);
+ return;
+
+ case Ocopy: {
+ if (req(i->to, i->arg[0]))
+ return;
+
+ if (rtype(i->to) == RSlot) {
+ int rt = rtype(i->arg[0]);
+ if (rt == RSlot) {
+ // SLOT -> SLOT bridge.
+ int64_t off_src = slot_offset(i->arg[0], fn);
+ int64_t off_dst = slot_offset(i->to, fn);
+ const char* scratch = KBASE(i->cls) ? "ft11" : "t6";
+ const char* ld;
+ const char* st;
+ switch (i->cls) {
+ case Kw:
+ ld = "lw";
+ st = "sw";
+ break;
+ case Kl:
+ ld = "ld";
+ st = "sd";
+ break;
+ case Ks:
+ ld = "flw";
+ st = "fsw";
+ break;
+ case Kd:
+ ld = "fld";
+ st = "fsd";
+ break;
+ default:
+ die("Ocopy SLOT->SLOT illegal cls=%d", i->cls);
+ }
+ // Check for large offsets.
+ if (off_src < -2048 || off_src > 2047 || off_dst < -2048 || off_dst > 2047)
+ die("Ocopy SLOT->SLOT large offset NYI off_src=%" PRId64 " off_dst=%" PRId64
+ " || %s",
+ off_src, off_dst, die_ctx_str(nullptr, i));
+ fprintf(f, "\t%s %s, %" PRId64 "(fp)\n", ld, scratch, off_src);
+ fprintf(f, "\t%s %s, %" PRId64 "(fp)\n", st, scratch, off_dst);
+ return;
+ }
+ if (rt == RCon)
+ die("unimplemented Ocopy SLOT-dst cls=%c to=t%d/v%u a0=t%d/v%u || %s",
+ "wlsd"[i->cls], rtype(i->to), i -> to.val, rtype(i->arg[0]), i->arg[0].val,
+ die_ctx_str(nullptr, i));
+ assert(isreg(i->arg[0]));
+ Ins ii = *i;
+ ii.arg[1] = ii.to;
+ ii.to = Ref::none();
+ switch (ii.cls) {
+ case Kw:
+ ii.op = Ostorew;
+ break;
+ case Kl:
+ ii.op = Ostorel;
+ break;
+ case Ks:
+ ii.op = Ostores;
+ break;
+ case Kd:
+ ii.op = Ostored;
+ break;
+ }
+ // Revert dopm Ks/Kd to Kw for Ostore*.
+ ii.cls = Kw;
+ materialize_mem(&ii.arg[1], fn, f);
+ emit_from_table(&ii, fn, f);
+ return;
+ }
+
+ assert(isreg(i->to));
+ switch (rtype(i->arg[0])) {
+ case RCon:
+ load_con_into(&fn->con[i->arg[0].val], static_cast<int>(i->to.val),
+ static_cast<int>(i->cls), f);
+ return;
+ case RSlot: {
+ Ins ii = *i;
+ ii.op = Oload;
+ materialize_mem(&ii.arg[0], fn, f);
+ emit_from_table(&ii, fn, f);
+ return;
+ }
+ default:
+ assert(isreg(i->arg[0]));
+ emit_from_table(i, fn, f);
+ return;
+ }
+ }
+
+ case Onop:
+ return;
+
+ case Oaddr: {
+ const char* rn = kRegName[i->to.val];
+ switch (rtype(i->arg[0])) {
+ case RSlot: {
+ int64_t s = slot_offset(i->arg[0], fn);
+ if (-s < 2048) {
+ fprintf(f, "\tadd %s, fp, %" PRId64 "\n", rn, s);
+ } else {
+ fprintf(f, "\tli %s, %" PRId64 "\n", rn, s);
+ fprintf(f, "\tadd %s, fp, %s\n", rn, rn);
+ }
+ return;
+ }
+ case RCon: {
+ // addr $sym generated from IRGen.
+ Con* c = &fn->con[i->arg[0].val];
+ assert(c->type == Con::CAddr);
+ load_sym_into(c, rn, f);
+ return;
+ }
+ default:
+ die("invalid Oaddr argument");
+ }
+ }
+
+ case Ocall:
+ switch (rtype(i->arg[0])) {
+ case RCon: {
+ Con* con = &fn->con[i->arg[0].val];
+ if (con->type != Con::CAddr || (con->sym.type & Sym::SThr) || con->bits.i)
+ die("invalid call argument");
+ fprintf(f, "\tcall %s\n", str(con->sym.id));
+ return;
+ }
+ case RTmp:
+ emit_fmt("jalr %0", i, fn, f);
+ return;
+ default:
+ die("invalid call argument");
+ }
+
+ case Osalloc:
+ emit_fmt("sub sp, sp, %0", i, fn, f);
+ if (!req(i->to, Ref::none()))
+ emit_fmt("mv %=, sp", i, fn, f);
+ return;
+
+ case Odbgloc:
+ emitdbgloc(i->arg[0].val, i->arg[1].val, f);
+ return;
+ }
+}
+
+// Function prologue / epilogue helpers.
+
+void save_clobbers(Fn* fn, FILE* f) {
+ int off = 0;
+ for (int* pr = rv64_rclob; *pr >= 0; ++pr) {
+ if (!(fn->reg & BIT(*pr)))
+ continue;
+ fprintf(f, "\t%s %s, %d(sp)\n", *pr < FT0 ? "sd" : "fsd", regname(*pr), off);
+ off += 8;
+ }
+}
+
+void restore_clobbers(Fn* fn, FILE* f) {
+ int off = 0;
+ for (int* pr = rv64_rclob; *pr >= 0; ++pr) {
+ if (!(fn->reg & BIT(*pr)))
+ continue;
+ fprintf(f, "\t%s %s, %d(sp)\n", *pr < FT0 ? "ld" : "fld", regname(*pr), off);
+ off += 8;
+ }
+}
+
+// Compute total stack frame size.
+int compute_frame_size(Fn* fn) {
+ int frame = (16 + 4 * fn->slot + 15) & ~15;
+ for (int* pr = rv64_rclob; *pr >= 0; ++pr) {
+ if (fn->reg & BIT(*pr))
+ frame += 8;
+ }
+ return (frame + 15) & ~15;
+}
+
+// Adjust sp by delta.
+void emit_sp_adjust(const char* op, const char* base, int delta, FILE* f) {
+ if (delta <= 2048) {
+ fprintf(f, "\t%s sp, %s, -%d\n", op, base, delta);
+ } else {
+ fprintf(f, "\tli t6, -%d\n\t%s sp, %s, t6\n", delta, op, base);
+ }
+}
+
+// Fused cmp+jnz -> single conditional branch.
+
+struct BrCfg {
+ const char* mn; // rv64 branch mnemonic.
+ bool swap; // Swap operands.
+};
+
+// Branch mnemonic for each condition code.
+constexpr BrCfg kBrCfg[NCmpI] = {
+ /* Cieq */ {"beq", false}, /* Cine */ {"bne", false},
+ /* Cisge */ {"bge", false}, /* Cisgt */ {"blt", true},
+ /* Cisle */ {"bge", true}, /* Cislt */ {"blt", false},
+ /* Ciuge */ {"bgeu", false}, /* Ciugt */ {"bltu", true},
+ /* Ciule */ {"bgeu", true}, /* Ciult */ {"bltu", false},
+};
+
+// Logical negation of condition codes.
+constexpr int kCmpNeg[NCmpI] = {
+ /* Cieq */ Cine, /* Cine */ Cieq, /* Cisge */ Cislt, /* Cisgt */ Cisle,
+ /* Cisle */ Cisgt, /* Cislt */ Cisge, /* Ciuge */ Ciult, /* Ciugt */ Ciule,
+ /* Ciule */ Ciugt, /* Ciult */ Ciuge,
+};
+
+// Resolve fused branch operand.
+const char* branch_operand(Ref r, int ck, Fn* fn, FILE* f, bool* slot_used, Blk* b) {
+ if (rtype(r) == RSlot) {
+ if (*slot_used)
+ die("fused branch: both operands spilled @%d", static_cast<int>(b->id));
+ Ins ii = {};
+ ii.arg[0] = r;
+ emit_fmt(KWIDE(ck) ? "ld t6, %M0" : "lw t6, %M0", &ii, fn, f);
+ *slot_used = true;
+ return kRegName[T6];
+ }
+ assert(isreg(r));
+ return kRegName[r.val];
+}
+
+} // namespace
+
+// rv64_emitfn: Function entry point.
+
+namespace {
+// Global block ID base.
+int g_blkid_base = 0;
+} // namespace
+
+void rv64_emitfn(Fn* fn, FILE* f) {
+ emitfnlnk(fn->name, &fn->lnk, f);
+
+ // Prologue.
+ if (fn->vararg) {
+ // TODO: only allocate for a* unused by named arguments.
+ fputs("\tadd sp, sp, -64\n", f);
+ for (int r = A0; r <= A7; ++r)
+ fprintf(f, "\tsd %s, %d(sp)\n", regname(r), 8 * (r - A0));
+ }
+ fputs("\tsd fp, -16(sp)\n", f);
+ fputs("\tsd ra, -8(sp)\n", f);
+ fputs("\tadd fp, sp, -16\n", f);
+
+ int frame = compute_frame_size(fn);
+ emit_sp_adjust("add", "sp", frame, f);
+ save_clobbers(fn, f);
+
+ // Body.
+ bool need_label = false;
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (need_label || b->npred > 1)
+ fprintf(f, ".L%d:\n", g_blkid_base + static_cast<int>(b->id));
+ for (Ins* i = b->ins; i != b->ins + b->nins; ++i)
+ emit_ins(i, fn, f);
+ need_label = true;
+
+ switch (b->jmp.type) {
+ case Jhlt:
+ fputs("\tebreak\n", f);
+ break;
+
+ case Jret0:
+ if (fn->dynalloc)
+ emit_sp_adjust("add", "fp", frame - 16, f);
+ restore_clobbers(fn, f);
+ fprintf(f,
+ "\tadd sp, fp, %d\n"
+ "\tld ra, 8(fp)\n"
+ "\tld fp, 0(fp)\n"
+ "\tret\n",
+ 16 + fn->vararg * 64);
+ break;
+
+ case Jjmp:
+ emit_jmp:
+ if (b->s1 != b->link)
+ fprintf(f, "\tj .L%d\n", g_blkid_base + static_cast<int>(b->s1->id));
+ else
+ need_label = false;
+ break;
+
+ case Jjnz: {
+ // Ordering optimization for fall-through.
+ bool neg = false;
+ if (b->link == b->s2) {
+ Blk* s = b->s1;
+ b->s1 = b->s2;
+ b->s2 = s;
+ neg = true;
+ }
+ if (rtype(b->jmp.arg) == RSlot) {
+ Ins ii = {};
+ ii.arg[0] = b->jmp.arg;
+ emit_fmt("lw t6, %M0", &ii, fn, f);
+ b->jmp.arg = Ref::tmp(static_cast<uint>(T6));
+ }
+ assert(isreg(b->jmp.arg));
+ fprintf(f, "\tb%sz %s, .L%d\n", neg ? "ne" : "eq", kRegName[b->jmp.arg.val],
+ g_blkid_base + static_cast<int>(b->s2->id));
+ goto emit_jmp;
+ }
+
+ default:
+ // Fused integer cmp+jnz.
+ if (b->jmp.type >= Jjfieq && b->jmp.type <= Jjfiult) {
+ int cc = static_cast<int>(b->jmp.type) - Jjfieq;
+ int ck = b->jmp.cls;
+
+ bool neg = false;
+ if (b->link == b->s2) {
+ Blk* s = b->s1;
+ b->s1 = b->s2;
+ b->s2 = s;
+ neg = true;
+ }
+ BrCfg cfg = kBrCfg[neg ? cc : kCmpNeg[cc]];
+
+ bool slot_used = false;
+ const char* r0 = branch_operand(b->jmp.arg, ck, fn, f, &slot_used, b);
+ const char* r1 = branch_operand(b->jmp.arg2, ck, fn, f, &slot_used, b);
+ if (cfg.swap) {
+ const char* t = r0;
+ r0 = r1;
+ r1 = t;
+ }
+ fprintf(f, "\t%s %s, %s, .L%d\n", cfg.mn, r0, r1,
+ g_blkid_base + static_cast<int>(b->s2->id));
+ goto emit_jmp;
+ }
+ break;
+ }
+ }
+
+ g_blkid_base += static_cast<int>(fn->nblk);
+ elf_emitfnfin(fn->name, f);
+}
+
+// Built-in runtime.
+
+namespace {
+
+void emit_runtime_memset(FILE* f) {
+ fprintf(f,
+ ".globl __rcc_memset\n"
+ "__rcc_memset:\n"
+ "\tandi a1, a1, 0xff\n"
+ "\tslli t0, a1, 8\n"
+ "\tor a1, a1, t0\n"
+ "\tslli t0, a1, 16\n"
+ "\tor a1, a1, t0\n"
+ "\tslli t0, a1, 32\n"
+ "\tor a1, a1, t0\n"
+ "\tmv t1, a0\n"
+ "\tli t2, 64\n"
+ ".Lrcc_ms64:\n"
+ "\tblt a2, t2, .Lrcc_ms8\n"
+ "\tsd a1, 0(t1)\n"
+ "\tsd a1, 8(t1)\n"
+ "\tsd a1, 16(t1)\n"
+ "\tsd a1, 24(t1)\n"
+ "\tsd a1, 32(t1)\n"
+ "\tsd a1, 40(t1)\n"
+ "\tsd a1, 48(t1)\n"
+ "\tsd a1, 56(t1)\n"
+ "\taddi t1, t1, 64\n"
+ "\taddi a2, a2, -64\n"
+ "\tj .Lrcc_ms64\n"
+ ".Lrcc_ms8:\n"
+ "\tli t2, 8\n"
+ ".Lrcc_ms8l:\n"
+ "\tblt a2, t2, .Lrcc_ms1\n"
+ "\tsd a1, 0(t1)\n"
+ "\taddi t1, t1, 8\n"
+ "\taddi a2, a2, -8\n"
+ "\tj .Lrcc_ms8l\n"
+ ".Lrcc_ms1:\n"
+ "\tbeqz a2, .Lrcc_msd\n"
+ "\tsb a1, 0(t1)\n"
+ "\taddi t1, t1, 1\n"
+ "\taddi a2, a2, -1\n"
+ "\tj .Lrcc_ms1\n"
+ ".Lrcc_msd:\n"
+ "\tret\n"
+ ".type __rcc_memset, @function\n"
+ ".size __rcc_memset, .-__rcc_memset\n\n");
+}
+
+void emit_runtime_memcpy(FILE* f) {
+ fprintf(f,
+ ".globl __rcc_memcpy\n"
+ "__rcc_memcpy:\n"
+ "\tmv t1, a0\n"
+ ".Lrcc_mc32:\n"
+ "\tli t2, 32\n"
+ "\tblt a2, t2, .Lrcc_mc8\n"
+ "\tld t3, 0(a1)\n"
+ "\tld t4, 8(a1)\n"
+ "\tld t5, 16(a1)\n"
+ "\tld t6, 24(a1)\n"
+ "\tsd t3, 0(t1)\n"
+ "\tsd t4, 8(t1)\n"
+ "\tsd t5, 16(t1)\n"
+ "\tsd t6, 24(t1)\n"
+ "\taddi a1, a1, 32\n"
+ "\taddi t1, t1, 32\n"
+ "\taddi a2, a2, -32\n"
+ "\tj .Lrcc_mc32\n"
+ ".Lrcc_mc8:\n"
+ "\tli t2, 8\n"
+ ".Lrcc_mc8l:\n"
+ "\tblt a2, t2, .Lrcc_mc1\n"
+ "\tld t3, 0(a1)\n"
+ "\tsd t3, 0(t1)\n"
+ "\taddi a1, a1, 8\n"
+ "\taddi t1, t1, 8\n"
+ "\taddi a2, a2, -8\n"
+ "\tj .Lrcc_mc8l\n"
+ ".Lrcc_mc1:\n"
+ "\tbeqz a2, .Lrcc_mcd\n"
+ "\tlb t3, 0(a1)\n"
+ "\tsb t3, 0(t1)\n"
+ "\taddi a1, a1, 1\n"
+ "\taddi t1, t1, 1\n"
+ "\taddi a2, a2, -1\n"
+ "\tj .Lrcc_mc1\n"
+ ".Lrcc_mcd:\n"
+ "\tret\n"
+ ".type __rcc_memcpy, @function\n"
+ ".size __rcc_memcpy, .-__rcc_memcpy\n\n");
+}
+
+void emit_runtime_memmove(FILE* f) {
+ fprintf(f,
+ ".globl __rcc_memmove\n"
+ "__rcc_memmove:\n"
+ "\tbgeu a0, a1, .Lrcc_mm_chk\n"
+ "\tj __rcc_memcpy\n"
+ ".Lrcc_mm_chk:\n"
+ "\tadd t0, a1, a2\n"
+ "\tbgeu a0, t0, __rcc_memcpy\n"
+ "\tadd t1, a0, a2\n"
+ "\tadd a1, a1, a2\n"
+ ".Lrcc_mmb32:\n"
+ "\tli t2, 32\n"
+ "\tblt a2, t2, .Lrcc_mmb8\n"
+ "\taddi t1, t1, -32\n"
+ "\taddi a1, a1, -32\n"
+ "\tld t3, 0(a1)\n"
+ "\tld t4, 8(a1)\n"
+ "\tld t5, 16(a1)\n"
+ "\tld t6, 24(a1)\n"
+ "\tsd t3, 0(t1)\n"
+ "\tsd t4, 8(t1)\n"
+ "\tsd t5, 16(t1)\n"
+ "\tsd t6, 24(t1)\n"
+ "\taddi a2, a2, -32\n"
+ "\tj .Lrcc_mmb32\n"
+ ".Lrcc_mmb8:\n"
+ "\tli t2, 8\n"
+ ".Lrcc_mmb8l:\n"
+ "\tblt a2, t2, .Lrcc_mmb1\n"
+ "\taddi t1, t1, -8\n"
+ "\taddi a1, a1, -8\n"
+ "\tld t3, 0(a1)\n"
+ "\tsd t3, 0(t1)\n"
+ "\taddi a2, a2, -8\n"
+ "\tj .Lrcc_mmb8l\n"
+ ".Lrcc_mmb1:\n"
+ "\tbeqz a2, .Lrcc_mmd\n"
+ "\taddi t1, t1, -1\n"
+ "\taddi a1, a1, -1\n"
+ "\tlb t3, 0(a1)\n"
+ "\tsb t3, 0(t1)\n"
+ "\taddi a2, a2, -1\n"
+ "\tj .Lrcc_mmb1\n"
+ ".Lrcc_mmd:\n"
+ "\tret\n"
+ ".type __rcc_memmove, @function\n"
+ ".size __rcc_memmove, .-__rcc_memmove\n\n");
+}
+
+} // namespace
+
+void rv64_emitfin(Ctx* ctx, FILE* f) {
+ fprintf(f, ".text\n");
+ emit_runtime_memset(f);
+ emit_runtime_memcpy(f);
+ emit_runtime_memmove(f);
+ elf_emitfin(ctx, f);
+}
+
+} // namespace rcc
diff --git a/src/backend/rv64/isel.cc b/src/backend/rv64/isel.cc
new file mode 100644
index 0000000..c2ded02
--- /dev/null
+++ b/src/backend/rv64/isel.cc
@@ -0,0 +1,518 @@
+/* Source code for RCC-Legacy.
+ Copyright (C) 2026 Richard Qin
+
+This file is part of RCC.
+
+RCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+RCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with RCC; see the file LICENSE. If not see
+<http://www.gnu.org/licenses/>. */
+
+/// @file isel.cc
+/// @brief RISC-V 64 instruction selection.
+
+#include "rcc/rv64.h"
+
+#include <cinttypes>
+#include <climits>
+#include <cstdio>
+
+namespace rcc {
+namespace {
+
+// Operand role classification.
+
+// is_mem_arg: Does the arg slot pointed to by r have "memory operand" semantics?
+bool is_mem_arg(Ref* r, int op, Ins* i) {
+ if (isload(op) || op == Ocall)
+ return r == &i->arg[0];
+ if (isstore(op))
+ return r == &i->arg[1];
+ return false;
+}
+
+// is_imm_arg: Is r the immediate slot for this op?
+bool is_imm_arg(Ref* r, int op, Ins* i) {
+ return rv64_op[op].imm && r == &i->arg[1];
+}
+
+// Compare op -> (sign, swap, neg) tuple.
+
+struct CmpCfg {
+ uint8_t sign; // 1=signed (Ocsltl), 0=unsigned (Ocultl)
+ uint8_t swap; // 1=swap arg0/arg1
+ uint8_t neg; // 1=negate i.to
+};
+
+constexpr CmpCfg kIntOrderCmp[8] = {
+ /* Cisge */ {1, 0, 1}, // a >= b ⇔ !(a < b)
+ /* Cisgt */ {1, 1, 0}, // a > b ⇔ (b < a)
+ /* Cisle */ {1, 1, 1}, // a <= b ⇔ !(b < a)
+ /* Cislt */ {1, 0, 0}, // a < b
+ /* Ciuge */ {0, 0, 1},
+ /* Ciugt */ {0, 1, 0},
+ /* Ciule */ {0, 1, 1},
+ /* Ciult */ {0, 0, 0},
+};
+
+// Utilities.
+
+// boolean negate: emit `xor pr, src, 1`
+void negate_to(Ctx* ctx, Ref* pr, Fn* fn) {
+ Ref src = newtmp(ctx, "isel", Kw, fn);
+ emit(ctx, Oxor, Kw, *pr, src, getcon(ctx, 1, fn));
+ *pr = src;
+}
+
+// fixarg: operand fixup.
+
+// Fix constant operand.
+void fix_con_arg(Ctx* ctx, Ref* r, int k, Ins* i, int op, Fn* fn) {
+ Con* c = &fn->con[r->val];
+
+ // CAddr as mem operand: no materialization needed.
+ if (c->type == Con::CAddr && is_mem_arg(r, op, i))
+ return;
+
+ // Integer immediate within I-form range.
+ if (KBASE(k) == 0 && c->type == Con::CBits && is_imm_arg(r, op, i)) {
+ if (-2048 <= c->bits.i && c->bits.i < 2048)
+ return;
+ }
+
+ // Integer store value of 0: emit `s* zero, ...` without materialization.
+ if (i && c->type == Con::CBits && c->bits.i == 0 && r == &i->arg[0] &&
+ INRANGE(op, Ostoreb, Ostorel)) {
+ return;
+ }
+
+ Ref dst = newtmp(ctx, "isel", k, fn);
+
+ if (KBASE(k) == 1) {
+ // Floating-point constants must be stashed to ro data section.
+ assert(c->type == Con::CBits);
+ int slot_id = stashbits(ctx, static_cast<bits>(c->bits.i), KWIDE(k) ? 8 : 4);
+
+ vgrow(ctx, &fn->con, static_cast<ulong>(++fn->ncon));
+ Con* nc = &fn->con[fn->ncon - 1];
+ *nc = {};
+ nc->type = Con::CAddr;
+
+ char buf[32];
+ snprintf(buf, sizeof(buf), "\"%sfp%d\"", T.asloc, slot_id);
+ nc->sym.id = intern(buf);
+
+ emit(ctx, Oload, k, dst, Ref::con(static_cast<uint>(nc - fn->con)), Ref::none());
+ } else {
+ // Integer constants out of imm range use Ocopy.
+ emit(ctx, Ocopy, k, dst, *r, Ref::none());
+ }
+
+ *r = dst;
+}
+
+// Operations that do not require sext.w when Kw is input.
+bool skip_sext_for_kw(int op) {
+ switch (op) {
+ // *w-form arithmetic.
+ case Oadd:
+ case Osub:
+ case Omul:
+ case Odiv:
+ case Orem:
+ case Oudiv:
+ case Ourem:
+ case Osar:
+ case Oshr:
+ case Oshl:
+ // Narrow stores.
+ case Ostoreb:
+ case Ostoreh:
+ case Ostorew:
+ // Extensions.
+ case Oextsb:
+ case Oextub:
+ case Oextsh:
+ case Oextuh:
+ case Oextsw:
+ case Oextuw:
+ // int->float conversions.
+ case Oswtof:
+ case Ouwtof:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Fix temporary operand.
+void fix_tmp_arg(Ctx* ctx, Ref* r, int k, Ins* i, int op, Fn* fn) {
+ if (isreg(*r))
+ return;
+
+ int s = fn->tmp[r->val].slot;
+ if (s != -1) {
+ // Tmp with slot allocated.
+ if (is_mem_arg(r, op, i)) {
+ *r = Ref::slot(s);
+ } else {
+ Ref dst = newtmp(ctx, "isel", k, fn);
+ emit(ctx, Oaddr, k, dst, Ref::slot(s), Ref::none());
+ *r = dst;
+ }
+ return;
+ }
+
+ // Tmp is Kl but used as Kw.
+ if (k == Kw && fn->tmp[r->val].cls == Kl) {
+ if (!skip_sext_for_kw(op)) {
+ Ref dst = newtmp(ctx, "isel", k, fn);
+ emit(ctx, Oextsw, Kl, dst, *r, Ref::none());
+ *r = dst;
+ }
+ // Skip sext.w.
+ return;
+ }
+
+ assert(k == fn->tmp[r->val].cls);
+}
+
+void fixarg(Ctx* ctx, Ref* r, int k, Ins* i, Fn* fn) {
+ int op = i ? static_cast<int>(i->op) : Ocopy;
+ switch (rtype(*r)) {
+ case RCon:
+ fix_con_arg(ctx, r, k, i, op, fn);
+ break;
+ case RTmp:
+ fix_tmp_arg(ctx, r, k, i, op, fn);
+ break;
+ default:
+ // RSlot/RType/RCall/RMem/RInt/RNone don't need fixing.
+ break;
+ }
+}
+
+// Fixarg both operands of the instruction.
+void fixarg2(Ctx* ctx, int k, Fn* fn) {
+ Ins* h = ctx->curi;
+ fixarg(ctx, &h->arg[0], k, h, fn);
+ fixarg(ctx, &h->arg[1], k, h, fn);
+}
+
+// Compare selection.
+
+void selcmp(Ctx* ctx, Ins i, int k, int op, Fn* fn) {
+ assert(op >= 0 && op < NCmp);
+
+ // (1) Integer eq/ne.
+ if (op == Cieq || op == Cine) {
+ Ref r = newtmp(ctx, "isel", k, fn);
+ emit(ctx, op == Cieq ? Oreqz : Ornez, static_cast<int>(i.cls), i.to, r, Ref::none());
+ emit(ctx, Oxor, k, r, i.arg[0], i.arg[1]);
+ fixarg2(ctx, k, fn);
+ return;
+ }
+
+ // (2) Integer ordered comparisons.
+ if (op < NCmpI) {
+ const CmpCfg cfg = kIntOrderCmp[op - Cisge];
+ i.op = static_cast<uint>(cfg.sign ? Ocsltl : Ocultl);
+ if (cfg.swap) {
+ Ref t = i.arg[0];
+ i.arg[0] = i.arg[1];
+ i.arg[1] = t;
+ }
+ if (cfg.neg)
+ negate_to(ctx, &i.to, fn);
+ emiti(ctx, i);
+ fixarg2(ctx, k, fn);
+ return;
+ }
+
+ // (3) Floating point comparisons.
+ int fop = op - NCmpI;
+
+ // Cfo (ordered) / Cfuo (unordered).
+ if (fop == Cfo || fop == Cfuo) {
+ if (fop == Cfuo)
+ negate_to(ctx, &i.to, fn);
+ Ref r0 = newtmp(ctx, "isel", static_cast<int>(i.cls), fn);
+ Ref r1 = newtmp(ctx, "isel", static_cast<int>(i.cls), fn);
+ int ceq_op = KWIDE(k) ? Oceqd : Oceqs;
+ emit(ctx, Oand, static_cast<int>(i.cls), i.to, r0, r1);
+ emit(ctx, ceq_op, static_cast<int>(i.cls), r0, i.arg[0], i.arg[0]);
+ fixarg2(ctx, k, fn);
+ emit(ctx, ceq_op, static_cast<int>(i.cls), r1, i.arg[1], i.arg[1]);
+ fixarg2(ctx, k, fn);
+ return;
+ }
+
+ // Cfne: a != b -> !(a == b).
+ if (fop == Cfne) {
+ i.op = static_cast<uint>(KWIDE(k) ? Oceqd : Oceqs);
+ negate_to(ctx, &i.to, fn);
+ }
+ // Remaining Cfeq/Cfge/Cfgt/Cfle/Cflt.
+
+ emiti(ctx, i);
+ fixarg2(ctx, k, fn);
+}
+
+// Load/store constant offset folding.
+
+Ref new_offset_mem(Ctx* ctx, Fn* fn, Ref base, int64_t off) {
+ vensure(ctx, &fn->mem, static_cast<ulong>(fn->nmem + 1), sizeof(Mem), PFn);
+ Mem* m = &fn->mem[fn->nmem];
+ *m = {};
+ m->offset.type = Con::CBits;
+ m->offset.bits.i = off;
+ m->base = base;
+ m->index = Ref::none();
+ m->scale = 1;
+ return Ref::mem(static_cast<uint>(fn->nmem++));
+}
+
+// Reverse fold Oaddr -> CAddr.
+[[maybe_unused]] void fold_addr_into_mem(Ctx* ctx, Ref* paddr, uint bid, Fn* fn) {
+ if (rtype(*paddr) == RTmp) {
+ Tmp* t = &fn->tmp[paddr->val];
+ Ins* def = t->def;
+ if (!def || static_cast<int>(def->op) != Oaddr)
+ return;
+ if (t->nuse != 1 || t->bid != bid)
+ return;
+ if (rtype(def->arg[0]) != RCon)
+ return;
+ Con* c = &fn->con[def->arg[0].val];
+ if (c->type != Con::CAddr)
+ return;
+ *paddr = def->arg[0];
+ def->op = Onop;
+ return;
+ }
+ if (rtype(*paddr) == RMem) {
+ Mem* m = &fn->mem[paddr->val];
+ if (rtype(m->base) != RTmp)
+ return;
+ if (!req(m->index, Ref::none()))
+ return;
+ if (m->offset.type != Con::CBits)
+ return;
+ Tmp* t = &fn->tmp[m->base.val];
+ Ins* def = t->def;
+ if (!def || static_cast<int>(def->op) != Oaddr)
+ return;
+ if (t->nuse != 1 || t->bid != bid)
+ return;
+ if (rtype(def->arg[0]) != RCon)
+ return;
+ Con* c = &fn->con[def->arg[0].val];
+ if (c->type != Con::CAddr)
+ return;
+ // Create new CAddr combining bits.
+ vgrow(ctx, &fn->con, static_cast<ulong>(fn->ncon + 1));
+ Con* nc = &fn->con[fn->ncon];
+ *nc = *c;
+ nc->bits.i = c->bits.i + m->offset.bits.i;
+ uint cidx = static_cast<uint>(fn->ncon++);
+ *paddr = Ref::con(cidx);
+ def->op = Onop;
+ return;
+ }
+}
+
+void fold_mem_offset(Ctx* ctx, Ref* paddr, uint bid, Fn* fn) {
+ if (rtype(*paddr) != RTmp)
+ return;
+ Tmp* t = &fn->tmp[paddr->val];
+ Ins* def = t->def;
+ if (!def || static_cast<int>(def->op) != Oadd || static_cast<int>(def->cls) != Kl)
+ return;
+ if (t->nuse != 1 || t->bid != bid)
+ return;
+
+ // Oadd is commutative.
+ Ref base, koff;
+ if (rtype(def->arg[0]) == RTmp && rtype(def->arg[1]) == RCon) {
+ base = def->arg[0];
+ koff = def->arg[1];
+ } else if (rtype(def->arg[0]) == RCon && rtype(def->arg[1]) == RTmp) {
+ base = def->arg[1];
+ koff = def->arg[0];
+ } else {
+ return;
+ }
+
+ Con* c = &fn->con[koff.val];
+ if (c->type != Con::CBits)
+ return;
+ int64_t off = c->bits.i;
+ if (off < -2048 || off > 2047)
+ return;
+
+ // base must be a normal tmp (value is address).
+ if (fn->tmp[base.val].slot != -1)
+ return;
+
+ *paddr = new_offset_mem(ctx, fn, base, off);
+ def->op = Onop;
+}
+
+// Single instruction selection.
+
+void sel(Ctx* ctx, Ins i, Blk* b, Fn* fn) {
+ // Oalloc{4,8,16} non-constant sizes.
+ if (INRANGE(i.op, Oalloc, Oalloc1)) {
+ Ins* head = ctx->curi - 1;
+ salloc(ctx, i.to, i.arg[0], fn);
+ fixarg(ctx, &head->arg[0], Kl, head, fn);
+ return;
+ }
+
+ int ck, cc;
+ if (iscmp(static_cast<int>(i.op), &ck, &cc)) {
+ selcmp(ctx, i, ck, cc, fn);
+ return;
+ }
+
+ // Consume Onop.
+ if (i.op == Onop)
+ return;
+
+ // Oaddr from IRGen.
+ if (i.op == Oaddr) {
+ emiti(ctx, i);
+ return;
+ }
+
+ // Load/store constant offset folding.
+ int op = static_cast<int>(i.op);
+ Ref* paddr = nullptr;
+ if (isload(op))
+ paddr = &i.arg[0];
+ else if (isstore(op))
+ paddr = &i.arg[1];
+ if (paddr) {
+ fold_mem_offset(ctx, paddr, b->id, fn);
+ }
+
+ emiti(ctx, i);
+ Ins* h = ctx->curi; // capture head.
+ fixarg(ctx, &h->arg[0], argcls(&i, 0), h, fn);
+ fixarg(ctx, &h->arg[1], argcls(&i, 1), h, fn);
+}
+
+// Jump selection.
+
+// cmp+jnz fusion.
+inline short cmp_to_jjfi(int cc) {
+ assert(cc >= 0 && cc < NCmpI);
+ return static_cast<short>(Jjfieq + cc);
+}
+
+void seljmp(Ctx* ctx, Blk* b, Fn* fn) {
+ if (b->jmp.type != Jjnz)
+ return;
+
+ if (rtype(b->jmp.arg) == RTmp) {
+ Tmp* t = &fn->tmp[b->jmp.arg.val];
+ Ins* def = t->def;
+ if (t->nuse == 1 && def && t->bid == b->id) {
+ int ck, cc;
+ if (iscmp(static_cast<int>(def->op), &ck, &cc) && cc < NCmpI) {
+ b->jmp.type = cmp_to_jjfi(cc);
+ b->jmp.cls = static_cast<short>(ck);
+ b->jmp.arg = def->arg[0];
+ b->jmp.arg2 = def->arg[1];
+ def->op = Onop;
+ fixarg(ctx, &b->jmp.arg, ck, nullptr, fn);
+ fixarg(ctx, &b->jmp.arg2, ck, nullptr, fn);
+ return;
+ }
+ }
+ }
+ fixarg(ctx, &b->jmp.arg, Kw, nullptr, fn);
+}
+
+// Pass A: fast alloc -> slot.
+
+void assign_fast_alloc_slots(Fn* fn) {
+ Blk* b = fn->start;
+
+ int align_bytes = 4;
+ for (int op = Oalloc; op <= Oalloc1; ++op, align_bytes *= 2) {
+ Ins* ie = b->ins + b->nins;
+ for (Ins* i = b->ins; i < ie; ++i) {
+ if (static_cast<int>(i->op) != op)
+ continue;
+ if (rtype(i->arg[0]) != RCon)
+ continue;
+
+ int64_t sz = fn->con[i->arg[0].val].bits.i;
+ if (sz < 0 || sz >= INT_MAX - 15)
+ err(const_cast<char*>("invalid alloc size %" PRId64), sz);
+
+ sz = (sz + align_bytes - 1) & -align_bytes;
+ sz /= 4;
+ if (sz > INT_MAX - fn->slot)
+ die("alloc too large");
+
+ fn->tmp[i->to.val].slot = fn->slot;
+ fn->slot += static_cast<int>(sz);
+
+ *i = Ins{};
+ i->op = Onop;
+ }
+ }
+}
+
+} // namespace
+
+// rv64_isel: Main entry point.
+
+void rv64_isel(Ctx* ctx, Fn* fn) {
+ assign_fast_alloc_slots(fn);
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ ctx->curi = ctx->insb + NIns;
+
+ // Phi nodes of successors.
+ Blk* succ[3] = {b->s1, b->s2, nullptr};
+ for (Blk** ps = succ; *ps; ++ps) {
+ for (Phi* p = (*ps)->phi; p; p = p->link) {
+ uint n = 0;
+ while (p->blk[n] != b) {
+ ++n;
+ assert(n < p->narg);
+ }
+ fixarg(ctx, &p->arg[n], static_cast<int>(p->cls), nullptr, fn);
+ }
+ }
+
+ seljmp(ctx, b, fn);
+
+ // Reverse iteration for emit prepending.
+ for (Ins* i = b->ins + b->nins; i != b->ins;) {
+ --i;
+ sel(ctx, *i, b, fn);
+ }
+
+ idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi));
+ }
+
+ if (debug['I']) {
+ fprintf(ctx->dbg_buf, "\n> After instruction selection:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+} // namespace rcc
diff --git a/src/backend/rv64/targ.cc b/src/backend/rv64/targ.cc
new file mode 100644
index 0000000..4d0ab8c
--- /dev/null
+++ b/src/backend/rv64/targ.cc
@@ -0,0 +1,142 @@
+/* Source code for RCC-Legacy.
+ Copyright (C) 2026 Richard Qin
+
+This file is part of RCC.
+
+RCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+RCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with RCC; see the file LICENSE. If not see
+<http://www.gnu.org/licenses/>. */
+
+/// @file targ.cc
+/// @brief RISC-V 64 lp64d target descriptor.
+
+#include "rcc/rv64.h"
+
+namespace rcc {
+
+// Per-op attribute table.
+Rv64Op rv64_op[NOp];
+
+namespace {
+struct Rv64OpInit {
+ Rv64OpInit() {
+ for (size_t i = 0; i < NOp; ++i)
+ rv64_op[i].imm = kops[i].imm;
+ }
+} rv64_op_init_;
+} // namespace
+
+// Caller-save list for rega/spill scanning.
+
+int rv64_rsave[] = {
+ // GP segment (NGPS = 14): args + temps
+ A0,
+ A1,
+ A2,
+ A3,
+ A4,
+ A5,
+ A6,
+ A7,
+ T0,
+ T1,
+ T2,
+ T3,
+ T4,
+ T5,
+
+ // FP segment (NFPS = 19): temps + args
+ FT0,
+ FT1,
+ FT2,
+ FT3,
+ FT4,
+ FT5,
+ FT6,
+ FT7,
+ FT8,
+ FT9,
+ FT10,
+ FA0,
+ FA1,
+ FA2,
+ FA3,
+ FA4,
+ FA5,
+ FA6,
+ FA7,
+
+ -1,
+};
+
+// Callee-save list for prologue/epilogue generation.
+
+int rv64_rclob[] = {
+ S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11, FS0,
+ FS1, FS2, FS3, FS4, FS5, FS6, FS7, FS8, FS9, FS10, FS11, -1,
+};
+
+// Global live registers bitmask.
+
+namespace {
+constexpr bits kRv64GlobLive = BIT(FP) | BIT(SP) | BIT(GP) | BIT(TP) | BIT(RA);
+
+// RV64 ISA has no memory operand instructions.
+int memargs_none(int /*op*/) {
+ return 0;
+}
+} // namespace
+
+// Target descriptor.
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wc++20-designator"
+#endif
+
+Target T_rv64 = {
+ .name = "rv64",
+ .apple = 0,
+ .windows = 0,
+ .gpr0 = T0,
+ .ngpr = NGPR,
+ .fpr0 = FT0,
+ .nfpr = NFPR,
+ .rglob = kRv64GlobLive,
+ .nrglob = 5,
+ .rsave = rv64_rsave,
+ .nrsave = {NGPS, NFPS},
+ .retregs = rv64_retregs,
+ .argregs = rv64_argregs,
+ .memargs = memargs_none,
+ .abi0 = elimsb,
+ .abi1 = rv64_abi,
+ .isel = rv64_isel,
+ .emitfn = rv64_emitfn,
+ .emitfin = rv64_emitfin,
+ .asloc = ".L",
+ .assym = "",
+ .cansel = 0,
+};
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+// Table size consistency checks.
+
+MAKESURE(rv64_rsave_size_ok,
+ sizeof(rv64_rsave) / sizeof(int) == static_cast<size_t>(NGPS + NFPS + 1));
+MAKESURE(rv64_rclob_size_ok, sizeof(rv64_rclob) / sizeof(int) == static_cast<size_t>(NCLR + 1));
+
+} // namespace rcc