/* 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 . */ /// @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(r.val & 3); int nfp = static_cast((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((r.val >> 4) & 15); int nfp = static_cast((r.val >> 8) & 15); int t5 = static_cast((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(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(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("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(8 * n); ++n; } c->nfp = 0; c->ngp = static_cast(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 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(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(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(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(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(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(gp - kGpReg); int passfp = static_cast(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(alloc(ctx, sizeof(*il))); int al = t->align - 2; // alignment logic. if (al < 0) al = 0; uint64_t sz = (t->size + 7) & ~static_cast(7); Ins ins = {}; ins.op = static_cast(Oalloc + al); ins.cls = Kl; ins.to = r; ins.arg[0] = getcon(ctx, static_cast(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(alloc(ctx, static_cast(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(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(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(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(c->reg[0])), r, Ref::none()); c->reg[0] = static_cast(r.val); } else { emit(ctx, Ocopy, c->cls[0], Ref::tmp(static_cast(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(c->reg[0])), i->arg[0], Ref::none()); } if (c->flags & kCptr) { emit(ctx, Oblit1, 0, Ref::none(), Ref::integer(static_cast(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(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(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(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(stk), fn), Ref::none()); } // Par lowering. Params selpar(Ctx* ctx, Fn* fn, Ins* i0, Ins* i1) { ArgClass* ca = static_cast(alloc(ctx, static_cast(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(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(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(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(&ctx->insb[NIns] - ctx->curi); int ioff = static_cast(i - b->ins); int n1 = static_cast(b->nins) - ioff; vgrow(ctx, &b->ins, static_cast(n0 + n1)); icpy(b->ins + n0, b->ins + ioff, static_cast(n1)); icpy(b->ins, ctx->curi, static_cast(n0)); b->nins = static_cast(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(&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