/* 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 isel.cc /// @brief RISC-V 64 instruction selection. #include "rcc/rv64.h" #include #include #include 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(c->bits.i), KWIDE(k) ? 8 : 4); vgrow(ctx, &fn->con, static_cast(++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(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(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(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(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(i.cls), fn); Ref r1 = newtmp(ctx, "isel", static_cast(i.cls), fn); int ceq_op = KWIDE(k) ? Oceqd : Oceqs; emit(ctx, Oand, static_cast(i.cls), i.to, r0, r1); emit(ctx, ceq_op, static_cast(i.cls), r0, i.arg[0], i.arg[0]); fixarg2(ctx, k, fn); emit(ctx, ceq_op, static_cast(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(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(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(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(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(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(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(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(def->op) != Oadd || static_cast(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(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(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(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(def->op), &ck, &cc) && cc < NCmpI) { b->jmp.type = cmp_to_jjfi(cc); b->jmp.cls = static_cast(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(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("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(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(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(ctx->insb + NIns - ctx->curi)); } if (debug['I']) { fprintf(ctx->dbg_buf, "\n> After instruction selection:\n"); printfn(fn, ctx->dbg_buf); } } } // namespace rcc