aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/backend/emit.cc304
-rw-r--r--src/backend/peephole.cc146
-rw-r--r--src/backend/rega.cc684
-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
-rw-r--r--src/backend/simpl.cc176
-rw-r--r--src/backend/spill.cc600
-rw-r--r--src/frontend/ast.cc214
-rw-r--r--src/frontend/lexer.l95
-rw-r--r--src/frontend/parser.y686
-rw-r--r--src/ir/irgen.cc728
-rw-r--r--src/ir/irgen_expr.cc328
-rw-r--r--src/ir/irgen_util.cc244
-rw-r--r--src/ir/print.cc388
-rw-r--r--src/main.cc386
-rw-r--r--src/mid/abi.cc45
-rw-r--r--src/mid/alias.cc445
-rw-r--r--src/mid/cfg.cc582
-rw-r--r--src/mid/copy.cc360
-rw-r--r--src/mid/dce.cc149
-rw-r--r--src/mid/dead_loop.cc227
-rw-r--r--src/mid/divopt.cc189
-rw-r--r--src/mid/fold.cc423
-rw-r--r--src/mid/gcm.cc373
-rw-r--r--src/mid/gvn.cc414
-rw-r--r--src/mid/ifopt.cc127
-rw-r--r--src/mid/inline.cc484
-rw-r--r--src/mid/licm.cc213
-rw-r--r--src/mid/live.cc198
-rw-r--r--src/mid/load.cc556
-rw-r--r--src/mid/loop_idiom.cc401
-rw-r--r--src/mid/loopinfo.cc226
-rw-r--r--src/mid/loopswap.cc469
-rw-r--r--src/mid/lsr.cc1001
-rw-r--r--src/mid/mem.cc653
-rw-r--r--src/mid/mulopt.cc208
-rw-r--r--src/mid/reassoc.cc436
-rw-r--r--src/mid/reduction_unroll.cc528
-rw-r--r--src/mid/scalar_repl.cc528
-rw-r--r--src/mid/sccp.cc449
-rw-r--r--src/mid/ssa.cc512
-rw-r--r--src/mid/storemrg.cc231
-rw-r--r--src/mid/tre.cc214
-rw-r--r--src/mid/unroll.cc847
-rw-r--r--src/mid/unswitch.cc447
-rw-r--r--src/util.cc778
48 files changed, 19991 insertions, 0 deletions
diff --git a/src/backend/emit.cc b/src/backend/emit.cc
new file mode 100644
index 0000000..6e1f971
--- /dev/null
+++ b/src/backend/emit.cc
@@ -0,0 +1,304 @@
+/* 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/emit.cc
+/// @brief Target-independent assembly/data/debug section output tools.
+
+#include "rcc/all.h"
+
+#include <cinttypes>
+#include <cstring>
+#include <vector>
+
+namespace rcc {
+
+// Section types.
+
+namespace {
+
+enum Sec : int { kSecText = 0, kSecData = 1, kSecBss = 2 };
+
+// Given (sec, thread), return the name/instruction string after .section.
+// When thread=true, SecText is unreachable.
+const char* section_name(int sec, bool thread) {
+ if (!thread) {
+ switch (sec) {
+ case kSecText:
+ return ".text";
+ case kSecData:
+ return ".data";
+ case kSecBss:
+ return ".bss";
+ }
+ } else {
+ switch (sec) {
+ case kSecText:
+ return ".abort \"unreachable\"";
+ case kSecData:
+ return ".section .tdata,\"awT\"";
+ case kSecBss:
+ return ".section .tbss,\"awT\"";
+ }
+ }
+ return nullptr; // unreachable
+}
+
+// Write ".globl / label / section switch" header. Apple branch has TLV trampolines.
+void write_lnk(char* n, Lnk* l, int sec_kind, FILE* f) {
+ const char* pfx = (n[0] == '"') ? "" : T.assym;
+ const char* sfx = "";
+
+ if (T.apple && l->thread) {
+ l->sec = const_cast<char*>("__DATA");
+ l->secf = const_cast<char*>("__thread_data,thread_local_regular");
+ sfx = "$tlv$init";
+ fputs(".section __DATA,__thread_vars,thread_local_variables\n", f);
+ fprintf(f, "%s%s:\n", pfx, n);
+ fprintf(f,
+ "\t.quad __tlv_bootstrap\n"
+ "\t.quad 0\n"
+ "\t.quad %s%s%s\n\n",
+ pfx, n, sfx);
+ }
+
+ if (l->sec) {
+ fprintf(f, ".section %s", l->sec);
+ if (l->secf)
+ fprintf(f, ",%s", l->secf);
+ } else {
+ fputs(section_name(sec_kind, l->thread != 0), f);
+ }
+ fputc('\n', f);
+
+ if (l->align)
+ fprintf(f, ".balign %d\n", l->align);
+ if (l->export_)
+ fprintf(f, ".globl %s%s\n", pfx, n);
+ fprintf(f, "%s%s%s:\n", pfx, n, sfx);
+}
+
+} // namespace
+
+void emitfnlnk(char* n, Lnk* l, FILE* f) {
+ write_lnk(n, l, kSecText, f);
+}
+
+// Data sections.
+
+namespace {
+int64_t g_data_zero = 0;
+
+struct DataDecl {
+ const char* decl;
+ int64_t mask;
+};
+
+constexpr DataDecl kDataDecl[] = {
+ /* DStart */ {nullptr, 0},
+ /* DEnd */ {nullptr, 0},
+ /* DB */ {"\t.byte", 0xffLL},
+ /* DH */ {"\t.short", 0xffffLL},
+ /* DW */ {"\t.int", 0xffffffffLL},
+ /* DL */ {"\t.quad", -1LL},
+ /* DZ */ {nullptr, 0},
+};
+
+} // namespace
+
+void emitdat(Dat* d, FILE* f) {
+ switch (d->type) {
+ case Dat::DStart:
+ g_data_zero = 0;
+ return;
+
+ case Dat::DEnd:
+ if (d->lnk->common) {
+ if (g_data_zero == -1)
+ die("invalid common data definition");
+ const char* p = (d->name[0] == '"') ? "" : T.assym;
+ fprintf(f, ".comm %s%s,%" PRId64, p, d->name, g_data_zero);
+ if (d->lnk->align)
+ fprintf(f, ",%d", d->lnk->align);
+ fputc('\n', f);
+ } else if (g_data_zero != -1) {
+ write_lnk(d->name, d->lnk, kSecBss, f);
+ fprintf(f, "\t.fill %" PRId64 ",1,0\n", g_data_zero);
+ }
+ return;
+
+ case Dat::DZ:
+ if (g_data_zero != -1)
+ g_data_zero += d->u.num;
+ else
+ fprintf(f, "\t.fill %" PRId64 ",1,0\n", d->u.num);
+ return;
+
+ case Dat::DB:
+ case Dat::DH:
+ case Dat::DW:
+ case Dat::DL:
+ break;
+ }
+
+ // Switch to .data and flush accumulated zeros on first non-zero content.
+ if (g_data_zero != -1) {
+ write_lnk(d->name, d->lnk, kSecData, f);
+ if (g_data_zero > 0)
+ fprintf(f, "\t.fill %" PRId64 ",1,0\n", g_data_zero);
+ g_data_zero = -1;
+ }
+
+ const DataDecl& dd = kDataDecl[d->type];
+ if (d->isstr) {
+ if (d->type != Dat::DB)
+ err(const_cast<char*>("strings only supported for 'b' currently"));
+ fprintf(f, "\t.ascii %s\n", d->u.str);
+ } else if (d->isref) {
+ const char* p = (d->u.ref.name[0] == '"') ? "" : T.assym;
+ fprintf(f, "%s %s%s%+" PRId64 "\n", dd.decl, p, d->u.ref.name, d->u.ref.off);
+ } else {
+ fprintf(f, "%s %" PRId64 "\n", dd.decl, d->u.num & dd.mask);
+ }
+}
+
+// FP constant pool (stashbits + emitfin).
+
+struct Asmbits {
+ bits n;
+ int size;
+ Asmbits* link;
+};
+
+int stashbits(Ctx* ctx, bits n, int size) {
+ assert(size == 4 || size == 8 || size == 16);
+ Asmbits** pb;
+ Asmbits* b;
+ int i = 0;
+ for (pb = &ctx->emit_stash; (b = *pb); pb = &b->link, ++i) {
+ if (size <= b->size && b->n == n)
+ return i;
+ }
+ b = static_cast<Asmbits*>(emalloc(sizeof(Asmbits)));
+ b->n = n;
+ b->size = size;
+ b->link = nullptr;
+ *pb = b;
+ return i;
+}
+
+namespace {
+
+// Layout by .quad / .quad 0 (16B) / .quad (8B) / .int (4B).
+void emit_fp_pool(Ctx* ctx, FILE* f, const char* sec[3]) {
+ if (!ctx->emit_stash)
+ return;
+ fprintf(f, "/* floating point constants */\n");
+
+ union {
+ int32_t i;
+ float f;
+ } u;
+
+ // Three passes from largest to smallest (16, 8, 4).
+ for (int lg = 4; lg >= 2; --lg) {
+ int width = 1 << lg;
+ int i = 0;
+ for (Asmbits* b = ctx->emit_stash; b; b = b->link, ++i) {
+ if (b->size != width)
+ continue;
+ fprintf(f,
+ ".section %s\n"
+ ".p2align %d\n"
+ "%sfp%d:",
+ sec[lg - 2], lg, T.asloc, i);
+ if (lg == 4) {
+ fprintf(f, "\n\t.quad %" PRId64 "\n\t.quad 0\n\n", static_cast<int64_t>(b->n));
+ } else if (lg == 3) {
+ double dv;
+ memcpy(&dv, &b->n, sizeof(dv));
+ fprintf(f, "\n\t.quad %" PRId64 " /* %f */\n\n", static_cast<int64_t>(b->n), dv);
+ } else /* lg == 2 */ {
+ u.i = static_cast<int32_t>(b->n);
+ fprintf(f, "\n\t.int %" PRId32 " /* %f */\n\n", u.i, static_cast<double>(u.f));
+ }
+ }
+ }
+
+ // Free linked list.
+ while (Asmbits* b = ctx->emit_stash) {
+ ctx->emit_stash = b->link;
+ free(b);
+ }
+}
+
+} // namespace
+
+void elf_emitfin(Ctx* ctx, FILE* f) {
+ static const char* sec[3] = {".rodata", ".rodata", ".rodata"};
+ emit_fp_pool(ctx, f, sec);
+ fprintf(f, ".section .note.GNU-stack,\"\",@progbits\n");
+}
+
+void elf_emitfnfin(char* fn, FILE* f) {
+ fprintf(f, ".type %s, @function\n", fn);
+ fprintf(f, ".size %s, .-%s\n", fn, fn);
+}
+
+void macho_emitfin(Ctx* ctx, FILE* f) {
+ static const char* sec[3] = {
+ "__TEXT,__literal4,4byte_literals",
+ "__TEXT,__literal8,8byte_literals",
+ "__TEXT,__literal16,16byte_literals",
+ };
+ emit_fp_pool(ctx, f, sec);
+}
+
+void pe_emitfin(Ctx* ctx, FILE* f) {
+ static const char* sec[3] = {".rodata", ".rodata", ".rodata"};
+ emit_fp_pool(ctx, f, sec);
+}
+
+// DWARF .file / .loc.
+
+namespace {
+std::vector<uint32_t> g_dbg_files; // intern id sequence.
+uint32_t g_dbg_curfile = 0; // 1-based (gas requires > 0).
+} // namespace
+
+void emitdbgfile(char* fn, FILE* f) {
+ uint32_t id = intern(fn);
+ for (size_t i = 0; i < g_dbg_files.size(); ++i) {
+ if (g_dbg_files[i] == id) {
+ g_dbg_curfile = static_cast<uint32_t>(i + 1);
+ return;
+ }
+ }
+ g_dbg_files.push_back(id);
+ g_dbg_curfile = static_cast<uint32_t>(g_dbg_files.size());
+ fprintf(f, ".file %u %s\n", g_dbg_curfile, fn);
+}
+
+void emitdbgloc(uint line, uint col, FILE* f) {
+ if (col != 0)
+ fprintf(f, "\t.loc %u %u %u\n", g_dbg_curfile, line, col);
+ else
+ fprintf(f, "\t.loc %u %u\n", g_dbg_curfile, line);
+}
+
+} // namespace rcc
diff --git a/src/backend/peephole.cc b/src/backend/peephole.cc
new file mode 100644
index 0000000..0c5e95f
--- /dev/null
+++ b/src/backend/peephole.cc
@@ -0,0 +1,146 @@
+/* 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 peephole.cc
+/// @brief Backend sliding-window peephole pass.
+
+#include "rcc/all.h"
+
+namespace rcc {
+
+namespace {
+
+// Store op to byte width.
+int store_size(int op) {
+ switch (op) {
+ case Ostoreb:
+ return 1;
+ case Ostoreh:
+ return 2;
+ case Ostorew:
+ case Ostores:
+ return 4;
+ case Ostorel:
+ case Ostored:
+ return 8;
+ default:
+ return 0;
+ }
+}
+
+// Check if store and load are compatible for forwarding.
+bool store_load_compatible(int sop, int lcls) {
+ switch (sop) {
+ case Ostorew:
+ return lcls == Kw;
+ case Ostorel:
+ return lcls == Kl;
+ case Ostores:
+ return lcls == Ks;
+ case Ostored:
+ return lcls == Kd;
+ default:
+ return false;
+ }
+}
+
+void peep_blk(Blk* b) {
+ uint n = b->nins;
+ if (n == 0)
+ return;
+
+ // Self Ocopy elimination.
+ for (uint k = 0; k < n; k++) {
+ Ins* i = &b->ins[k];
+ if (i->op == Ocopy && req(i->to, i->arg[0]) && rtype(i->to) != -1)
+ i->op = Onop;
+ }
+
+ // Window 2 mode.
+ auto next_live = [&](uint from) -> int {
+ for (uint k = from; k < n; k++)
+ if (b->ins[k].op != Onop)
+ return static_cast<int>(k);
+ return -1;
+ };
+
+ int p = next_live(0);
+ while (p >= 0) {
+ int q = next_live(static_cast<uint>(p) + 1);
+ if (q < 0)
+ break;
+ Ins* pi = &b->ins[p];
+ Ins* qi = &b->ins[q];
+
+ // Store + Load to same slot: keep store, change load to copy.
+ if (isstore(pi->op) && qi->op == Oload && rtype(pi->arg[1]) == RSlot &&
+ rtype(qi->arg[0]) == RSlot && req(pi->arg[1], qi->arg[0]) &&
+ store_load_compatible(pi->op, qi->cls) && rtype(pi->arg[0]) != RCon) {
+ // Skip forwarding if value is a constant.
+ qi->op = Ocopy;
+ qi->arg[0] = pi->arg[0];
+ qi->arg[1] = Ref::none();
+ // Keep cls and to.
+ p = q;
+ continue;
+ }
+
+ // Consecutive stores to same slot: drop first.
+ if (isstore(pi->op) && pi->op == qi->op && rtype(pi->arg[1]) == RSlot &&
+ rtype(qi->arg[1]) == RSlot && req(pi->arg[1], qi->arg[1]) && store_size(pi->op) > 0) {
+ pi->op = Onop;
+ p = q;
+ continue;
+ }
+
+ p = q;
+ }
+
+ // Redundant Oextsw elimination.
+ for (uint pidx = 0; pidx < n; pidx++) {
+ Ins* pi = &b->ins[pidx];
+ if (pi->op != Oextsw)
+ continue;
+ if (!isreg(pi->to) || !isreg(pi->arg[0]))
+ continue;
+ for (uint qidx = pidx + 1; qidx < n; qidx++) {
+ Ins* qi = &b->ins[qidx];
+ if (qi->op == Onop)
+ continue;
+ // Identical Oextsw.
+ if (qi->op == pi->op && req(qi->to, pi->to) && req(qi->arg[0], pi->arg[0]) &&
+ req(qi->arg[1], pi->arg[1])) {
+ qi->op = Onop;
+ continue;
+ }
+ // Anchor invalidated.
+ if (isreg(qi->to) && (req(qi->to, pi->to) || req(qi->to, pi->arg[0])))
+ break;
+ }
+ }
+}
+
+} // namespace
+
+void peephole(Ctx* /*ctx*/, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ peep_blk(b);
+}
+
+} // namespace rcc
diff --git a/src/backend/rega.cc b/src/backend/rega.cc
new file mode 100644
index 0000000..e4f9da1
--- /dev/null
+++ b/src/backend/rega.cc
@@ -0,0 +1,684 @@
+/* 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/rega.cc
+/// @brief Register allocation pass.
+
+#include "rcc/all.h"
+
+#include <algorithm>
+#include <climits>
+#include <cstdio>
+#include <cstring>
+
+namespace rcc {
+
+namespace {
+
+#ifdef TEST_PMOV
+#undef assert
+#define assert(x) assert_test(#x, x)
+#endif
+
+struct RMap {
+ int t[Tmp0];
+ int r[Tmp0];
+ int w[Tmp0]; /* wait list, for unmatched hints */
+ BSet b[1];
+ int n;
+};
+
+int* hint(Ctx* ctx, int t) {
+ return &ctx->rega_tmp[phicls(t, ctx->rega_tmp)].hint.r;
+}
+
+void sethint(Ctx* ctx, int t, int r) {
+ Tmp* p;
+
+ p = &ctx->rega_tmp[phicls(t, ctx->rega_tmp)];
+ if (p->hint.r == -1 || p->hint.w > ctx->rega_loop) {
+ p->hint.r = r;
+ p->hint.w = ctx->rega_loop;
+ ctx->rega_tmp[t].visit = -1;
+ }
+}
+
+void rcopy(RMap* ma, RMap* mb) {
+ memcpy(ma->t, mb->t, sizeof ma->t);
+ memcpy(ma->r, mb->r, sizeof ma->r);
+ memcpy(ma->w, mb->w, sizeof ma->w);
+ bscopy(ma->b, mb->b);
+ ma->n = mb->n;
+}
+
+int rfind(RMap* m, int t) {
+ int i;
+
+ for (i = 0; i < m->n; i++)
+ if (m->t[i] == t)
+ return m->r[i];
+ return -1;
+}
+
+Ref rref(Ctx* ctx, RMap* m, int t) {
+ int r, s;
+
+ r = rfind(m, t);
+ if (r == -1) {
+ s = ctx->rega_tmp[t].slot;
+ assert(s != -1 && "should have spilled");
+ return Ref::slot(s);
+ } else
+ return Ref::tmp(static_cast<uint>(r));
+}
+
+void radd(Ctx* ctx, RMap* m, int t, int r) {
+ assert((t >= Tmp0 || t == r) && "invalid temporary");
+ assert(((T.gpr0 <= r && r < T.gpr0 + T.ngpr) || (T.fpr0 <= r && r < T.fpr0 + T.nfpr)) &&
+ "invalid register");
+ assert(!bshas(m->b, static_cast<uint>(t)) && "temporary has mapping");
+ assert(!bshas(m->b, static_cast<uint>(r)) && "register already allocated");
+ assert(m->n <= T.ngpr + T.nfpr && "too many mappings");
+ bsset(m->b, static_cast<uint>(t));
+ bsset(m->b, static_cast<uint>(r));
+ m->t[m->n] = t;
+ m->r[m->n] = r;
+ m->n++;
+ ctx->rega_regu |= BIT(r);
+}
+
+Ref ralloctry(Ctx* ctx, RMap* m, int t, int try_) {
+ bits regs;
+ int h, r, r0, r1;
+
+ if (t < Tmp0) {
+ assert(bshas(m->b, static_cast<uint>(t)));
+ return Ref::tmp(static_cast<uint>(t));
+ }
+ if (bshas(m->b, static_cast<uint>(t))) {
+ r = rfind(m, t);
+ assert(r != -1);
+ return Ref::tmp(static_cast<uint>(r));
+ }
+ r = ctx->rega_tmp[t].visit;
+ if (r == -1 || bshas(m->b, static_cast<uint>(r)))
+ r = *hint(ctx, t);
+ if (r == -1 || bshas(m->b, static_cast<uint>(r))) {
+ if (try_)
+ return Ref::none();
+ regs = ctx->rega_tmp[phicls(t, ctx->rega_tmp)].hint.m;
+ regs |= m->b->t[0];
+ if (KBASE(ctx->rega_tmp[t].cls) == 0) {
+ r0 = T.gpr0;
+ r1 = r0 + T.ngpr;
+ } else {
+ r0 = T.fpr0;
+ r1 = r0 + T.nfpr;
+ }
+ for (r = r0; r < r1; r++)
+ if (!(regs & BIT(r)))
+ goto Found;
+ for (r = r0; r < r1; r++)
+ if (!bshas(m->b, static_cast<uint>(r)))
+ goto Found;
+ fprintf(stderr, "DBG no-more-regs: t=%d cls=%d range=[R%d,R%d) m->n=%d\n", t,
+ ctx->rega_tmp[t].cls, r0, r1, m->n);
+ fprintf(stderr, " m->t/r entries:\n");
+ for (int idx = 0; idx < m->n; ++idx)
+ fprintf(stderr, " [%d] t=%d -> r=R%d\n", idx, m->t[idx], m->r[idx]);
+ fprintf(stderr, " m->b set bits:");
+ for (int rr = 0; rr < 64; ++rr)
+ if (bshas(m->b, static_cast<uint>(rr)))
+ fprintf(stderr, " %d", rr);
+ fprintf(stderr, "\n");
+ err(const_cast<char*>("no more regs"));
+ }
+Found:
+ radd(ctx, m, t, r);
+ sethint(ctx, t, r);
+ ctx->rega_tmp[t].visit = r;
+ h = *hint(ctx, t);
+ if (h != -1 && h != r)
+ m->w[h] = t;
+ return Ref::tmp(static_cast<uint>(r));
+}
+
+inline Ref ralloc(Ctx* ctx, RMap* m, int t) {
+ return ralloctry(ctx, m, t, 0);
+}
+
+int rfree(RMap* m, int t) {
+ int i, r;
+
+ assert(t >= Tmp0 || !(BIT(t) & T.rglob));
+ if (!bshas(m->b, static_cast<uint>(t)))
+ return -1;
+ for (i = 0; m->t[i] != t; i++)
+ assert(i + 1 < m->n);
+ r = m->r[i];
+ bsclr(m->b, static_cast<uint>(t));
+ bsclr(m->b, static_cast<uint>(r));
+ m->n--;
+ memmove(&m->t[i], &m->t[i + 1], static_cast<size_t>(m->n - i) * sizeof m->t[0]);
+ memmove(&m->r[i], &m->r[i + 1], static_cast<size_t>(m->n - i) * sizeof m->r[0]);
+ assert(t >= Tmp0 || t == r);
+ return r;
+}
+
+void mdump(Ctx* ctx, RMap* m) {
+ int i;
+
+ for (i = 0; i < m->n; i++)
+ if (m->t[i] >= Tmp0)
+ fprintf(ctx->dbg_buf, " (%s, R%d)", ctx->rega_tmp[m->t[i]].name, m->r[i]);
+ fprintf(ctx->dbg_buf, "\n");
+}
+
+void pmadd(Ctx* ctx, Ref src, Ref dst, int k) {
+ if (ctx->rega_npm == NPm)
+ err(const_cast<char*>("no more pm slots"));
+ ctx->rega_pm[ctx->rega_npm].src = src;
+ ctx->rega_pm[ctx->rega_npm].dst = dst;
+ ctx->rega_pm[ctx->rega_npm].cls = k;
+ ctx->rega_npm++;
+}
+
+enum PMStat { ToMove, Moving, Moved };
+
+int pmrec(Ctx* ctx, enum PMStat* status, int i, int* k) {
+ int j, c;
+
+ /* note, this routine might emit
+ * too many large instructions
+ */
+ if (req(ctx->rega_pm[i].src, ctx->rega_pm[i].dst)) {
+ status[i] = Moved;
+ return -1;
+ }
+ assert(KBASE(ctx->rega_pm[i].cls) == KBASE(*k));
+ assert((Kw | Kl) == Kl && (Ks | Kd) == Kd);
+ *k |= ctx->rega_pm[i].cls;
+ for (j = 0; j < ctx->rega_npm; j++)
+ if (req(ctx->rega_pm[j].dst, ctx->rega_pm[i].src))
+ break;
+ switch (j == ctx->rega_npm ? Moved : status[j]) {
+ case Moving:
+ c = j; /* start of cycle */
+ emit(ctx, Oswap, *k, Ref::none(), ctx->rega_pm[i].src, ctx->rega_pm[i].dst);
+ break;
+ case ToMove:
+ status[i] = Moving;
+ c = pmrec(ctx, status, j, k);
+ if (c == i) {
+ c = -1; /* end of cycle */
+ break;
+ }
+ if (c != -1) {
+ emit(ctx, Oswap, *k, Ref::none(), ctx->rega_pm[i].src, ctx->rega_pm[i].dst);
+ break;
+ }
+ [[fallthrough]];
+ case Moved:
+ c = -1;
+ emit(ctx, Ocopy, ctx->rega_pm[i].cls, ctx->rega_pm[i].dst, ctx->rega_pm[i].src,
+ Ref::none());
+ break;
+ default:
+ die("unreachable");
+ }
+ status[i] = Moved;
+ return c;
+}
+
+void pmgen(Ctx* ctx) {
+ int i;
+ enum PMStat* status;
+
+ status = static_cast<enum PMStat*>(
+ alloc(ctx, static_cast<size_t>(ctx->rega_npm) * sizeof status[0]));
+ assert(!ctx->rega_npm || status[ctx->rega_npm - 1] == ToMove);
+ for (i = 0; i < ctx->rega_npm; i++)
+ if (status[i] == ToMove) {
+ int k = ctx->rega_pm[i].cls;
+ pmrec(ctx, status, i, &k);
+ }
+}
+
+void move(Ctx* ctx, int r, Ref to, RMap* m) {
+ int n, t, r1;
+
+ r1 = req(to, Ref::none()) ? -1 : rfree(m, static_cast<int>(to.val));
+ if (bshas(m->b, static_cast<uint>(r))) {
+ /* r is used and not by to */
+ assert(r1 != r);
+ for (n = 0; m->r[n] != r; n++)
+ assert(n + 1 < m->n);
+ t = m->t[n];
+ rfree(m, t);
+ bsset(m->b, static_cast<uint>(r));
+ ralloc(ctx, m, t);
+ bsclr(m->b, static_cast<uint>(r));
+ }
+ t = req(to, Ref::none()) ? r : static_cast<int>(to.val);
+ radd(ctx, m, t, r);
+}
+
+int regcpy(Ins* i) {
+ return i->op == Ocopy && isreg(i->arg[0]);
+}
+
+Ins* dopm(Ctx* ctx, Blk* b, Ins* i, RMap* m) {
+ RMap m0;
+ int n, r, r1, t, s;
+ Ins *i1, *ip;
+ bits def;
+
+ m0 = *m; /* okay since we don't use m0.b */
+ m0.b->t = 0;
+ i1 = ++i;
+ do {
+ i--;
+ move(ctx, static_cast<int>(i->arg[0].val), i->to, m);
+ } while (i != b->ins && regcpy(i - 1));
+ assert(m0.n <= m->n);
+ if (i != b->ins && (i - 1)->op == Ocall) {
+ def = T.retregs((i - 1)->arg[1], 0) | T.rglob;
+ for (r = 0; T.rsave[r] >= 0; r++)
+ if (!(BIT(T.rsave[r]) & def))
+ move(ctx, T.rsave[r], Ref::none(), m);
+ }
+ for (ctx->rega_npm = 0, n = 0; n < m->n; n++) {
+ t = m->t[n];
+ s = ctx->rega_tmp[t].slot;
+ r1 = m->r[n];
+ r = rfind(&m0, t);
+ if (r != -1)
+ pmadd(ctx, Ref::tmp(static_cast<uint>(r1)), Ref::tmp(static_cast<uint>(r)),
+ ctx->rega_tmp[t].cls);
+ else if (s != -1)
+ pmadd(ctx, Ref::tmp(static_cast<uint>(r1)), Ref::slot(s), ctx->rega_tmp[t].cls);
+ }
+ for (ip = i; ip < i1; ip++) {
+ if (!req(ip->to, Ref::none()))
+ rfree(m, static_cast<int>(ip->to.val));
+ r = static_cast<int>(ip->arg[0].val);
+ if (rfind(m, r) == -1)
+ radd(ctx, m, r, r);
+ }
+ pmgen(ctx);
+ return i;
+}
+
+int prio1(Ctx* ctx, Ref r1, Ref r2) {
+ /* trivial heuristic to begin with,
+ * later we can use the distance to
+ * the definition instruction
+ */
+ (void)r2;
+ return *hint(ctx, static_cast<int>(r1.val)) != -1;
+}
+
+void insert(Ctx* ctx, Ref* r, Ref** rs, int p) {
+ int i;
+
+ rs[i = p] = r;
+ while (i-- > 0 && prio1(ctx, *r, *rs[i])) {
+ rs[i + 1] = rs[i];
+ rs[i] = r;
+ }
+}
+
+void doblk(Ctx* ctx, Blk* b, RMap* cur) {
+ int t, x, r, rf, rt, nr;
+ bits rs;
+ Ins *i, *i1;
+ Mem* m;
+ Ref* ra[4];
+
+ if (rtype(b->jmp.arg) == RTmp)
+ b->jmp.arg = ralloc(ctx, cur, static_cast<int>(b->jmp.arg.val));
+ if (rtype(b->jmp.arg2) == RTmp)
+ b->jmp.arg2 = ralloc(ctx, cur, static_cast<int>(b->jmp.arg2.val));
+ ctx->curi = &ctx->insb[NIns];
+ for (i1 = &b->ins[b->nins]; i1 != b->ins;) {
+ emiti(ctx, *--i1);
+ i = ctx->curi;
+ rf = -1;
+ switch (i->op) {
+ case Ocall:
+ rs = T.argregs(i->arg[1], 0) | T.rglob;
+ for (r = 0; T.rsave[r] >= 0; r++)
+ if (!(BIT(T.rsave[r]) & rs))
+ rfree(cur, T.rsave[r]);
+ break;
+ case Ocopy:
+ if (regcpy(i)) {
+ ctx->curi++;
+ i1 = dopm(ctx, b, i1, cur);
+ ctx->rega_stmov += static_cast<uint>(i + 1 - ctx->curi);
+ continue;
+ }
+ if (isreg(i->to))
+ if (rtype(i->arg[0]) == RTmp)
+ sethint(ctx, static_cast<int>(i->arg[0].val), static_cast<int>(i->to.val));
+ [[fallthrough]];
+ default:
+ if (!req(i->to, Ref::none())) {
+ assert(rtype(i->to) == RTmp);
+ r = static_cast<int>(i->to.val);
+ if (r < Tmp0 && (BIT(r) & T.rglob))
+ break;
+ rf = rfree(cur, r);
+ if (rf == -1) {
+ assert(!isreg(i->to));
+ ctx->curi++;
+ continue;
+ }
+ i->to = Ref::tmp(static_cast<uint>(rf));
+ }
+ break;
+ }
+ for (x = 0, nr = 0; x < 2; x++)
+ switch (rtype(i->arg[x])) {
+ case RMem:
+ m = &ctx->rega_mem[i->arg[x].val];
+ if (rtype(m->base) == RTmp)
+ insert(ctx, &m->base, ra, nr++);
+ if (rtype(m->index) == RTmp)
+ insert(ctx, &m->index, ra, nr++);
+ break;
+ case RTmp:
+ insert(ctx, &i->arg[x], ra, nr++);
+ break;
+ }
+ for (r = 0; r < nr; r++)
+ *ra[r] = ralloc(ctx, cur, static_cast<int>(ra[r]->val));
+ if (i->op == Ocopy && req(i->to, i->arg[0]))
+ ctx->curi++;
+
+ /* try to change the register of a hinted
+ * temporary if rf is available */
+ if (rf != -1 && (t = cur->w[rf]) != 0)
+ if (!bshas(cur->b, static_cast<uint>(rf)) && *hint(ctx, t) == rf &&
+ (rt = rfree(cur, t)) != -1) {
+ ctx->rega_tmp[t].visit = -1;
+ ralloc(ctx, cur, t);
+ assert(bshas(cur->b, static_cast<uint>(rf)));
+ emit(ctx, Ocopy, ctx->rega_tmp[t].cls, Ref::tmp(static_cast<uint>(rt)),
+ Ref::tmp(static_cast<uint>(rf)), Ref::none());
+ ctx->rega_stmov += 1;
+ cur->w[rf] = 0;
+ for (r = 0; r < nr; r++)
+ if (req(*ra[r], Ref::tmp(static_cast<uint>(rt))))
+ *ra[r] = Ref::tmp(static_cast<uint>(rf));
+ /* one could iterate this logic with
+ * the newly freed rt, but in this case
+ * the above loop must be changed */
+ }
+ }
+ idup(ctx, b, ctx->curi, static_cast<ulong>(&ctx->insb[NIns] - ctx->curi));
+}
+
+/* comparison function to order temporaries
+ * for allocation at the end of blocks */
+int prio2(Ctx* ctx, int t1, int t2) {
+ if ((ctx->rega_tmp[t1].visit ^ ctx->rega_tmp[t2].visit) < 0) /* != signs */
+ return ctx->rega_tmp[t1].visit != -1 ? +1 : -1;
+ if ((*hint(ctx, t1) ^ *hint(ctx, t2)) < 0)
+ return *hint(ctx, t1) != -1 ? +1 : -1;
+ return static_cast<int>(ctx->rega_tmp[t1].cost) - static_cast<int>(ctx->rega_tmp[t2].cost);
+}
+
+} // namespace
+
+/* register allocation
+ * depends on rpo, phi, cost, (and obviously spill)
+ */
+void rega(Ctx* ctx, Fn* fn) {
+ int j, t, r, x, rl[Tmp0];
+ Blk *b, *b1, *s, ***ps, *blist, **blk, **bp;
+ RMap *end, *beg, cur, old, *m;
+ Ins* i;
+ Phi* p;
+ uint u, n;
+ Ref src, dst;
+
+ /* setup */
+ ctx->rega_stmov = 0;
+ ctx->rega_stblk = 0;
+ ctx->rega_regu = 0;
+ ctx->rega_tmp = fn->tmp;
+ ctx->rega_mem = fn->mem;
+ blk = static_cast<Blk**>(alloc(ctx, fn->nblk * sizeof blk[0]));
+ end = static_cast<RMap*>(alloc(ctx, fn->nblk * sizeof end[0]));
+ beg = static_cast<RMap*>(alloc(ctx, fn->nblk * sizeof beg[0]));
+ for (n = 0; n < fn->nblk; n++) {
+ bsinit(ctx, end[n].b, static_cast<uint>(fn->ntmp));
+ bsinit(ctx, beg[n].b, static_cast<uint>(fn->ntmp));
+ }
+ bsinit(ctx, cur.b, static_cast<uint>(fn->ntmp));
+ bsinit(ctx, old.b, static_cast<uint>(fn->ntmp));
+ (void)old; /* declares but doesn't use this, suppress -Wunused here */
+
+ ctx->rega_loop = INT_MAX;
+ for (t = 0; t < fn->ntmp; t++) {
+ ctx->rega_tmp[t].hint.r = t < Tmp0 ? t : -1;
+ ctx->rega_tmp[t].hint.w = ctx->rega_loop;
+ ctx->rega_tmp[t].visit = -1;
+ }
+ for (bp = blk, b = fn->start; b; b = b->link)
+ *bp++ = b;
+ /* qsort() comparison function to peel
+ * loop nests from inside out */
+ std::sort(blk, blk + fn->nblk, [](Blk* ba, Blk* bb) {
+ /* todo, evaluate if this order is really
+ * better than the simple postorder */
+ if (ba->loop == bb->loop)
+ return ba->id > bb->id;
+ return ba->loop > bb->loop;
+ });
+ for (b = fn->start, i = b->ins; i < &b->ins[b->nins]; i++)
+ if (i->op != Ocopy || !isreg(i->arg[0]))
+ break;
+ else {
+ assert(rtype(i->to) == RTmp);
+ sethint(ctx, static_cast<int>(i->to.val), static_cast<int>(i->arg[0].val));
+ }
+
+ /* assign registers */
+ for (bp = blk; bp < &blk[fn->nblk]; bp++) {
+ b = *bp;
+ n = b->id;
+ ctx->rega_loop = b->loop;
+ cur.n = 0;
+ bszero(cur.b);
+ memset(cur.w, 0, sizeof cur.w);
+ for (x = 0, t = Tmp0; bsiter(b->out, &t); t++) {
+ j = x++;
+ rl[j] = t;
+ while (j-- > 0 && prio2(ctx, t, rl[j]) > 0) {
+ rl[j + 1] = rl[j];
+ rl[j] = t;
+ }
+ }
+ for (r = 0; bsiter(b->out, &r) && r < Tmp0; r++)
+ radd(ctx, &cur, r, r);
+ for (j = 0; j < x; j++)
+ ralloctry(ctx, &cur, rl[j], 1);
+ for (j = 0; j < x; j++)
+ ralloc(ctx, &cur, rl[j]);
+ rcopy(&end[n], &cur);
+ doblk(ctx, b, &cur);
+ bscopy(b->in, cur.b);
+ for (p = b->phi; p; p = p->link)
+ if (rtype(p->to) == RTmp)
+ bsclr(b->in, p->to.val);
+ rcopy(&beg[n], &cur);
+ }
+
+ /* emit copies shared by multiple edges
+ * to the same block */
+ for (s = fn->start; s; s = s->link) {
+ if (s->npred <= 1)
+ continue;
+ m = &beg[s->id];
+
+ /* rl maps a register that is live at the
+ * beginning of s to the one used in all
+ * predecessors (if any, -1 otherwise) */
+ memset(rl, 0, sizeof rl);
+
+ /* to find the register of a phi in a
+ * predecessor, we have to find the
+ * corresponding argument */
+ for (p = s->phi; p; p = p->link) {
+ if (rtype(p->to) != RTmp || (r = rfind(m, static_cast<int>(p->to.val))) == -1)
+ continue;
+ for (u = 0; u < p->narg; u++) {
+ b = p->blk[u];
+ src = p->arg[u];
+ if (rtype(src) != RTmp)
+ continue;
+ x = rfind(&end[b->id], static_cast<int>(src.val));
+ if (x == -1) /* spilled */
+ continue;
+ rl[r] = (!rl[r] || rl[r] == x) ? x : -1;
+ }
+ if (rl[r] == 0)
+ rl[r] = -1;
+ }
+
+ /* process non-phis temporaries */
+ for (j = 0; j < m->n; j++) {
+ t = m->t[j];
+ r = m->r[j];
+ if (rl[r] || t < Tmp0 /* todo, remove this */)
+ continue;
+ for (bp = s->pred; bp < &s->pred[s->npred]; bp++) {
+ x = rfind(&end[(*bp)->id], t);
+ if (x == -1) /* spilled */
+ continue;
+ rl[r] = (!rl[r] || rl[r] == x) ? x : -1;
+ }
+ if (rl[r] == 0)
+ rl[r] = -1;
+ }
+
+ ctx->rega_npm = 0;
+ for (j = 0; j < m->n; j++) {
+ t = m->t[j];
+ r = m->r[j];
+ x = rl[r];
+ assert(x != 0 || t < Tmp0 /* todo, ditto */);
+ if (x > 0 && !bshas(m->b, static_cast<uint>(x))) {
+ pmadd(ctx, Ref::tmp(static_cast<uint>(x)), Ref::tmp(static_cast<uint>(r)),
+ ctx->rega_tmp[t].cls);
+ m->r[j] = x;
+ bsset(m->b, static_cast<uint>(x));
+ }
+ }
+ ctx->curi = &ctx->insb[NIns];
+ pmgen(ctx);
+ j = static_cast<int>(&ctx->insb[NIns] - ctx->curi);
+ if (j == 0)
+ continue;
+ ctx->rega_stmov += static_cast<uint>(j);
+ s->nins += static_cast<uint>(j);
+ i = static_cast<Ins*>(alloc(ctx, s->nins * sizeof(Ins)));
+ icpy(icpy(i, ctx->curi, static_cast<ulong>(j)), s->ins, s->nins - static_cast<uint>(j));
+ s->ins = i;
+ }
+
+ if (debug['R']) {
+ fprintf(ctx->dbg_buf, "\n> Register mappings:\n");
+ for (n = 0; n < fn->nblk; n++) {
+ b = fn->rpo[n];
+ fprintf(ctx->dbg_buf, "\t%-10s beg", b->name);
+ mdump(ctx, &beg[n]);
+ fprintf(ctx->dbg_buf, "\t end");
+ mdump(ctx, &end[n]);
+ }
+ fprintf(ctx->dbg_buf, "\n");
+ }
+
+ /* emit remaining copies in new blocks */
+ blist = 0;
+ for (b = fn->start;; b = b->link) {
+ Blk* null_blk = nullptr;
+ Blk** ps_arr[3] = {&b->s1, &b->s2, &null_blk};
+ ps = ps_arr;
+ for (; (s = **ps); ps++) {
+ ctx->rega_npm = 0;
+ for (p = s->phi; p; p = p->link) {
+ dst = p->to;
+ assert(rtype(dst) == RSlot || rtype(dst) == RTmp);
+ if (rtype(dst) == RTmp) {
+ r = rfind(&beg[s->id], static_cast<int>(dst.val));
+ if (r == -1)
+ continue;
+ dst = Ref::tmp(static_cast<uint>(r));
+ }
+ for (u = 0; p->blk[u] != b; u++)
+ assert(u + 1 < p->narg);
+ src = p->arg[u];
+ if (rtype(src) == RTmp)
+ src = rref(ctx, &end[b->id], static_cast<int>(src.val));
+ pmadd(ctx, src, dst, p->cls);
+ }
+ for (t = Tmp0; bsiter(s->in, &t); t++) {
+ src = rref(ctx, &end[b->id], t);
+ dst = rref(ctx, &beg[s->id], t);
+ pmadd(ctx, src, dst, ctx->rega_tmp[t].cls);
+ }
+ ctx->curi = &ctx->insb[NIns];
+ pmgen(ctx);
+ if (ctx->curi == &ctx->insb[NIns])
+ continue;
+ b1 = newblk(ctx);
+ b1->loop = (b->loop + s->loop) / 2;
+ b1->link = blist;
+ blist = b1;
+ fn->nblk++;
+ b1->name = strf(ctx, PFn, "%s_%s", b->name, s->name);
+ ctx->rega_stmov += static_cast<uint>(&ctx->insb[NIns] - ctx->curi);
+ ctx->rega_stblk += 1;
+ idup(ctx, b1, ctx->curi, static_cast<ulong>(&ctx->insb[NIns] - ctx->curi));
+ b1->jmp.type = Jjmp;
+ b1->s1 = s;
+ **ps = b1;
+ }
+ if (!b->link) {
+ b->link = blist;
+ break;
+ }
+ }
+ for (b = fn->start; b; b = b->link)
+ b->phi = 0;
+ fn->reg = ctx->rega_regu;
+
+ if (debug['R']) {
+ fprintf(ctx->dbg_buf, "\n> Register allocation statistics:\n");
+ fprintf(ctx->dbg_buf, "\tnew moves: %d\n", ctx->rega_stmov);
+ fprintf(ctx->dbg_buf, "\tnew blocks: %d\n", ctx->rega_stblk);
+ fprintf(ctx->dbg_buf, "\n> After register allocation:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+} // namespace rcc
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
diff --git a/src/backend/simpl.cc b/src/backend/simpl.cc
new file mode 100644
index 0000000..0ec2082
--- /dev/null
+++ b/src/backend/simpl.cc
@@ -0,0 +1,176 @@
+/* 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/simpl.cc
+/// @brief Frontend simplification pass.
+
+#include "rcc/all.h"
+
+#include <cinttypes>
+#include <climits>
+#include <cstdlib>
+
+namespace rcc {
+
+// Memory block copy expansion: Oblit pair -> explicit load/store chain.
+namespace {
+
+struct BlitTier {
+ int store_op;
+ int load_op;
+ int load_cls;
+ int width;
+};
+
+constexpr BlitTier kBlitTiers[] = {
+ {Ostorel, Oload, Kl, 8},
+ {Ostorew, Oload, Kw, 4},
+ {Ostoreh, Oloaduh, Kw, 2},
+ {Ostoreb, Oloadub, Kw, 1},
+};
+
+void blit(Ctx* ctx, Ref sd[2], int signed_sz, Fn* fn) {
+ bool fwd = signed_sz >= 0;
+ int sz = std::abs(signed_sz);
+ int off = fwd ? sz : 0;
+
+ for (const BlitTier& t : kBlitTiers) {
+ while (sz >= t.width) {
+ if (fwd)
+ off -= t.width;
+
+ Ref val = newtmp(ctx, "blt", Kl, fn);
+ Ref dst_addr = newtmp(ctx, "blt", Kl, fn);
+ Ref off_con = getcon(ctx, off, fn);
+
+ // Reverse emit: store <- dst_addr <- load <- src_addr.
+ emit(ctx, t.store_op, 0, Ref::none(), val, dst_addr);
+ emit(ctx, Oadd, Kl, dst_addr, sd[1], off_con);
+
+ Ref src_addr = newtmp(ctx, "blt", Kl, fn);
+ emit(ctx, t.load_op, t.load_cls, val, src_addr, Ref::none());
+ emit(ctx, Oadd, Kl, src_addr, sd[0], off_con);
+
+ if (!fwd)
+ off += t.width;
+ sz -= t.width;
+ }
+ }
+}
+
+inline bool is_pow2(uint64_t v) {
+ return v != 0 && (v & (v - 1)) == 0;
+}
+
+// Single instruction simplification: Oblit1 / Oudiv / Ourem.
+void simplify_one(Ctx* ctx, Ins** pi, bool* rebuilt, Blk* b, Fn* fn) {
+ Ins* i = *pi;
+
+ switch (i->op) {
+ case Oblit1: {
+ assert(i > b->ins);
+ assert((i - 1)->op == Oblit0);
+
+ // First rebuild trigger: move unprocessed tail to insb.
+ if (!*rebuilt) {
+ ctx->curi = ctx->insb + NIns;
+ ulong tail = static_cast<ulong>((b->ins + b->nins) - (i + 1));
+ ctx->curi -= tail;
+ icpy(ctx->curi, i + 1, tail);
+ *rebuilt = true;
+ }
+
+ blit(ctx, (i - 1)->arg, rsval(i->arg[0]), fn);
+ *pi = i - 1; // Retreat pointer to Oblit0.
+ return;
+ }
+
+ case Oudiv:
+ case Ourem: {
+ // Unsigned integer + constant divisor + pow2.
+ if (KBASE(i->cls) != 0)
+ break;
+ if (rtype(i->arg[1]) != RCon)
+ break;
+ Con* c = &fn->con[i->arg[1].val];
+ if (c->type != Con::CBits)
+ break;
+ if (!is_pow2(static_cast<uint64_t>(c->bits.i)))
+ break;
+
+ int n = lowest_bit(static_cast<bits>(c->bits.i));
+ if (i->op == Ourem) {
+ i->op = Oand;
+ i->arg[1] = getcon(ctx, (1LL << n) - 1, fn);
+ } else {
+ i->op = Oshr;
+ i->arg[1] = getcon(ctx, n, fn);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ if (*rebuilt)
+ emiti(ctx, *i);
+}
+
+} // namespace
+
+void simpl(Ctx* ctx, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ bool rebuilt = false;
+ for (Ins* i = b->ins + b->nins; i != b->ins;) {
+ --i;
+ simplify_one(ctx, &i, &rebuilt, b, fn);
+ }
+ if (rebuilt)
+ idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi));
+ }
+}
+
+// salloc: Dynamic alloca expanded on stack. Called by rv64_isel.
+
+void salloc(Ctx* ctx, Ref rt, Ref rs, Fn* fn) {
+ fn->dynalloc = 1;
+
+ if (rtype(rs) == RCon) {
+ int64_t sz = fn->con[rs.val].bits.i;
+ if (sz < 0 || sz >= INT_MAX - 15)
+ err(const_cast<char*>("invalid alloc size %" PRId64), sz);
+ sz = (sz + 15) & -16;
+ emit(ctx, Osalloc, Kl, rt, getcon(ctx, sz, fn), Ref::none());
+ return;
+ }
+
+ // Non-constant size path.
+ if (rtype(rs) == RTmp && fn->tmp[rs.val].slot != -1)
+ err(const_cast<char*>("unlikely alloc argument %%%s for %%%s"), fn->tmp[rs.val].name,
+ fn->tmp[rt.val].name);
+
+ Ref r0 = newtmp(ctx, "isel", Kl, fn);
+ Ref r1 = newtmp(ctx, "isel", Kl, fn);
+ emit(ctx, Osalloc, Kl, rt, r0, Ref::none());
+ emit(ctx, Oand, Kl, r0, r1, getcon(ctx, -16, fn));
+ emit(ctx, Oadd, Kl, r1, rs, getcon(ctx, 15, fn));
+}
+
+} // namespace rcc
diff --git a/src/backend/spill.cc b/src/backend/spill.cc
new file mode 100644
index 0000000..360dcb3
--- /dev/null
+++ b/src/backend/spill.cc
@@ -0,0 +1,600 @@
+/* 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/spill.cc
+/// @brief Register spilling pass.
+
+#include "rcc/all.h"
+
+#include <algorithm>
+#include <climits>
+#include <cstdio>
+#include <vector>
+
+namespace rcc {
+
+// fillcost
+
+namespace {
+
+// loopiter callback: aggregate gen/nlive of loop body to loop header.
+void aggreg(Blk* hd, Blk* b) {
+ bsunion(hd->gen, b->gen);
+ for (int k = 0; k < 2; ++k)
+ if (b->nlive[k] > hd->nlive[k])
+ hd->nlive[k] = b->nlive[k];
+}
+
+// Single Ref use accumulation.
+void tmpuse(Ref r, int use, int loop, Fn* fn) {
+ if (rtype(r) == RMem) {
+ Mem* m = &fn->mem[r.val];
+ tmpuse(m->base, 1, loop, fn);
+ tmpuse(m->index, 1, loop, fn);
+ return;
+ }
+ if (rtype(r) == RTmp && r.val >= Tmp0) {
+ Tmp* t = &fn->tmp[r.val];
+ t->nuse += static_cast<uint>(use);
+ t->ndef += static_cast<uint>(!use);
+ t->cost += static_cast<uint>(loop);
+ }
+}
+
+void accumulate_phi_costs(Blk* b, Fn* fn) {
+ for (Phi* p = b->phi; p; p = p->link) {
+ Tmp* t = &fn->tmp[p->to.val];
+ tmpuse(p->to, 0, 0, fn);
+ for (uint a = 0; a < p->narg; ++a) {
+ int n = p->blk[a]->loop;
+ t->cost += static_cast<uint>(n);
+ tmpuse(p->arg[a], 1, n, fn);
+ }
+ }
+}
+
+void accumulate_ins_costs(Blk* b, Fn* fn) {
+ int n = b->loop;
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) {
+ tmpuse(i->to, 0, n, fn);
+ tmpuse(i->arg[0], 1, n, fn);
+ tmpuse(i->arg[1], 1, n, fn);
+ }
+ tmpuse(b->jmp.arg, 1, n, fn);
+ tmpuse(b->jmp.arg2, 1, n, fn); // Second operand for Jjfi/Jjff.
+}
+
+void dump_loop_info(Ctx* ctx, Fn* fn) {
+ FILE* f = ctx->dbg_buf;
+ fprintf(f, "\n> Loop information:\n");
+ for (Blk* b = fn->start; b; b = b->link) {
+ uint a;
+ for (a = 0; a < b->npred; ++a)
+ if (b->id <= b->pred[a]->id)
+ break;
+ if (a != b->npred) {
+ fprintf(f, "\t%-10s", b->name);
+ fprintf(f, " (% 3d ", b->nlive[0]);
+ fprintf(f, "% 3d) ", b->nlive[1]);
+ dumpts(b->gen, fn->tmp, f);
+ }
+ }
+}
+
+} // namespace
+
+void fillcost(Ctx* ctx, Fn* fn) {
+ loopiter(fn, aggreg);
+
+ if (debug['S'])
+ dump_loop_info(ctx, fn);
+
+ // Register cost = UINT_MAX (never spill); normal tmp accumulates from 0.
+ for (Tmp* t = fn->tmp; t - fn->tmp < fn->ntmp; ++t) {
+ t->cost = (t - fn->tmp < Tmp0) ? UINT_MAX : 0;
+ t->nuse = 0;
+ t->ndef = 0;
+ }
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ accumulate_phi_costs(b, fn);
+ accumulate_ins_costs(b, fn);
+ }
+
+ if (debug['S']) {
+ FILE* f = ctx->dbg_buf;
+ fprintf(f, "\n> Spill costs:\n");
+ for (int n = Tmp0; n < fn->ntmp; ++n)
+ fprintf(f, "\t%-10s %d\n", fn->tmp[n].name, fn->tmp[n].cost);
+ fprintf(f, "\n");
+ }
+}
+
+// spill
+
+namespace {
+
+// Per-call state.
+struct SpillCtx {
+ Ctx* ctx; // Back reference to ctx.
+ Tmp* tmp; // Cache of fn->tmp.
+ int ntmp;
+ int locs; // fn->slot entrance snapshot.
+ int slot4; // Next 4B slot.
+ int slot8; // Next 8B slot.
+ BSet mask[2]; // GPR and FPR bitsets.
+ BSet b2_scratch; // Scratch buffer for limit2.
+ std::vector<int> tarr; // Scratch buffer for limit.
+};
+
+// Allocate stack slot on demand.
+Ref alloc_spill_slot(SpillCtx& s, int t) {
+ assert(t >= Tmp0 && "cannot spill register");
+ int existing = s.tmp[t].slot;
+ if (existing != -1)
+ return Ref::slot(existing);
+
+ int pos;
+ if (KWIDE(s.tmp[t].cls)) {
+ pos = s.slot8;
+ if (s.slot4 == s.slot8)
+ s.slot4 += 2;
+ s.slot8 += 2;
+ } else {
+ pos = s.slot4;
+ if (s.slot4 == s.slot8) {
+ s.slot8 += 2;
+ s.slot4 += 1;
+ } else {
+ // 4B hole filled, jump to slot8.
+ s.slot4 = s.slot8;
+ }
+ }
+ pos += s.locs;
+ s.tmp[t].slot = pos;
+ return Ref::slot(pos);
+}
+
+// Limit bitset to at most k elements.
+void limit_one(SpillCtx& s, BSet* b, int k, BSet* f) {
+ int nt = static_cast<int>(bscount(b));
+ if (nt <= k)
+ return;
+
+ s.tarr.resize(static_cast<size_t>(nt));
+
+ // Pull elements from b to tarr, clear b.
+ int collected = 0;
+ int t = 0;
+ while (bsiter(b, &t)) {
+ bsclr(b, static_cast<uint>(t));
+ s.tarr[static_cast<size_t>(collected++)] = t;
+ ++t;
+ }
+ assert(collected == nt);
+
+ if (nt > 1) {
+ Tmp* tmp = s.tmp;
+ if (f) {
+ BSet* fst = f;
+ std::stable_sort(s.tarr.begin(), s.tarr.begin() + nt, [fst, tmp](int x, int y) {
+ int fx = bshas(fst, static_cast<uint>(x));
+ int fy = bshas(fst, static_cast<uint>(y));
+ if (fx != fy)
+ return fx > fy;
+ return tmp[x].cost > tmp[y].cost;
+ });
+ } else {
+ std::stable_sort(s.tarr.begin(), s.tarr.begin() + nt,
+ [tmp](int x, int y) { return tmp[x].cost > tmp[y].cost; });
+ }
+ }
+
+ int i = 0;
+ for (; i < k && i < nt; ++i)
+ bsset(b, static_cast<uint>(s.tarr[static_cast<size_t>(i)]));
+ for (; i < nt; ++i)
+ alloc_spill_slot(s, s.tarr[static_cast<size_t>(i)]);
+}
+
+// Limit GPR/FPR separately.
+void limit_two(SpillCtx& s, BSet* b1, int k1, int k2, BSet* f) {
+ BSet* b2 = &s.b2_scratch;
+ bscopy(b2, b1);
+ bsinter(b1, &s.mask[0]);
+ bsinter(b2, &s.mask[1]);
+ limit_one(s, b1, T.ngpr - k1, f);
+ {
+ int cnt = static_cast<int>(bscount(b2));
+ if (cnt > s.ctx->spill_fp_live_peak)
+ s.ctx->spill_fp_live_peak = cnt;
+ }
+ limit_one(s, b2, T.nfpr - k2, f);
+ bsunion(b1, b2);
+}
+
+// Set register hint for tmp in u.
+void sethint(SpillCtx& s, BSet* u, bits r) {
+ int t = Tmp0;
+ while (bsiter(u, &t)) {
+ s.tmp[phicls(t, s.tmp)].hint.m |= r;
+ ++t;
+ }
+}
+
+// Emit Oload for tmp in u \ v.
+void reloads(Ctx* ctx, SpillCtx& s, BSet* u, BSet* v) {
+ int t = Tmp0;
+ while (bsiter(u, &t)) {
+ if (!bshas(v, static_cast<uint>(t)))
+ emit(ctx, Oload, s.tmp[t].cls, Ref::tmp(static_cast<uint>(t)), alloc_spill_slot(s, t),
+ Ref::none());
+ ++t;
+ }
+}
+
+// Emit Ostore to SLOT(s) if s != -1.
+void store_to_slot(Ctx* ctx, SpillCtx& spc, Ref r, int slot_id) {
+ if (slot_id == -1)
+ return;
+ int cls = spc.tmp[r.val].cls;
+ emit(ctx, Ostorew + cls, 0, Ref::none(), r, Ref::slot(slot_id));
+}
+
+inline bool is_regcopy(Ins* i) {
+ return i->op == Ocopy && isreg(i->arg[0]);
+}
+
+// Process parallel moves (contiguous reg->tmp copies).
+Ins* spill_par_moves(Ctx* ctx, SpillCtx& s, Blk* b, Ins* i, BSet* v) {
+ BSet u[1];
+ bsinit(ctx, u, static_cast<uint>(s.ntmp));
+
+ // Backward scan of contiguous regcpy segment.
+ Ins* i1 = ++i;
+ do {
+ --i;
+ int t = i->to.val;
+ if (!req(i->to, Ref::none()))
+ if (bshas(v, static_cast<uint>(t))) {
+ bsclr(v, static_cast<uint>(t));
+ store_to_slot(ctx, s, i->to, s.tmp[t].slot);
+ }
+ bsset(v, i->arg[0].val);
+ } while (i != b->ins && is_regcopy(i - 1));
+
+ bscopy(u, v);
+
+ bits r;
+ if (i != b->ins && (i - 1)->op == Ocall) {
+ // Call site: return registers are dead, argregs must be preserved.
+ Ins* call = i - 1;
+ v->t[0] &= ~T.retregs(call->arg[1], nullptr);
+ limit_two(s, v, T.nrsave[0], T.nrsave[1], nullptr);
+ r = 0;
+ for (int n = 0; T.rsave[n] >= 0; ++n)
+ r |= BIT(T.rsave[n]);
+ v->t[0] |= T.argregs(call->arg[1], nullptr);
+ } else {
+ limit_two(s, v, 0, 0, nullptr);
+ r = v->t[0];
+ }
+ sethint(s, v, r);
+ reloads(ctx, s, u, v);
+
+ // Re-emit original regcpy in the segment.
+ do
+ emiti(ctx, *--i1);
+ while (i1 != i);
+ return i;
+}
+
+// Loop header path merge.
+void merge_pred(SpillCtx& s, BSet* u, Blk* bu, BSet* v, Blk* bv) {
+ if (bu->loop <= bv->loop) {
+ bsunion(u, v);
+ return;
+ }
+ int t = 0;
+ while (bsiter(v, &t)) {
+ if (s.tmp[t].slot == -1)
+ bsset(u, static_cast<uint>(t));
+ ++t;
+ }
+}
+
+// Compute exit set v.
+void compute_exit_set(SpillCtx& s, Blk* b, BSet* u, BSet* v, BSet* w) {
+ Blk* s1 = b->s1;
+ Blk* s2 = b->s2;
+ Blk* hd = nullptr;
+ if (s1 && s1->id <= b->id)
+ hd = s1;
+ if (s2 && s2->id <= b->id)
+ if (!hd || s2->id >= hd->id)
+ hd = s2;
+
+ if (hd) {
+ // Back-edge handling.
+ bszero(v);
+ hd->gen->t[0] |= T.rglob; // Global registers never spill.
+ for (int k = 0; k < 2; ++k) {
+ uint n = (k == 0) ? static_cast<uint>(T.ngpr) : static_cast<uint>(T.nfpr);
+ bscopy(u, b->out);
+ bsinter(u, &s.mask[k]);
+ bscopy(w, u);
+ bsinter(u, hd->gen);
+ bsdiff(w, hd->gen);
+ if (bscount(u) < n) {
+ int j = static_cast<int>(bscount(w)); // Live-through.
+ int l = hd->nlive[k];
+ if (k == 1 && static_cast<int>(bscount(w)) > s.ctx->spill_fp_live_peak)
+ s.ctx->spill_fp_live_peak = static_cast<int>(bscount(w));
+ limit_one(s, w, static_cast<int>(n) - (l - j), nullptr);
+ bsunion(u, w);
+ } else {
+ if (k == 1 && static_cast<int>(bscount(u)) > s.ctx->spill_fp_live_peak)
+ s.ctx->spill_fp_live_peak = static_cast<int>(bscount(u));
+ limit_one(s, u, static_cast<int>(n), nullptr);
+ }
+ bsunion(v, u);
+ }
+ } else if (s1) {
+ // Forward edge handling.
+ bszero(v);
+ liveon(w, b, s1);
+ merge_pred(s, v, b, w, s1);
+ if (s2) {
+ liveon(u, b, s2);
+ merge_pred(s, v, b, u, s2);
+ bsinter(w, u);
+ }
+ limit_two(s, v, 0, 0, w);
+ } else {
+ bscopy(v, b->out);
+ if (rtype(b->jmp.arg) == RCall)
+ v->t[0] |= T.retregs(b->jmp.arg, nullptr);
+ }
+}
+
+// Single instruction processing in main loop.
+
+// Process i->to.
+void process_def(Ins* i, BSet* v, BSet* w) {
+ if (req(i->to, Ref::none()))
+ return;
+ assert(rtype(i->to) == RTmp);
+ uint t = i->to.val;
+ if (bshas(v, t)) {
+ bsclr(v, t);
+ } else {
+ // Dead def.
+ assert(t >= Tmp0 && "dead reg");
+ bsset(v, t);
+ bsset(w, t);
+ }
+}
+
+// Process args.
+void process_args(Ins* i, BSet* v, BSet* w, int lvarg[2], Fn* fn) {
+ int memquota = T.memargs(static_cast<int>(i->op));
+ for (int n = 0; n < 2; ++n)
+ if (rtype(i->arg[n]) == RMem)
+ --memquota;
+
+ for (int n = 0; n < 2; ++n) {
+ switch (rtype(i->arg[n])) {
+ case RMem: {
+ uint t = i->arg[n].val;
+ Mem* m = &fn->mem[t];
+ if (rtype(m->base) == RTmp) {
+ bsset(v, m->base.val);
+ bsset(w, m->base.val);
+ }
+ if (rtype(m->index) == RTmp) {
+ bsset(v, m->index.val);
+ bsset(w, m->index.val);
+ }
+ break;
+ }
+ case RTmp: {
+ uint t = i->arg[n].val;
+ lvarg[n] = bshas(v, t);
+ bsset(v, t);
+ if (memquota-- <= 0)
+ bsset(w, t);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+}
+
+// Rewrite evicted args after limit2.
+void rewrite_evicted_args(SpillCtx& s, Ins* i, BSet* u, BSet* v, const int lvarg[2]) {
+ for (int n = 0; n < 2; ++n) {
+ if (rtype(i->arg[n]) != RTmp)
+ continue;
+ uint t = i->arg[n].val;
+ if (!bshas(v, t)) {
+ if (!lvarg[n])
+ bsclr(u, t);
+ i->arg[n] = alloc_spill_slot(s, static_cast<int>(t));
+ }
+ }
+}
+
+// Process jmp args.
+void process_jmp_arg(SpillCtx& s, Blk* b, BSet* v) {
+ bool has1 = rtype(b->jmp.arg) == RTmp;
+ bool has2 = rtype(b->jmp.arg2) == RTmp;
+ if (!has1 && !has2)
+ return;
+ int t1 = has1 ? static_cast<int>(b->jmp.arg.val) : -1;
+ int t2 = has2 ? static_cast<int>(b->jmp.arg2.val) : -1;
+ if (has1) {
+ assert(KBASE(s.tmp[t1].cls) == 0);
+ bsset(v, static_cast<uint>(t1));
+ }
+ if (has2) {
+ assert(KBASE(s.tmp[t2].cls) == 0);
+ bsset(v, static_cast<uint>(t2));
+ }
+ limit_two(s, v, 0, 0, nullptr);
+ if (has1 && !bshas(v, static_cast<uint>(t1)))
+ b->jmp.arg = alloc_spill_slot(s, t1);
+ if (has2 && !bshas(v, static_cast<uint>(t2)))
+ b->jmp.arg2 = alloc_spill_slot(s, t2);
+}
+
+// Slot unselected outs.
+void slot_unselected_outs(SpillCtx& s, Blk* b, BSet* v) {
+ int t = Tmp0;
+ while (bsiter(b->out, &t)) {
+ if (!bshas(v, static_cast<uint>(t)))
+ alloc_spill_slot(s, t);
+ ++t;
+ }
+}
+
+// Process block phis.
+void process_block_phis(Ctx* ctx, SpillCtx& s, Blk* b, BSet* v) {
+ for (Phi* p = b->phi; p; p = p->link) {
+ assert(rtype(p->to) == RTmp);
+ uint t = p->to.val;
+ if (bshas(v, t)) {
+ bsclr(v, t);
+ store_to_slot(ctx, s, p->to, s.tmp[t].slot);
+ } else if (bshas(b->in, t)) {
+ p->to = alloc_spill_slot(s, static_cast<int>(t));
+ }
+ }
+}
+
+void dump_spill_result(Ctx* ctx, Fn* fn) {
+ FILE* f = ctx->dbg_buf;
+ fprintf(f, "\n> Block information:\n");
+ for (Blk* b = fn->start; b; b = b->link) {
+ fprintf(f, "\t%-10s (% 5d) ", b->name, b->loop);
+ dumpts(b->out, fn->tmp, f);
+ }
+ fprintf(f, "\n> After spilling:\n");
+ printfn(fn, f);
+}
+
+} // namespace
+
+// spill main entry.
+
+void spill(Ctx* ctx, Fn* fn) {
+ SpillCtx s{};
+ s.ctx = ctx;
+ s.tmp = fn->tmp;
+ s.ntmp = fn->ntmp;
+ s.locs = fn->slot;
+ s.slot4 = 0;
+ s.slot8 = 0;
+
+ uint n = static_cast<uint>(s.ntmp);
+ bsinit(ctx, &s.mask[0], n);
+ bsinit(ctx, &s.mask[1], n);
+ bsinit(ctx, &s.b2_scratch, n);
+
+ BSet u[1], v[1], w[1];
+ bsinit(ctx, u, n);
+ bsinit(ctx, v, n);
+ bsinit(ctx, w, n);
+
+ // Mask initialization.
+ for (int t = 0; t < s.ntmp; ++t) {
+ int k = 0;
+ if (t >= T.fpr0 && t < T.fpr0 + T.nfpr)
+ k = 1;
+ if (t >= Tmp0)
+ k = KBASE(s.tmp[t].cls);
+ bsset(&s.mask[k], static_cast<uint>(t));
+ }
+
+ // RPO reverse order iteration.
+ for (Blk** bp = &fn->rpo[fn->nblk]; bp != fn->rpo;) {
+ Blk* b = *--bp;
+
+ // Determine exit set v.
+ ctx->curi = nullptr; // emit/reloads disabled here.
+ compute_exit_set(s, b, u, v, w);
+ process_jmp_arg(s, b, v);
+ slot_unselected_outs(s, b, v);
+ bscopy(b->out, v);
+
+ // Reverse process instructions, fill insb.
+ ctx->curi = ctx->insb + NIns;
+ for (Ins* i = &b->ins[b->nins]; i != b->ins;) {
+ --i;
+ if (is_regcopy(i)) {
+ i = spill_par_moves(ctx, s, b, i, v);
+ continue;
+ }
+
+ bszero(w);
+ int lvarg[2] = {0, 0};
+ process_def(i, v, w);
+ process_args(i, v, w, lvarg, fn);
+
+ bscopy(u, v);
+ limit_two(s, v, 0, 0, w);
+ rewrite_evicted_args(s, i, u, v, lvarg);
+ reloads(ctx, s, u, v);
+
+ if (!req(i->to, Ref::none())) {
+ uint t = i->to.val;
+ store_to_slot(ctx, s, i->to, s.tmp[t].slot);
+ if (t >= Tmp0)
+ bsclr(v, t);
+ }
+ emiti(ctx, *i);
+
+ bits r = v->t[0]; // Low 64 bits are physical registers.
+ if (r)
+ sethint(s, v, r);
+ }
+
+ // Entry handling.
+ if (b == fn->start) {
+ assert(v->t[0] == (T.rglob | fn->reg));
+ } else {
+ assert(v->t[0] == T.rglob);
+ }
+
+ // Block entry phis.
+ process_block_phis(ctx, s, b, v);
+ bscopy(b->in, v);
+
+ idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi));
+ }
+
+ // 16-byte alignment.
+ s.slot8 += s.slot8 & 3;
+ fn->slot += s.slot8;
+
+ if (debug['S'])
+ dump_spill_result(ctx, fn);
+}
+
+} // namespace rcc
diff --git a/src/frontend/ast.cc b/src/frontend/ast.cc
new file mode 100644
index 0000000..c82272a
--- /dev/null
+++ b/src/frontend/ast.cc
@@ -0,0 +1,214 @@
+/* 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 ast.cc
+/// @brief AST debug printing and built-in function registration.
+
+#include "rcc/ast.h"
+
+#include "rcc/symtab.h"
+
+#include <cstdio>
+
+namespace rcc {
+
+// AST debug printing
+
+static void indent(int n) {
+ for (int i = 0; i < n; i++)
+ fprintf(stderr, " ");
+}
+
+void ast_dump(const ASTNode* node, int depth) {
+ if (!node) {
+ indent(depth);
+ fprintf(stderr, "(null)\n");
+ return;
+ }
+
+ indent(depth);
+ switch (node->kind) {
+ case NodeKind::CompUnit:
+ fprintf(stderr, "CompUnit\n");
+ for (auto& c : static_cast<const CompUnitNode*>(node)->decls_and_funcs)
+ ast_dump(c.get(), depth + 1);
+ break;
+
+ case NodeKind::FuncDef: {
+ auto* f = static_cast<const FuncDefNode*>(node);
+ fprintf(stderr, "FuncDef '%s' -> %s\n", f->name.c_str(), btype_str(f->ret_type));
+ for (auto& p : f->params)
+ ast_dump(p.get(), depth + 1);
+ ast_dump(f->body.get(), depth + 1);
+ break;
+ }
+
+ case NodeKind::FuncFParam: {
+ auto* p = static_cast<const FuncFParamNode*>(node);
+ fprintf(stderr, "Param '%s' : %s%s\n", p->name.c_str(), btype_str(p->btype),
+ p->is_array ? "[]" : "");
+ break;
+ }
+
+ case NodeKind::VarDecl: {
+ auto* d = static_cast<const VarDeclNode*>(node);
+ fprintf(stderr, "%s %s\n", d->is_const ? "ConstDecl" : "VarDecl", btype_str(d->btype));
+ for (auto& def : d->defs)
+ ast_dump(def.get(), depth + 1);
+ break;
+ }
+
+ case NodeKind::VarDef: {
+ auto* d = static_cast<const VarDefNode*>(node);
+ fprintf(stderr, "VarDef '%s'", d->name.c_str());
+ if (!d->dims.empty())
+ fprintf(stderr, " (array, %zu dims)", d->dims.size());
+ fprintf(stderr, "\n");
+ if (d->init_val)
+ ast_dump(d->init_val.get(), depth + 1);
+ break;
+ }
+
+ case NodeKind::InitList:
+ fprintf(stderr, "InitList\n");
+ for (auto& i : static_cast<const InitListNode*>(node)->items)
+ ast_dump(i.get(), depth + 1);
+ break;
+
+ case NodeKind::Block:
+ fprintf(stderr, "Block\n");
+ for (auto& i : static_cast<const BlockNode*>(node)->items)
+ ast_dump(i.get(), depth + 1);
+ break;
+
+ case NodeKind::AssignStmt:
+ fprintf(stderr, "Assign\n");
+ ast_dump(static_cast<const AssignStmtNode*>(node)->lval.get(), depth + 1);
+ ast_dump(static_cast<const AssignStmtNode*>(node)->expr.get(), depth + 1);
+ break;
+
+ case NodeKind::ExprStmt:
+ fprintf(stderr, "ExprStmt\n");
+ if (static_cast<const ExprStmtNode*>(node)->expr)
+ ast_dump(static_cast<const ExprStmtNode*>(node)->expr.get(), depth + 1);
+ break;
+
+ case NodeKind::IfStmt: {
+ auto* s = static_cast<const IfStmtNode*>(node);
+ fprintf(stderr, "If\n");
+ ast_dump(s->cond.get(), depth + 1);
+ ast_dump(s->then_stmt.get(), depth + 1);
+ if (s->else_stmt)
+ ast_dump(s->else_stmt.get(), depth + 1);
+ break;
+ }
+
+ case NodeKind::WhileStmt:
+ fprintf(stderr, "While\n");
+ ast_dump(static_cast<const WhileStmtNode*>(node)->cond.get(), depth + 1);
+ ast_dump(static_cast<const WhileStmtNode*>(node)->body.get(), depth + 1);
+ break;
+
+ case NodeKind::BreakStmt:
+ fprintf(stderr, "Break\n");
+ break;
+ case NodeKind::ContinueStmt:
+ fprintf(stderr, "Continue\n");
+ break;
+
+ case NodeKind::ReturnStmt:
+ fprintf(stderr, "Return\n");
+ if (static_cast<const ReturnStmtNode*>(node)->expr)
+ ast_dump(static_cast<const ReturnStmtNode*>(node)->expr.get(), depth + 1);
+ break;
+
+ case NodeKind::IntLiteral:
+ fprintf(stderr, "IntLit %d\n", static_cast<const IntLiteralNode*>(node)->value);
+ break;
+
+ case NodeKind::FloatLiteral:
+ fprintf(stderr, "FloatLit %f\n",
+ static_cast<double>(static_cast<const FloatLiteralNode*>(node)->value));
+ break;
+
+ case NodeKind::LVal: {
+ auto* l = static_cast<const LValNode*>(node);
+ fprintf(stderr, "LVal '%s'\n", l->name.c_str());
+ for (auto& idx : l->indices)
+ ast_dump(idx.get(), depth + 1);
+ break;
+ }
+
+ case NodeKind::FuncCall: {
+ auto* c = static_cast<const FuncCallNode*>(node);
+ fprintf(stderr, "Call '%s'\n", c->name.c_str());
+ for (auto& a : c->args)
+ ast_dump(a.get(), depth + 1);
+ break;
+ }
+
+ case NodeKind::BinaryExpr: {
+ auto* e = static_cast<const BinaryExprNode*>(node);
+ const char* ops[] = {"Add", "Sub", "Mul", "Div", "Mod", "Lt", "Gt",
+ "Le", "Ge", "Eq", "Ne", "And", "Or"};
+ fprintf(stderr, "BinExpr %s\n", ops[static_cast<int>(e->op)]);
+ ast_dump(e->lhs.get(), depth + 1);
+ ast_dump(e->rhs.get(), depth + 1);
+ break;
+ }
+
+ case NodeKind::UnaryExpr: {
+ auto* e = static_cast<const UnaryExprNode*>(node);
+ const char* ops[] = {"Pos", "Neg", "Not"};
+ fprintf(stderr, "UnaryExpr %s\n", ops[static_cast<int>(e->op)]);
+ ast_dump(e->operand.get(), depth + 1);
+ break;
+ }
+
+ case NodeKind::ImplicitCast: {
+ auto* c = static_cast<const ImplicitCastNode*>(node);
+ fprintf(stderr, "ImplicitCast -> %s\n", btype_str(c->target_type));
+ ast_dump(c->operand.get(), depth + 1);
+ break;
+ }
+ }
+}
+
+// built-in functions
+
+void SymbolTable::register_builtins() {
+ // I/O functions
+ register_func({"getint", BType::Int, {}});
+ register_func({"getch", BType::Int, {}});
+ register_func({"getfloat", BType::Float, {}});
+ register_func({"getarray", BType::Int, {{BType::Int, true}}});
+ register_func({"getfarray", BType::Int, {{BType::Float, true}}});
+ register_func({"putint", BType::Void, {{BType::Int, false}}});
+ register_func({"putch", BType::Void, {{BType::Int, false}}});
+ register_func({"putfloat", BType::Void, {{BType::Float, false}}});
+ register_func({"putarray", BType::Void, {{BType::Int, false}, {BType::Int, true}}});
+ register_func({"putfarray", BType::Void, {{BType::Int, false}, {BType::Float, true}}});
+ register_func({"putf", BType::Void, {}}); // variable parameters
+
+ // timing functions (actually call _sysy_starttime / _sysy_stoptime)
+ register_func({"starttime", BType::Void, {}});
+ register_func({"stoptime", BType::Void, {}});
+}
+
+} // namespace rcc
diff --git a/src/frontend/lexer.l b/src/frontend/lexer.l
new file mode 100644
index 0000000..33359ef
--- /dev/null
+++ b/src/frontend/lexer.l
@@ -0,0 +1,95 @@
+/* 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/>. */
+
+%{
+/* lexer.l - SysY2022 Lexer */
+#include "rcc/ast.h"
+#include "parser.tab.h"
+
+#include <cstdlib>
+#include <cstring>
+
+// Line number tracking
+int yylineno_val = 1;
+#define YY_USER_ACTION yylloc.first_line = yylineno_val;
+
+using namespace rcc;
+%}
+
+%option noyywrap nounput noinput
+
+/* Exclusive start condition: multi-line comments */
+%x COMMENT
+
+%%
+
+ /* ─── Whitespace and Newlines ─── */
+[ \t\r]+ { /* skip whitespace */ }
+\n { yylineno_val++; }
+
+ /* ─── Comments ─── */
+"//"[^\n]* { /* single-line comment */ }
+"/*" { BEGIN(COMMENT); }
+<COMMENT>"*/" { BEGIN(INITIAL); }
+<COMMENT>\n { yylineno_val++; }
+<COMMENT>. { /* eat anything inside comment */ }
+
+ /* ─── Keywords ─── */
+"int" { return T_INT; }
+"float" { return T_FLOAT; }
+"void" { return T_VOID; }
+"const" { return T_CONST; }
+"if" { return T_IF; }
+"else" { return T_ELSE; }
+"while" { return T_WHILE; }
+"break" { return T_BREAK; }
+"continue" { return T_CONTINUE; }
+"return" { return T_RETURN; }
+
+ /* ─── Integer Constants (Hex > Octal > Decimal, order is important) ─── */
+0[xX][0-9a-fA-F]+ { yylval.int_val = static_cast<int32_t>(strtoul(yytext, nullptr, 16)); return INT_CONST; }
+0[0-7]+ { yylval.int_val = static_cast<int32_t>(strtoul(yytext, nullptr, 8)); return INT_CONST; }
+[0-9]+ { yylval.int_val = static_cast<int32_t>(strtoul(yytext, nullptr, 10)); return INT_CONST; }
+
+ /* ─── Floating Point Constants (Hex float + Decimal float) ─── */
+0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+([pP][+-]?[0-9]+)? { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; }
+0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*([pP][+-]?[0-9]+)? { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; }
+0[xX][0-9a-fA-F]+[pP][+-]?[0-9]+ { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; }
+[0-9]*\.[0-9]+([eE][+-]?[0-9]+)? { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; }
+[0-9]+\.[0-9]*([eE][+-]?[0-9]+)? { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; }
+[0-9]+[eE][+-]?[0-9]+ { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; }
+
+ /* ─── Identifiers ─── */
+[a-zA-Z_][a-zA-Z0-9_]* { yylval.str_val = strdup(yytext); return IDENT; }
+
+ /* ─── Multi-character Operators ─── */
+"&&" { return T_AND; }
+"||" { return T_OR; }
+"<=" { return T_LE; }
+">=" { return T_GE; }
+"==" { return T_EQ; }
+"!=" { return T_NE; }
+
+ /* ─── Format Strings (for putf only) ─── */
+\"[^\"]*\" { yylval.str_val = strdup(yytext); return STRING_CONST; }
+
+ /* ─── Single-character Operators and Delimiters, return ASCII directly ─── */
+. { return yytext[0]; }
+
+%%
diff --git a/src/frontend/parser.y b/src/frontend/parser.y
new file mode 100644
index 0000000..b2d5506
--- /dev/null
+++ b/src/frontend/parser.y
@@ -0,0 +1,686 @@
+/* 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/>. */
+%{
+/* parser.y - SysY2022 Parser */
+#include "rcc/ast.h"
+
+#include <cstdio>
+#include <cstdlib>
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace rcc;
+
+// Global AST root node
+extern CompUnitNode* g_ast_root;
+
+extern int yylex();
+extern int yylineno_val;
+void yyerror(const char* msg);
+extern FILE* yyin;
+%}
+
+/* ─── Bison Configuration ─── */
+%locations
+%define parse.error verbose
+
+/* ─── Value Types ─── */
+%union {
+ int32_t int_val;
+ float float_val;
+ char* str_val;
+ rcc::ASTNode* node;
+ rcc::ASTList* node_list;
+ rcc::BType btype;
+}
+
+/* ─── Terminals ─── */
+%token <int_val> INT_CONST
+%token <float_val> FLOAT_CONST
+%token <str_val> IDENT STRING_CONST
+
+%token T_INT T_FLOAT T_VOID T_CONST
+%token T_IF T_ELSE T_WHILE T_BREAK T_CONTINUE T_RETURN
+%token T_AND T_OR T_LE T_GE T_EQ T_NE
+
+/* ─── Non-terminal Types ─── */
+%type <node> CompUnit
+%type <node> GlobalItem FuncDef_Void
+%type <node> FuncFParam
+%type <node> Block BlockItem Stmt
+%type <node> Decl ConstDecl VarDecl ConstDef VarDef
+%type <node> InitVal ConstInitVal
+%type <node> Exp Cond LVal PrimaryExp UnaryExp Number
+%type <node> MulExp AddExp RelExp EqExp LAndExp LOrExp ConstExp
+%type <node_list> ConstDefList VarDefList
+%type <node_list> FuncFParams FuncRParams
+%type <node_list> BlockItemList
+%type <node_list> ArrayDims ArrayIndices
+%type <node_list> InitValList ConstInitValList
+%type <btype> BType
+
+/* ─── Resolve dangling else ─── */
+%nonassoc LOWER_THAN_ELSE
+%nonassoc T_ELSE
+
+/* ─── Start Symbol ─── */
+%start CompUnit
+
+%%
+
+/* Compilation Unit */
+
+CompUnit
+ : CompUnit GlobalItem {
+ auto* unit = static_cast<CompUnitNode*>($1);
+ unit->decls_and_funcs.push_back(ASTPtr($2));
+ $$ = unit;
+ }
+ | CompUnit ConstDecl {
+ auto* unit = static_cast<CompUnitNode*>($1);
+ unit->decls_and_funcs.push_back(ASTPtr($2));
+ $$ = unit;
+ }
+ | CompUnit FuncDef_Void {
+ auto* unit = static_cast<CompUnitNode*>($1);
+ unit->decls_and_funcs.push_back(ASTPtr($2));
+ $$ = unit;
+ }
+ | GlobalItem {
+ auto* unit = new CompUnitNode();
+ unit->decls_and_funcs.push_back(ASTPtr($1));
+ $$ = unit;
+ g_ast_root = unit;
+ }
+ | ConstDecl {
+ auto* unit = new CompUnitNode();
+ unit->decls_and_funcs.push_back(ASTPtr($1));
+ $$ = unit;
+ g_ast_root = unit;
+ }
+ | FuncDef_Void {
+ auto* unit = new CompUnitNode();
+ unit->decls_and_funcs.push_back(ASTPtr($1));
+ $$ = unit;
+ g_ast_root = unit;
+ }
+ ;
+
+/* GlobalItem */
+GlobalItem
+ : BType IDENT '(' ')' Block {
+ /* FuncDef: int/float foo() { ... } */
+ auto* func = new FuncDefNode();
+ func->ret_type = $1;
+ func->name = $2; free($2);
+ func->body = ASTPtr($5);
+ func->lineno = @1.first_line;
+ $$ = func;
+ }
+ | BType IDENT '(' FuncFParams ')' Block {
+ /* FuncDef: int/float foo(params) { ... } */
+ auto* func = new FuncDefNode();
+ func->ret_type = $1;
+ func->name = $2; free($2);
+ func->params = std::move(*$4); delete $4;
+ func->body = ASTPtr($6);
+ func->lineno = @1.first_line;
+ $$ = func;
+ }
+ | BType IDENT ArrayDims ';' {
+ /* VarDecl: int x; / int x[10]; */
+ auto* decl = new VarDeclNode();
+ decl->is_const = false;
+ decl->btype = $1;
+ auto* def = new VarDefNode();
+ def->name = $2; free($2);
+ def->dims = std::move(*$3); delete $3;
+ def->lineno = @2.first_line;
+ decl->defs.push_back(ASTPtr(def));
+ $$ = decl;
+ }
+ | BType IDENT ArrayDims '=' InitVal ';' {
+ /* VarDecl: int x = 5; / int x[10] = {1,2}; */
+ auto* decl = new VarDeclNode();
+ decl->is_const = false;
+ decl->btype = $1;
+ auto* def = new VarDefNode();
+ def->name = $2; free($2);
+ def->dims = std::move(*$3); delete $3;
+ def->init_val = ASTPtr($5);
+ def->lineno = @2.first_line;
+ decl->defs.push_back(ASTPtr(def));
+ $$ = decl;
+ }
+ | BType IDENT ArrayDims ',' VarDefList ';' {
+ /* VarDecl: int x, y, z; */
+ auto* decl = new VarDeclNode();
+ decl->is_const = false;
+ decl->btype = $1;
+ auto* def = new VarDefNode();
+ def->name = $2; free($2);
+ def->dims = std::move(*$3); delete $3;
+ def->lineno = @2.first_line;
+ decl->defs.push_back(ASTPtr(def));
+ for (auto& d : *$5) decl->defs.push_back(std::move(d));
+ delete $5;
+ $$ = decl;
+ }
+ | BType IDENT ArrayDims '=' InitVal ',' VarDefList ';' {
+ /* VarDecl: int x = 1, y, z; */
+ auto* decl = new VarDeclNode();
+ decl->is_const = false;
+ decl->btype = $1;
+ auto* def = new VarDefNode();
+ def->name = $2; free($2);
+ def->dims = std::move(*$3); delete $3;
+ def->init_val = ASTPtr($5);
+ def->lineno = @2.first_line;
+ decl->defs.push_back(ASTPtr(def));
+ for (auto& d : *$7) decl->defs.push_back(std::move(d));
+ delete $7;
+ $$ = decl;
+ }
+ ;
+
+/* void functions */
+FuncDef_Void
+ : T_VOID IDENT '(' ')' Block {
+ auto* func = new FuncDefNode();
+ func->ret_type = BType::Void;
+ func->name = $2; free($2);
+ func->body = ASTPtr($5);
+ func->lineno = @1.first_line;
+ $$ = func;
+ }
+ | T_VOID IDENT '(' FuncFParams ')' Block {
+ auto* func = new FuncDefNode();
+ func->ret_type = BType::Void;
+ func->name = $2; free($2);
+ func->params = std::move(*$4); delete $4;
+ func->body = ASTPtr($6);
+ func->lineno = @1.first_line;
+ $$ = func;
+ }
+ ;
+
+/* Types */
+
+BType : T_INT { $$ = BType::Int; }
+ | T_FLOAT { $$ = BType::Float; }
+ ;
+
+/* Declarations */
+
+/* Decl */
+Decl : ConstDecl { $$ = $1; }
+ | VarDecl { $$ = $1; }
+ ;
+
+ConstDecl
+ : T_CONST BType ConstDefList ';' {
+ auto* decl = new VarDeclNode();
+ decl->is_const = true;
+ decl->btype = $2;
+ decl->defs = std::move(*$3);
+ delete $3;
+ $$ = decl;
+ }
+ ;
+
+ConstDefList
+ : ConstDef {
+ $$ = new ASTList();
+ $$->push_back(ASTPtr($1));
+ }
+ | ConstDefList ',' ConstDef {
+ $1->push_back(ASTPtr($3));
+ $$ = $1;
+ }
+ ;
+
+ConstDef
+ : IDENT ArrayDims '=' ConstInitVal {
+ auto* def = new VarDefNode();
+ def->name = $1; free($1);
+ def->dims = std::move(*$2); delete $2;
+ def->init_val = ASTPtr($4);
+ def->lineno = @1.first_line;
+ $$ = def;
+ }
+ ;
+
+ConstInitVal
+ : ConstExp { $$ = $1; }
+ | '{' '}' {
+ $$ = new InitListNode();
+ }
+ | '{' ConstInitValList '}' {
+ auto* list = new InitListNode();
+ list->items = std::move(*$2); delete $2;
+ $$ = list;
+ }
+ ;
+
+ConstInitValList
+ : ConstInitVal {
+ $$ = new ASTList();
+ $$->push_back(ASTPtr($1));
+ }
+ | ConstInitValList ',' ConstInitVal {
+ $1->push_back(ASTPtr($3));
+ $$ = $1;
+ }
+ ;
+
+/* Local VarDecl */
+VarDecl
+ : BType VarDefList ';' {
+ auto* decl = new VarDeclNode();
+ decl->is_const = false;
+ decl->btype = $1;
+ decl->defs = std::move(*$2);
+ delete $2;
+ $$ = decl;
+ }
+ ;
+
+VarDefList
+ : VarDef {
+ $$ = new ASTList();
+ $$->push_back(ASTPtr($1));
+ }
+ | VarDefList ',' VarDef {
+ $1->push_back(ASTPtr($3));
+ $$ = $1;
+ }
+ ;
+
+VarDef
+ : IDENT ArrayDims {
+ auto* def = new VarDefNode();
+ def->name = $1; free($1);
+ def->dims = std::move(*$2); delete $2;
+ def->lineno = @1.first_line;
+ $$ = def;
+ }
+ | IDENT ArrayDims '=' InitVal {
+ auto* def = new VarDefNode();
+ def->name = $1; free($1);
+ def->dims = std::move(*$2); delete $2;
+ def->init_val = ASTPtr($4);
+ def->lineno = @1.first_line;
+ $$ = def;
+ }
+ ;
+
+InitVal
+ : Exp { $$ = $1; }
+ | '{' '}' {
+ $$ = new InitListNode();
+ }
+ | '{' InitValList '}' {
+ auto* list = new InitListNode();
+ list->items = std::move(*$2); delete $2;
+ $$ = list;
+ }
+ ;
+
+InitValList
+ : InitVal {
+ $$ = new ASTList();
+ $$->push_back(ASTPtr($1));
+ }
+ | InitValList ',' InitVal {
+ $1->push_back(ASTPtr($3));
+ $$ = $1;
+ }
+ ;
+
+/* Array Dimensions */
+ArrayDims
+ : /* empty */ { $$ = new ASTList(); }
+ | ArrayDims '[' ConstExp ']' {
+ $1->push_back(ASTPtr($3));
+ $$ = $1;
+ }
+ ;
+
+/* Function Parameters */
+
+FuncFParams
+ : FuncFParam {
+ $$ = new ASTList();
+ $$->push_back(ASTPtr($1));
+ }
+ | FuncFParams ',' FuncFParam {
+ $1->push_back(ASTPtr($3));
+ $$ = $1;
+ }
+ ;
+
+FuncFParam
+ : BType IDENT {
+ auto* p = new FuncFParamNode();
+ p->btype = $1;
+ p->name = $2; free($2);
+ p->lineno = @1.first_line;
+ $$ = p;
+ }
+ | BType IDENT '[' ']' ArrayDims {
+ /* Array parameter */
+ auto* p = new FuncFParamNode();
+ p->btype = $1;
+ p->name = $2; free($2);
+ p->is_array = true;
+ p->dims = std::move(*$5); delete $5;
+ p->lineno = @1.first_line;
+ $$ = p;
+ }
+ ;
+
+/* Blocks and Statements */
+
+Block
+ : '{' BlockItemList '}' {
+ auto* block = new BlockNode();
+ block->items = std::move(*$2); delete $2;
+ $$ = block;
+ }
+ ;
+
+BlockItemList
+ : /* empty */ { $$ = new ASTList(); }
+ | BlockItemList BlockItem {
+ $1->push_back(ASTPtr($2));
+ $$ = $1;
+ }
+ ;
+
+BlockItem
+ : Decl { $$ = $1; }
+ | Stmt { $$ = $1; }
+ ;
+
+Stmt
+ : LVal '=' Exp ';' {
+ auto* s = new AssignStmtNode();
+ s->lval = ASTPtr($1);
+ s->expr = ASTPtr($3);
+ s->lineno = @1.first_line;
+ $$ = s;
+ }
+ | Exp ';' {
+ auto* s = new ExprStmtNode();
+ s->expr = ASTPtr($1);
+ $$ = s;
+ }
+ | ';' {
+ $$ = new ExprStmtNode();
+ }
+ | Block { $$ = $1; }
+ | T_IF '(' Cond ')' Stmt %prec LOWER_THAN_ELSE {
+ auto* s = new IfStmtNode();
+ s->cond = ASTPtr($3);
+ s->then_stmt = ASTPtr($5);
+ s->lineno = @1.first_line;
+ $$ = s;
+ }
+ | T_IF '(' Cond ')' Stmt T_ELSE Stmt {
+ auto* s = new IfStmtNode();
+ s->cond = ASTPtr($3);
+ s->then_stmt = ASTPtr($5);
+ s->else_stmt = ASTPtr($7);
+ s->lineno = @1.first_line;
+ $$ = s;
+ }
+ | T_WHILE '(' Cond ')' Stmt {
+ auto* s = new WhileStmtNode();
+ s->cond = ASTPtr($3);
+ s->body = ASTPtr($5);
+ s->lineno = @1.first_line;
+ $$ = s;
+ }
+ | T_BREAK ';' {
+ $$ = new BreakStmtNode();
+ $$->lineno = @1.first_line;
+ }
+ | T_CONTINUE ';' {
+ $$ = new ContinueStmtNode();
+ $$->lineno = @1.first_line;
+ }
+ | T_RETURN ';' {
+ $$ = new ReturnStmtNode();
+ $$->lineno = @1.first_line;
+ }
+ | T_RETURN Exp ';' {
+ auto* s = new ReturnStmtNode();
+ s->expr = ASTPtr($2);
+ s->lineno = @1.first_line;
+ $$ = s;
+ }
+ ;
+
+/* Expressions */
+
+/* Exp relaxed to LOrExp */
+Exp : LOrExp { $$ = $1; } ;
+
+Cond : LOrExp { $$ = $1; } ;
+
+ConstExp : AddExp { $$ = $1; } ;
+
+LVal
+ : IDENT ArrayIndices {
+ auto* lv = new LValNode();
+ lv->name = $1; free($1);
+ lv->indices = std::move(*$2); delete $2;
+ lv->lineno = @1.first_line;
+ $$ = lv;
+ }
+ ;
+
+ArrayIndices
+ : /* empty */ { $$ = new ASTList(); }
+ | ArrayIndices '[' Exp ']' {
+ $1->push_back(ASTPtr($3));
+ $$ = $1;
+ }
+ ;
+
+PrimaryExp
+ : '(' Exp ')' { $$ = $2; }
+ | LVal { $$ = $1; }
+ | Number { $$ = $1; }
+ ;
+
+Number
+ : INT_CONST {
+ auto* n = new IntLiteralNode();
+ n->value = $1;
+ n->lineno = @1.first_line;
+ $$ = n;
+ }
+ | FLOAT_CONST {
+ auto* n = new FloatLiteralNode();
+ n->value = $1;
+ n->lineno = @1.first_line;
+ $$ = n;
+ }
+ ;
+
+UnaryExp
+ : PrimaryExp { $$ = $1; }
+ | IDENT '(' ')' {
+ auto* c = new FuncCallNode();
+ c->name = $1; free($1);
+ c->lineno = @1.first_line;
+ $$ = c;
+ }
+ | IDENT '(' FuncRParams ')' {
+ auto* c = new FuncCallNode();
+ c->name = $1; free($1);
+ c->args = std::move(*$3); delete $3;
+ c->lineno = @1.first_line;
+ $$ = c;
+ }
+ | '+' UnaryExp {
+ auto* u = new UnaryExprNode();
+ u->op = UnaryOp::Pos;
+ u->operand = ASTPtr($2);
+ u->lineno = @1.first_line;
+ $$ = u;
+ }
+ | '-' UnaryExp {
+ auto* u = new UnaryExprNode();
+ u->op = UnaryOp::Neg;
+ u->operand = ASTPtr($2);
+ u->lineno = @1.first_line;
+ $$ = u;
+ }
+ | '!' UnaryExp {
+ auto* u = new UnaryExprNode();
+ u->op = UnaryOp::Not;
+ u->operand = ASTPtr($2);
+ u->lineno = @1.first_line;
+ $$ = u;
+ }
+ ;
+
+FuncRParams
+ : Exp {
+ $$ = new ASTList();
+ $$->push_back(ASTPtr($1));
+ }
+ | FuncRParams ',' Exp {
+ $1->push_back(ASTPtr($3));
+ $$ = $1;
+ }
+ /* putf format string parameter */
+ | STRING_CONST {
+ $$ = new ASTList();
+ auto* n = new IntLiteralNode(); /* placeholder */
+ n->value = 0;
+ $$->push_back(ASTPtr(static_cast<ASTNode*>(n)));
+ free($1);
+ }
+ | FuncRParams ',' STRING_CONST {
+ free($3);
+ $$ = $1;
+ }
+ ;
+
+MulExp
+ : UnaryExp { $$ = $1; }
+ | MulExp '*' UnaryExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Mul;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ | MulExp '/' UnaryExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Div;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ | MulExp '%' UnaryExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Mod;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ ;
+
+AddExp
+ : MulExp { $$ = $1; }
+ | AddExp '+' MulExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Add;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ | AddExp '-' MulExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Sub;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ ;
+
+RelExp
+ : AddExp { $$ = $1; }
+ | RelExp '<' AddExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Lt;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ | RelExp '>' AddExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Gt;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ | RelExp T_LE AddExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Le;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ | RelExp T_GE AddExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Ge;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ ;
+
+EqExp
+ : RelExp { $$ = $1; }
+ | EqExp T_EQ RelExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Eq;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ | EqExp T_NE RelExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Ne;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ ;
+
+LAndExp
+ : EqExp { $$ = $1; }
+ | LAndExp T_AND EqExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::And;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ ;
+
+LOrExp
+ : LAndExp { $$ = $1; }
+ | LOrExp T_OR LAndExp {
+ auto* e = new BinaryExprNode(); e->op = BinOp::Or;
+ e->lhs = ASTPtr($1); e->rhs = ASTPtr($3);
+ e->lineno = @2.first_line; $$ = e;
+ }
+ ;
+
+%%
+
+/* Global Variables and Error Handling */
+CompUnitNode* g_ast_root = nullptr;
+
+void yyerror(const char* msg) {
+ fprintf(stderr, "rcc: parse error (line %d): %s\n", yylineno_val, msg);
+}
diff --git a/src/ir/irgen.cc b/src/ir/irgen.cc
new file mode 100644
index 0000000..1332109
--- /dev/null
+++ b/src/ir/irgen.cc
@@ -0,0 +1,728 @@
+/* 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 irgen.cc
+/// @brief AST -> SSA IR: Main framework, Braun SSA, statement translation
+
+#include "rcc/irgen.h"
+
+#include <cstddef>
+
+namespace rcc {
+
+// BType -> K class
+int IRGen::btype_to_cls(BType t) {
+ return (t == BType::Float) ? Ks : Kw;
+}
+
+// Constant / Ref Construction
+Fn* IRGen::new_fn(const char* name) {
+ auto* fn = static_cast<Fn*>(alloc(ctx_, sizeof(Fn)));
+ memset(fn, 0, sizeof(Fn));
+ fn->name = strf(ctx_, PFn, "%s", name);
+
+ // Initialize tmp array
+ fn->ntmp = 0;
+ fn->tmp = static_cast<Tmp*>(vnew(ctx_, 0, sizeof(Tmp), PFn));
+ for (int i = 0; i < Tmp0; i++) {
+ int cls = (T.fpr0 <= i && i < T.fpr0 + T.nfpr) ? Kd : Kl;
+ newtmp(ctx_, nullptr, cls, fn);
+ }
+
+ // Initialize con array: con[0] = CUndef (corresponds to R/UNDEF)
+ fn->ncon = 2;
+ fn->con = static_cast<Con*>(vnew(ctx_, static_cast<ulong>(fn->ncon), sizeof(Con), PFn));
+ memset(fn->con, 0, 2 * sizeof(Con));
+ fn->con[0].type = Con::CUndef;
+ // con[1] = CBits{0} (corresponds to CON_Z)
+ fn->con[1].type = Con::CBits;
+ fn->con[1].bits.i = 0;
+
+ fn->retty = -1;
+ fn->slot = 0;
+ fn->salign = 4;
+ // All user functions visible to linker
+ fn->lnk.export_ = 1;
+ return fn;
+}
+
+Blk* IRGen::new_blk(const char* name) {
+ auto* b = static_cast<Blk*>(alloc(ctx_, sizeof(Blk)));
+ memset(b, 0, sizeof(Blk));
+ b->id = cur_fn_->nblk++;
+ b->name = strf(ctx_, PFn, "%s.%u", name, b->id);
+ b->ins = static_cast<Ins*>(vnew(ctx_, 4, sizeof(Ins), PFn));
+ b->nins = 0;
+ b->visit = 0;
+
+ // Append to function block list tail
+ if (!cur_fn_->start) {
+ cur_fn_->start = b;
+ } else {
+ Blk* tail = cur_fn_->start;
+ while (tail->link)
+ tail = tail->link;
+ tail->link = b;
+ }
+
+ blk_vec_.push_back(b);
+ return b;
+}
+
+Blk* IRGen::get_blk(uint id) {
+ return blk_vec_[id];
+}
+
+bool IRGen::is_sealed(Blk* b) const {
+ auto it = sealed_.find(b->id);
+ return it != sealed_.end() && it->second;
+}
+
+// Constant / Ref Construction
+Ref IRGen::make_int_ref(int32_t val) {
+ return getcon(ctx_, static_cast<int64_t>(val), cur_fn_);
+}
+
+Ref IRGen::make_float_ref(float val) {
+ Con c;
+ memset(&c, 0, sizeof(Con));
+ c.type = Con::CBits;
+ c.flt = 1;
+ c.bits.s = val;
+ return newcon(ctx_, &c, cur_fn_);
+}
+
+Ref IRGen::make_global_ref(const char* name) {
+ Con c;
+ memset(&c, 0, sizeof(Con));
+ c.type = Con::CAddr;
+ c.sym.type = Sym::SGlo;
+ c.sym.id = intern(name);
+ return newcon(ctx_, &c, cur_fn_);
+}
+
+Ref IRGen::make_global_addr_ref(const char* name) {
+ Ref sym = make_global_ref(name);
+ Ref dst = new_tmp_ref(Kl);
+ Ins i{};
+ i.op = Oaddr;
+ i.cls = Kl;
+ i.to = dst;
+ i.arg[0] = sym;
+ i.arg[1] = Ref::none();
+ push_ins(i);
+ return dst;
+}
+
+Ref IRGen::new_tmp_ref(int cls) {
+ return newtmp(ctx_, "t", cls, cur_fn_);
+}
+
+// Entry
+IROutput IRGen::generate(CompUnitNode* root) {
+ output_ = {};
+ sym_.register_builtins();
+ sym_.enter_scope();
+ visit_comp_unit(root);
+ sym_.leave_scope();
+ return std::move(output_);
+}
+
+void IRGen::visit_comp_unit(CompUnitNode* node) {
+ for (auto& child : node->decls_and_funcs) {
+ if (child->kind == NodeKind::FuncDef)
+ visit_func_def(static_cast<FuncDefNode*>(child.get()));
+ else if (child->kind == NodeKind::VarDecl)
+ visit_global_decl(static_cast<VarDeclNode*>(child.get()));
+ }
+}
+
+// Function Definition
+void IRGen::visit_func_def(FuncDefNode* node) {
+ cur_fn_ = new_fn(node->name.c_str());
+ cur_fn_->leaf = 1;
+ blk_vec_.clear();
+ current_def_.clear();
+ incomplete_phis_.clear();
+ sealed_.clear();
+
+ bool is_void = (node->ret_type == BType::Void);
+
+ cur_fn_ret_cls_ = is_void ? Kx : btype_to_cls(node->ret_type);
+
+ // Register function signature
+ FuncSig sig;
+ sig.name = node->name;
+ sig.ret_type = node->ret_type;
+
+ // Entry block
+ Blk* entry = new_blk("entry");
+ cur_blk_ = entry;
+ sealed_[entry->id] = true;
+
+ sym_.enter_scope();
+
+ // Process formal parameters
+ for (auto& p : node->params) {
+ auto* fp = static_cast<FuncFParamNode*>(p.get());
+ int cls = fp->is_array ? Kl : btype_to_cls(fp->btype);
+ sig.params.push_back({fp->btype, fp->is_array});
+
+ Symbol sym;
+ sym.name = fp->name;
+ sym.type = fp->btype;
+ sym.is_param = true;
+ sym.is_array = fp->is_array;
+ sym.uid = sym_.allocate_uid();
+ if (fp->is_array) {
+ for (auto& d : fp->dims)
+ sym.dims.push_back(eval_const_int(d.get()));
+ }
+
+ // Parameter as SSA value: emit Opar instruction (one %t =cls par per parameter)
+ Ref param_ref = new_tmp_ref(cls);
+ Ins par_i;
+ par_i.op = Opar;
+ par_i.cls = static_cast<uint>(cls);
+ par_i.to = param_ref;
+ par_i.arg[0] = Ref::none();
+ par_i.arg[1] = Ref::none();
+ push_ins(par_i);
+
+ write_variable(sym.uid, cur_blk_, param_ref);
+ sym_.insert(fp->name, std::move(sym));
+ }
+
+ sym_.register_func(std::move(sig));
+
+ // Traverse function body
+ visit_block(static_cast<BlockNode*>(node->body.get()));
+
+ // Ensure last block is terminated
+ if (!cur_blk_terminated()) {
+ if (is_void) {
+ emit_ret_void();
+ } else {
+ Ref zero = (cur_fn_ret_cls_ == Ks) ? make_float_ref(0.0f) : make_int_ref(0);
+ emit_ret(zero, cur_fn_ret_cls_);
+ }
+ }
+
+ sym_.leave_scope();
+ output_.functions.push_back(cur_fn_);
+ cur_fn_ = nullptr;
+}
+
+// Global Declaration
+void IRGen::visit_global_decl(VarDeclNode* node) {
+ for (auto& d : node->defs) {
+ auto* def = static_cast<VarDefNode*>(d.get());
+ Symbol sym;
+ sym.name = def->name;
+ sym.type = node->btype;
+ sym.is_const = node->is_const;
+ sym.is_global = true;
+
+ for (auto& dim_expr : def->dims)
+ sym.dims.push_back(eval_const_int(dim_expr.get()));
+ sym.is_array = !sym.dims.empty();
+
+ GlobalData gd;
+ gd.name = def->name;
+ gd.elem_cls = btype_to_cls(node->btype);
+ gd.is_const = node->is_const;
+ gd.total_elems = 1;
+ for (int d2 : sym.dims)
+ gd.total_elems *= static_cast<size_t>(d2);
+ if (!sym.is_array)
+ gd.total_elems = 1;
+
+ if (def->init_val) {
+ if (sym.is_array) {
+ std::vector<ASTNode*> flat;
+ flatten_init(sym.dims, def->init_val.get(), flat);
+ for (size_t i = 0; i < gd.total_elems; i++) {
+ if (i < flat.size() && flat[i]) {
+ if (node->btype == BType::Int)
+ gd.init_vals.push_back(eval_const_int(flat[i]));
+ else
+ gd.init_vals.push_back(eval_const_float(flat[i]));
+ } else {
+ if (node->btype == BType::Int)
+ gd.init_vals.push_back(static_cast<int32_t>(0));
+ else
+ gd.init_vals.push_back(0.0f);
+ }
+ }
+ } else {
+ if (node->btype == BType::Int)
+ gd.init_vals.push_back(eval_const_int(def->init_val.get()));
+ else
+ gd.init_vals.push_back(eval_const_float(def->init_val.get()));
+ }
+ } else {
+ gd.is_zeroinit = true;
+ }
+
+ if (node->is_const && !sym.is_array && def->init_val) {
+ if (node->btype == BType::Int)
+ sym.const_val = eval_const_int(def->init_val.get());
+ else
+ sym.const_val = eval_const_float(def->init_val.get());
+ }
+
+ sym_.insert(def->name, std::move(sym));
+ output_.globals.push_back(std::move(gd));
+ }
+}
+
+// Local Declaration
+void IRGen::visit_local_decl(VarDeclNode* node) {
+ for (auto& d : node->defs) {
+ auto* def = static_cast<VarDefNode*>(d.get());
+ Symbol sym;
+ sym.name = def->name;
+ sym.type = node->btype;
+ sym.is_const = node->is_const;
+ sym.uid = sym_.allocate_uid();
+
+ for (auto& dim_expr : def->dims)
+ sym.dims.push_back(eval_const_int(dim_expr.get()));
+ sym.is_array = !sym.dims.empty();
+
+ int cls = btype_to_cls(node->btype);
+
+ if (sym.is_array) {
+ int total = 1;
+ for (int d2 : sym.dims)
+ total *= d2;
+ Ref size_ref = make_int_ref(total * 4);
+ // Arrays are always 8B aligned
+ Ref addr = emit_ins(Oalloc8, Kl, size_ref, Ref::none());
+ sym.alloca_temp_id = static_cast<int>(addr.val);
+
+ if (def->init_val) {
+ std::vector<ASTNode*> flat;
+ flatten_init(sym.dims, def->init_val.get(), flat);
+ // Count implicit zero tail
+ constexpr int kBulkZeroThreshold = 65536;
+ size_t nonzero_tail = flat.size();
+ while (nonzero_tail > 0 && flat[nonzero_tail - 1] == nullptr)
+ nonzero_tail--;
+ int zero_count = total - static_cast<int>(nonzero_tail);
+ bool bulk = zero_count >= kBulkZeroThreshold;
+ if (bulk)
+ emit_bulk_zero(addr, total * 4);
+ size_t up_to = bulk ? nonzero_tail : static_cast<size_t>(total);
+ for (size_t i = 0; i < up_to; i++) {
+ bool has_init = i < flat.size() && flat[i] != nullptr;
+ if (bulk && !has_init)
+ continue; // bulk-zero covered
+ Ref off = make_int_ref(static_cast<int>(i * 4));
+ Ref elem_addr = emit_ins(Oadd, Kl, addr, off);
+ Ref val;
+ if (has_init) {
+ val = visit_expr(flat[i]);
+ val = coerce(val, cls);
+ } else {
+ val = (cls == Ks) ? make_float_ref(0.0f) : make_int_ref(0);
+ }
+ emit_store(val, elem_addr, cls);
+ }
+ }
+ sym_.insert(def->name, std::move(sym));
+ } else {
+ if (node->is_const && def->init_val) {
+ if (node->btype == BType::Int)
+ sym.const_val = eval_const_int(def->init_val.get());
+ else
+ sym.const_val = eval_const_float(def->init_val.get());
+ }
+ int uid = sym.uid;
+ sym_.insert(def->name, std::move(sym));
+
+ if (def->init_val) {
+ Ref val = visit_expr(def->init_val.get());
+ val = coerce(val, cls);
+ write_variable(uid, cur_blk_, val);
+ }
+ }
+ }
+}
+
+// Statements
+void IRGen::visit_block(BlockNode* node) {
+ sym_.enter_scope();
+ for (auto& item : node->items) {
+ if (cur_blk_terminated())
+ break;
+ visit_stmt(item.get());
+ }
+ sym_.leave_scope();
+}
+
+void IRGen::visit_stmt(ASTNode* node) {
+ if (cur_blk_terminated())
+ return;
+ switch (node->kind) {
+ case NodeKind::VarDecl:
+ visit_local_decl(static_cast<VarDeclNode*>(node));
+ break;
+ case NodeKind::AssignStmt:
+ visit_assign(static_cast<AssignStmtNode*>(node));
+ break;
+ case NodeKind::ExprStmt: {
+ auto* es = static_cast<ExprStmtNode*>(node);
+ if (es->expr)
+ visit_expr(es->expr.get());
+ break;
+ }
+ case NodeKind::Block:
+ visit_block(static_cast<BlockNode*>(node));
+ break;
+ case NodeKind::IfStmt:
+ visit_if(static_cast<IfStmtNode*>(node));
+ break;
+ case NodeKind::WhileStmt:
+ visit_while(static_cast<WhileStmtNode*>(node));
+ break;
+ case NodeKind::ReturnStmt:
+ visit_return(static_cast<ReturnStmtNode*>(node));
+ break;
+ case NodeKind::BreakStmt:
+ if (!loop_stack_.empty())
+ emit_jmp(loop_stack_.back().exit_block);
+ break;
+ case NodeKind::ContinueStmt:
+ if (!loop_stack_.empty())
+ emit_jmp(loop_stack_.back().cond_block);
+ break;
+ default:
+ break;
+ }
+}
+
+void IRGen::visit_assign(AssignStmtNode* node) {
+ Ref rhs = visit_expr(node->expr.get());
+ auto* lv = static_cast<LValNode*>(node->lval.get());
+ Symbol* sym = sym_.lookup(lv->name);
+ int cls = btype_to_cls(sym->type);
+ // Cross-boundary type coordination
+ rhs = coerce(rhs, cls);
+
+ if (!lv->indices.empty() || sym->is_array || sym->is_global) {
+ Ref addr;
+ if (!lv->indices.empty())
+ addr = compute_array_addr(lv);
+ else
+ addr = get_var_addr_ref(sym);
+ emit_store(rhs, addr, cls);
+ } else {
+ write_variable(sym->uid, cur_blk_, rhs);
+ }
+}
+
+void IRGen::visit_if(IfStmtNode* node) {
+ Blk* then_bb = new_blk("if.then");
+ Blk* else_bb = node->else_stmt ? new_blk("if.else") : nullptr;
+ Blk* merge_bb = new_blk("if.merge");
+ Blk* false_target = else_bb ? else_bb : merge_bb;
+
+ visit_cond(node->cond.get(), then_bb, false_target);
+
+ seal_block(then_bb);
+ cur_blk_ = then_bb;
+ visit_stmt(node->then_stmt.get());
+ if (!cur_blk_terminated())
+ emit_jmp(merge_bb);
+
+ if (else_bb) {
+ seal_block(else_bb);
+ cur_blk_ = else_bb;
+ visit_stmt(node->else_stmt.get());
+ if (!cur_blk_terminated())
+ emit_jmp(merge_bb);
+ }
+
+ seal_block(merge_bb);
+ cur_blk_ = merge_bb;
+}
+
+void IRGen::visit_while(WhileStmtNode* node) {
+ Blk* cond_bb = new_blk("while.cond");
+ Blk* body_bb = new_blk("while.body");
+ Blk* exit_bb = new_blk("while.exit");
+
+ emit_jmp(cond_bb);
+
+ cur_blk_ = cond_bb;
+ visit_cond(node->cond.get(), body_bb, exit_bb);
+
+ seal_block(body_bb);
+ cur_blk_ = body_bb;
+ loop_stack_.push_back({cond_bb, exit_bb});
+ visit_stmt(node->body.get());
+ loop_stack_.pop_back();
+ if (!cur_blk_terminated())
+ emit_jmp(cond_bb);
+
+ seal_block(cond_bb);
+ seal_block(exit_bb);
+ cur_blk_ = exit_bb;
+}
+
+void IRGen::visit_return(ReturnStmtNode* node) {
+ if (node->expr) {
+ Ref val = visit_expr(node->expr.get());
+ // cur_fn_ret_cls_ is set at visit_func_def entry based on AST ret_type; void functions
+ // won't reach here.
+ assert(cur_fn_ret_cls_ != Kx && "non-void return in void function");
+ val = coerce(val, cur_fn_ret_cls_);
+ emit_ret(val, cur_fn_ret_cls_);
+ } else {
+ emit_ret_void();
+ }
+}
+
+// Braun SSA. Variable identity key is Symbol::uid(int), avoids name collision across scopes.
+void IRGen::write_variable(int uid, Blk* blk, Ref val) {
+ current_def_[uid][blk->id] = val;
+}
+
+Ref IRGen::read_variable(int uid, Blk* blk, int cls) {
+ auto it = current_def_[uid].find(blk->id);
+ if (it != current_def_[uid].end())
+ return it->second;
+ return read_variable_recursive(uid, blk, cls);
+}
+
+Ref IRGen::read_variable_recursive(int uid, Blk* blk, int cls) {
+ Ref val;
+ uint npred = blk->npred;
+
+ if (!is_sealed(blk)) {
+ // Block not sealed: insert incomplete Phi
+ auto* phi = static_cast<Phi*>(alloc(ctx_, sizeof(Phi)));
+ memset(phi, 0, sizeof(Phi));
+ phi->cls = static_cast<short>(cls);
+ phi->to = new_tmp_ref(cls);
+ phi->link = blk->phi;
+ blk->phi = phi;
+ val = phi->to;
+ incomplete_phis_.push_back({uid, phi, blk->id, cls});
+ } else if (npred == 1) {
+ val = read_variable(uid, blk->pred[0], cls);
+ } else if (npred == 0) {
+ // Entry block, variable undefined
+ val = (cls == Ks) ? make_float_ref(0.0f) : make_int_ref(0);
+ } else {
+ // Multiple predecessors: insert Phi
+ auto* phi = static_cast<Phi*>(alloc(ctx_, sizeof(Phi)));
+ memset(phi, 0, sizeof(Phi));
+ phi->cls = static_cast<short>(cls);
+ phi->to = new_tmp_ref(cls);
+ phi->link = blk->phi;
+ blk->phi = phi;
+ val = phi->to;
+
+ write_variable(uid, blk, val); // Prevent infinite loop
+
+ // Populate phi arguments
+ phi->narg = npred;
+ phi->arg = static_cast<Ref*>(alloc(ctx_, npred * sizeof(Ref)));
+ phi->blk = static_cast<Blk**>(alloc(ctx_, npred * sizeof(Blk*)));
+ for (uint i = 0; i < npred; i++) {
+ phi->blk[i] = blk->pred[i];
+ phi->arg[i] = read_variable(uid, blk->pred[i], cls);
+ }
+
+ val = try_remove_trivial_phi(phi);
+ }
+
+ write_variable(uid, blk, val);
+ return val;
+}
+
+void IRGen::seal_block(Blk* blk) {
+ // Snapshot to_process
+ std::vector<IncompletePhi> to_process;
+ for (auto it = incomplete_phis_.begin(); it != incomplete_phis_.end();) {
+ if (it->block_id == blk->id) {
+ to_process.push_back(std::move(*it));
+ it = incomplete_phis_.erase(it);
+ } else {
+ ++it;
+ }
+ }
+ sealed_[blk->id] = true;
+ for (auto& p : to_process)
+ add_phi_operands(p.var_uid, blk, p.phi, p.cls);
+}
+
+void IRGen::add_phi_operands(int uid, Blk* blk, Phi* phi, int cls) {
+ uint npred = blk->npred;
+ phi->narg = npred;
+ phi->arg = static_cast<Ref*>(alloc(ctx_, npred * sizeof(Ref)));
+ phi->blk = static_cast<Blk**>(alloc(ctx_, npred * sizeof(Blk*)));
+ for (uint i = 0; i < npred; i++) {
+ phi->blk[i] = blk->pred[i];
+ phi->arg[i] = read_variable(uid, blk->pred[i], cls);
+ }
+ try_remove_trivial_phi(phi);
+}
+
+Ref IRGen::try_remove_trivial_phi(Phi* phi) {
+ Ref same = Ref::none();
+ bool same_set = false;
+ for (uint i = 0; i < phi->narg; i++) {
+ if (req(phi->arg[i], phi->to))
+ continue;
+ if (same_set && !req(phi->arg[i], same))
+ return phi->to;
+ same = phi->arg[i];
+ same_set = true;
+ }
+ return same_set ? same : phi->to;
+}
+
+// Emission helpers
+void IRGen::push_ins(Ins i) {
+ vgrow(ctx_, &cur_blk_->ins, cur_blk_->nins + 1);
+ cur_blk_->ins[cur_blk_->nins++] = i;
+}
+
+Ref IRGen::emit_ins(int op, int cls, Ref a0, Ref a1) {
+ Ref dst = new_tmp_ref(cls);
+ Ins i;
+ i.op = static_cast<uint>(op);
+ i.cls = static_cast<uint>(cls);
+ i.to = dst;
+ i.arg[0] = a0;
+ i.arg[1] = a1;
+ push_ins(i);
+ return dst;
+}
+
+void IRGen::emit_store(Ref val, Ref addr, int cls) {
+ int op = (cls == Ks) ? Ostores : Ostorew;
+ Ins i;
+ i.op = static_cast<uint>(op);
+ i.cls = Kw;
+ i.to = Ref::none();
+ i.arg[0] = val;
+ i.arg[1] = addr;
+ push_ins(i);
+}
+
+Ref IRGen::emit_load(Ref addr, int cls) {
+ return emit_ins(Oload, cls, addr, Ref::none());
+}
+
+void IRGen::emit_bulk_zero(Ref addr, int size_bytes) {
+ // Rewrite as Ocall __rcc_memset
+ Ins arg_dst{};
+ arg_dst.op = Oarg;
+ arg_dst.cls = Kl;
+ arg_dst.to = Ref::none();
+ arg_dst.arg[0] = addr;
+ arg_dst.arg[1] = Ref::none();
+ push_ins(arg_dst);
+
+ Ins arg_val{};
+ arg_val.op = Oarg;
+ arg_val.cls = Kw;
+ arg_val.to = Ref::none();
+ arg_val.arg[0] = make_int_ref(0);
+ arg_val.arg[1] = Ref::none();
+ push_ins(arg_val);
+
+ Ins arg_sz{};
+ arg_sz.op = Oarg;
+ arg_sz.cls = Kl;
+ arg_sz.to = Ref::none();
+ arg_sz.arg[0] = make_int_ref(size_bytes);
+ arg_sz.arg[1] = Ref::none();
+ push_ins(arg_sz);
+
+ Ins call{};
+ call.op = Ocall;
+ call.cls = Kw;
+ call.to = Ref::none();
+ call.arg[0] = make_global_ref("__rcc_memset");
+ call.arg[1] = Ref::none();
+ push_ins(call);
+
+ cur_fn_->leaf = 0;
+}
+
+void IRGen::emit_jmp(Blk* target) {
+ if (cur_blk_terminated())
+ return;
+ cur_blk_->jmp.type = Jjmp;
+ cur_blk_->s1 = target;
+ add_edge(cur_blk_, target);
+}
+
+void IRGen::emit_jnz(Ref cond, Blk* true_bb, Blk* false_bb) {
+ if (cur_blk_terminated())
+ return;
+ cur_blk_->jmp.type = Jjnz;
+ cur_blk_->jmp.arg = cond;
+ cur_blk_->s1 = true_bb;
+ cur_blk_->s2 = false_bb;
+ add_edge(cur_blk_, true_bb);
+ add_edge(cur_blk_, false_bb);
+}
+
+void IRGen::emit_ret(Ref val, int cls) {
+ // Choose retw/rets based on cls
+ int jtype = (cls == Ks) ? Jrets : Jretw;
+ cur_blk_->jmp.type = static_cast<short>(jtype);
+ cur_blk_->jmp.arg = val;
+}
+
+void IRGen::emit_ret_void() {
+ cur_blk_->jmp.type = Jret0;
+}
+
+bool IRGen::cur_blk_terminated() const {
+ return cur_blk_ && cur_blk_->jmp.type != Jxxx;
+}
+
+void IRGen::add_edge(Blk* from, Blk* to) {
+ // Add to 'to's pred list
+ uint n = to->npred;
+ if (n == 0) {
+ to->pred = static_cast<Blk**>(vnew(ctx_, 2, sizeof(Blk*), PFn));
+ } else {
+ vgrow(ctx_, &to->pred, n + 1);
+ }
+ to->pred[n] = from;
+ to->npred = n + 1;
+}
+
+Ref IRGen::get_var_addr_ref(Symbol* sym) {
+ if (sym->is_global)
+ return make_global_ref(sym->name.c_str());
+ if (sym->is_param && sym->is_array)
+ return read_variable(sym->uid, cur_blk_, Kl);
+ return Ref::tmp(static_cast<uint>(sym->alloca_temp_id));
+}
+
+} // namespace rcc
diff --git a/src/ir/irgen_expr.cc b/src/ir/irgen_expr.cc
new file mode 100644
index 0000000..e18895c
--- /dev/null
+++ b/src/ir/irgen_expr.cc
@@ -0,0 +1,328 @@
+/* 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 irgen_expr.cc
+/// @brief Expression evaluation + short-circuit conditional jump
+
+#include "rcc/irgen.h"
+
+namespace rcc {
+
+int IRGen::ref_cls_of(Ref r, Fn* fn) {
+ if (rtype(r) == RTmp && r.val >= Tmp0)
+ return fn->tmp[r.val].cls;
+ if (rtype(r) == RCon) {
+ Con* c = &fn->con[r.val];
+ if (c->type == Con::CBits && c->flt)
+ return Ks;
+ }
+ return Kw;
+}
+
+Ref IRGen::coerce(Ref v, int target_cls) {
+ int cur = ref_cls_of(v, cur_fn_);
+ if (cur == target_cls)
+ return v;
+ if (target_cls == Ks && cur == Kw)
+ return emit_ins(Oswtof, Ks, v, Ref::none());
+ if (target_cls == Kw && cur == Ks)
+ return emit_ins(Ostosi, Kw, v, Ref::none());
+ return v;
+}
+
+// Expression evaluation
+
+Ref IRGen::visit_expr(ASTNode* node) {
+ switch (node->kind) {
+ case NodeKind::IntLiteral:
+ return make_int_ref(static_cast<IntLiteralNode*>(node)->value);
+
+ case NodeKind::FloatLiteral:
+ return make_float_ref(static_cast<FloatLiteralNode*>(node)->value);
+
+ case NodeKind::LVal: {
+ auto* lv = static_cast<LValNode*>(node);
+ Symbol* sym = sym_.lookup(lv->name);
+ if (!sym)
+ die("undefined variable");
+
+ // Constant scalar: return constant value directly
+ if (sym->is_const && !sym->is_array && lv->indices.empty()) {
+ if (auto* iv = std::get_if<int32_t>(&sym->const_val))
+ return make_int_ref(*iv);
+ if (auto* fv = std::get_if<float>(&sym->const_val))
+ return make_float_ref(*fv);
+ }
+
+ // Array without index -> return address
+ if (sym->is_array && lv->indices.empty())
+ return get_var_addr_ref(sym);
+
+ if (sym->is_array && !lv->indices.empty()) {
+ if (lv->indices.size() < sym->dims.size() + (sym->is_param ? 1 : 0))
+ return compute_array_addr(lv);
+ Ref addr = compute_array_addr(lv);
+ return emit_load(addr, btype_to_cls(sym->type));
+ }
+
+ // Global variable
+ if (sym->is_global) {
+ Ref addr = make_global_ref(sym->name.c_str());
+ return emit_load(addr, btype_to_cls(sym->type));
+ }
+
+ // Alloca variable
+ if (sym->alloca_temp_id >= 0) {
+ Ref addr = Ref::tmp(static_cast<uint>(sym->alloca_temp_id));
+ return emit_load(addr, btype_to_cls(sym->type));
+ }
+
+ // Normal local scalar: Braun
+ int cls = btype_to_cls(sym->type);
+ return read_variable(sym->uid, cur_blk_, cls);
+ }
+
+ case NodeKind::BinaryExpr: {
+ auto* bin = static_cast<BinaryExprNode*>(node);
+
+ // Logical operation -> short-circuit evaluation -> phi(1,0)
+ if (bin->op == BinOp::And || bin->op == BinOp::Or) {
+ Blk* true_bb = new_blk("cond.true");
+ Blk* false_bb = new_blk("cond.false");
+ Blk* merge_bb = new_blk("cond.merge");
+
+ visit_cond(node, true_bb, false_bb);
+
+ seal_block(true_bb);
+ cur_blk_ = true_bb;
+ emit_jmp(merge_bb);
+
+ seal_block(false_bb);
+ cur_blk_ = false_bb;
+ emit_jmp(merge_bb);
+
+ seal_block(merge_bb);
+ cur_blk_ = merge_bb;
+
+ // Phi: true→1, false→0
+ auto* phi = static_cast<Phi*>(alloc(ctx_, sizeof(Phi)));
+ memset(phi, 0, sizeof(Phi));
+ phi->cls = Kw;
+ phi->to = new_tmp_ref(Kw);
+ phi->narg = 2;
+ phi->arg = static_cast<Ref*>(alloc(ctx_, 2 * sizeof(Ref)));
+ phi->blk = static_cast<Blk**>(alloc(ctx_, 2 * sizeof(Blk*)));
+ phi->arg[0] = make_int_ref(1);
+ phi->blk[0] = true_bb;
+ phi->arg[1] = make_int_ref(0);
+ phi->blk[1] = false_bb;
+ phi->link = merge_bb->phi;
+ merge_bb->phi = phi;
+ return phi->to;
+ }
+
+ Ref lhs = visit_expr(bin->lhs.get());
+ Ref rhs = visit_expr(bin->rhs.get());
+
+ int lk = ref_cls_of(lhs, cur_fn_);
+ int rk = ref_cls_of(rhs, cur_fn_);
+
+ bool is_float = (lk == Ks || rk == Ks);
+ if (is_float && lk == Kw)
+ lhs = emit_ins(Oswtof, Ks, lhs, Ref::none());
+ if (is_float && rk == Kw)
+ rhs = emit_ins(Oswtof, Ks, rhs, Ref::none());
+
+ int rt = is_float ? Ks : Kw;
+ switch (bin->op) {
+ case BinOp::Add:
+ return emit_ins(Oadd, rt, lhs, rhs);
+ case BinOp::Sub:
+ return emit_ins(Osub, rt, lhs, rhs);
+ case BinOp::Mul:
+ return emit_ins(Omul, rt, lhs, rhs);
+ case BinOp::Div:
+ return emit_ins(Odiv, rt, lhs, rhs);
+ case BinOp::Mod:
+ return emit_ins(Orem, rt, lhs, rhs);
+ case BinOp::Lt:
+ return emit_ins(is_float ? Oclts : Ocsltw, Kw, lhs, rhs);
+ case BinOp::Gt:
+ return emit_ins(is_float ? Ocgts : Ocsgtw, Kw, lhs, rhs);
+ case BinOp::Le:
+ return emit_ins(is_float ? Ocles : Ocslew, Kw, lhs, rhs);
+ case BinOp::Ge:
+ return emit_ins(is_float ? Ocges : Ocsgew, Kw, lhs, rhs);
+ case BinOp::Eq:
+ return emit_ins(is_float ? Oceqs : Oceqw, Kw, lhs, rhs);
+ case BinOp::Ne:
+ return emit_ins(is_float ? Ocnes : Ocnew, Kw, lhs, rhs);
+ default:
+ return make_int_ref(0);
+ }
+ }
+
+ case NodeKind::UnaryExpr: {
+ auto* u = static_cast<UnaryExprNode*>(node);
+ Ref operand = visit_expr(u->operand.get());
+ switch (u->op) {
+ case UnaryOp::Pos:
+ return operand;
+ case UnaryOp::Neg: {
+ int k = ref_cls_of(operand, cur_fn_);
+ if (k == Ks)
+ return emit_ins(Oneg, Ks, operand, Ref::none());
+ return emit_ins(Osub, Kw, make_int_ref(0), operand);
+ }
+ case UnaryOp::Not:
+ return emit_ins(Oceqw, Kw, operand, make_int_ref(0));
+ }
+ return make_int_ref(0);
+ }
+
+ case NodeKind::FuncCall: {
+ auto* call_node = static_cast<FuncCallNode*>(node);
+
+ // starttime/stoptime special handling
+ if (call_node->name == "starttime" || call_node->name == "stoptime") {
+ std::string real_name = "_sysy_" + call_node->name;
+ Ref fn_ref = make_global_ref(real_name.c_str());
+ Ref line_ref = make_int_ref(call_node->lineno);
+
+ // arg w, line
+ Ins arg_i;
+ arg_i.op = Oarg;
+ arg_i.cls = Kw;
+ arg_i.to = Ref::none();
+ arg_i.arg[0] = line_ref;
+ arg_i.arg[1] = Ref::none();
+ push_ins(arg_i);
+
+ // call
+ Ins call_i;
+ call_i.op = Ocall;
+ call_i.cls = Kw;
+ call_i.to = Ref::none();
+ call_i.arg[0] = fn_ref;
+ call_i.arg[1] = Ref::none();
+ push_ins(call_i);
+ return make_int_ref(0);
+ }
+
+ FuncSig* fsig = sym_.lookup_func(call_node->name);
+ bool is_void = fsig && fsig->ret_type == BType::Void;
+ int ret_cls = fsig ? btype_to_cls(fsig->ret_type) : Kw;
+
+ std::vector<Ref> arg_refs;
+ std::vector<int> arg_clses;
+ arg_refs.reserve(call_node->args.size());
+ arg_clses.reserve(call_node->args.size());
+ for (size_t i = 0; i < call_node->args.size(); i++) {
+ Ref v = visit_expr(call_node->args[i].get());
+ int acls = Kw;
+ if (fsig && i < fsig->params.size())
+ acls = fsig->params[i].second ? Kl : btype_to_cls(fsig->params[i].first);
+
+ if (acls == Kw || acls == Ks)
+ v = coerce(v, acls);
+ arg_refs.push_back(v);
+ arg_clses.push_back(acls);
+ }
+ for (size_t i = 0; i < arg_refs.size(); i++) {
+ Ins arg_i;
+ arg_i.op = Oarg;
+ arg_i.cls = static_cast<uint>(arg_clses[i]);
+ arg_i.to = Ref::none();
+ arg_i.arg[0] = arg_refs[i];
+ arg_i.arg[1] = Ref::none();
+ push_ins(arg_i);
+ }
+
+ // call instruction
+ Ref fn_ref = make_global_ref(call_node->name.c_str());
+ if (is_void) {
+ Ins call_i;
+ call_i.op = Ocall;
+ call_i.cls = Kw;
+ call_i.to = Ref::none();
+ call_i.arg[0] = fn_ref;
+ call_i.arg[1] = Ref::none();
+ push_ins(call_i);
+ cur_fn_->leaf = 0;
+ return make_int_ref(0);
+ }
+
+ Ref dst = new_tmp_ref(ret_cls);
+ Ins call_i;
+ call_i.op = Ocall;
+ call_i.cls = static_cast<uint>(ret_cls);
+ call_i.to = dst;
+ call_i.arg[0] = fn_ref;
+ call_i.arg[1] = Ref::none();
+ push_ins(call_i);
+ cur_fn_->leaf = 0;
+ return dst;
+ }
+
+ case NodeKind::ImplicitCast: {
+ auto* c = static_cast<ImplicitCastNode*>(node);
+ Ref v = visit_expr(c->operand.get());
+ int k = ref_cls_of(v, cur_fn_);
+ if (c->target_type == BType::Float && k == Kw)
+ return emit_ins(Oswtof, Ks, v, Ref::none());
+ if (c->target_type == BType::Int && k == Ks)
+ return emit_ins(Ostosi, Kw, v, Ref::none());
+ return v;
+ }
+
+ default:
+ return make_int_ref(0);
+ }
+}
+
+// Short-circuit conditional jump
+
+void IRGen::visit_cond(ASTNode* cond, Blk* true_bb, Blk* false_bb) {
+ if (auto* bin = dynamic_cast<BinaryExprNode*>(cond)) {
+ if (bin->op == BinOp::And)
+ return visit_cond_and(bin, true_bb, false_bb);
+ if (bin->op == BinOp::Or)
+ return visit_cond_or(bin, true_bb, false_bb);
+ }
+ Ref val = visit_expr(cond);
+ emit_jnz(val, true_bb, false_bb);
+}
+
+void IRGen::visit_cond_and(BinaryExprNode* node, Blk* true_bb, Blk* false_bb) {
+ Blk* rhs_bb = new_blk("and.rhs");
+ visit_cond(node->lhs.get(), rhs_bb, false_bb);
+ seal_block(rhs_bb);
+ cur_blk_ = rhs_bb;
+ visit_cond(node->rhs.get(), true_bb, false_bb);
+}
+
+void IRGen::visit_cond_or(BinaryExprNode* node, Blk* true_bb, Blk* false_bb) {
+ Blk* rhs_bb = new_blk("or.rhs");
+ visit_cond(node->lhs.get(), true_bb, rhs_bb);
+ seal_block(rhs_bb);
+ cur_blk_ = rhs_bb;
+ visit_cond(node->rhs.get(), true_bb, false_bb);
+}
+
+} // namespace rcc
diff --git a/src/ir/irgen_util.cc b/src/ir/irgen_util.cc
new file mode 100644
index 0000000..e7e89f5
--- /dev/null
+++ b/src/ir/irgen_util.cc
@@ -0,0 +1,244 @@
+/* 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 irgen_util.cc
+/// @brief Constant evaluation, array init flattening, array address computation
+
+#include "rcc/irgen.h"
+
+#include <cstddef>
+
+namespace rcc {
+
+// Compile-time constant evaluation
+
+int32_t IRGen::eval_const_int(ASTNode* node) {
+ switch (node->kind) {
+ case NodeKind::IntLiteral:
+ return static_cast<IntLiteralNode*>(node)->value;
+ case NodeKind::FloatLiteral:
+ return static_cast<int32_t>(static_cast<FloatLiteralNode*>(node)->value);
+ case NodeKind::LVal: {
+ auto* lv = static_cast<LValNode*>(node);
+ Symbol* sym = sym_.lookup(lv->name);
+ if (sym && sym->is_const && !sym->is_array) {
+ if (auto* iv = std::get_if<int32_t>(&sym->const_val))
+ return *iv;
+ if (auto* fv = std::get_if<float>(&sym->const_val))
+ return static_cast<int32_t>(*fv);
+ }
+ return 0;
+ }
+ case NodeKind::UnaryExpr: {
+ auto* u = static_cast<UnaryExprNode*>(node);
+ int32_t v = eval_const_int(u->operand.get());
+ if (u->op == UnaryOp::Neg)
+ return -v;
+ if (u->op == UnaryOp::Not)
+ return !v;
+ return v;
+ }
+ case NodeKind::BinaryExpr: {
+ auto* b = static_cast<BinaryExprNode*>(node);
+ int32_t l = eval_const_int(b->lhs.get());
+ int32_t r = eval_const_int(b->rhs.get());
+ switch (b->op) {
+ case BinOp::Add:
+ return l + r;
+ case BinOp::Sub:
+ return l - r;
+ case BinOp::Mul:
+ return l * r;
+ case BinOp::Div:
+ return r ? l / r : 0;
+ case BinOp::Mod:
+ return r ? l % r : 0;
+ case BinOp::Lt:
+ return l < r;
+ case BinOp::Gt:
+ return l > r;
+ case BinOp::Le:
+ return l <= r;
+ case BinOp::Ge:
+ return l >= r;
+ case BinOp::Eq:
+ return l == r;
+ case BinOp::Ne:
+ return l != r;
+ case BinOp::And:
+ return l && r;
+ case BinOp::Or:
+ return l || r;
+ }
+ return 0;
+ }
+ default:
+ return 0;
+ }
+}
+
+float IRGen::eval_const_float(ASTNode* node) {
+ switch (node->kind) {
+ case NodeKind::FloatLiteral:
+ return static_cast<FloatLiteralNode*>(node)->value;
+ case NodeKind::IntLiteral:
+ return static_cast<float>(static_cast<IntLiteralNode*>(node)->value);
+ case NodeKind::LVal: {
+ auto* lv = static_cast<LValNode*>(node);
+ Symbol* sym = sym_.lookup(lv->name);
+ if (sym && sym->is_const && !sym->is_array) {
+ if (auto* fv = std::get_if<float>(&sym->const_val))
+ return *fv;
+ if (auto* iv = std::get_if<int32_t>(&sym->const_val))
+ return static_cast<float>(*iv);
+ }
+ return 0.0f;
+ }
+ case NodeKind::UnaryExpr: {
+ auto* u = static_cast<UnaryExprNode*>(node);
+ float v = eval_const_float(u->operand.get());
+ if (u->op == UnaryOp::Neg)
+ return -v;
+ return v;
+ }
+ case NodeKind::BinaryExpr: {
+ auto* b = static_cast<BinaryExprNode*>(node);
+ float l = eval_const_float(b->lhs.get());
+ float r = eval_const_float(b->rhs.get());
+ switch (b->op) {
+ case BinOp::Add:
+ return l + r;
+ case BinOp::Sub:
+ return l - r;
+ case BinOp::Mul:
+ return l * r;
+ case BinOp::Div:
+ return r != 0.0f ? l / r : 0.0f;
+ default:
+ return 0.0f;
+ }
+ }
+ default:
+ return 0.0f;
+ }
+}
+
+// Array initialization flattening
+
+void IRGen::flatten_init(const std::vector<int>& dims, ASTNode* init, std::vector<ASTNode*>& out) {
+ if (!init)
+ return;
+
+ if (init->kind != NodeKind::InitList) {
+ out.push_back(init);
+ return;
+ }
+
+ auto* list = static_cast<InitListNode*>(init);
+ if (dims.empty())
+ return;
+
+ int sub_size = 1;
+ for (size_t i = 1; i < dims.size(); i++)
+ sub_size *= dims[i];
+
+ int total = dims[0] * sub_size;
+ int pos = 0;
+
+ for (auto& item : list->items) {
+ if (pos >= total)
+ break;
+
+ if (item->kind == NodeKind::InitList) {
+ if (dims.size() > 1) {
+ std::vector<int> sub_dims(dims.begin() + 1, dims.end());
+ int before = static_cast<int>(out.size());
+ flatten_init(sub_dims, item.get(), out);
+ int filled = static_cast<int>(out.size()) - before;
+ while (filled < sub_size) {
+ out.push_back(nullptr);
+ filled++;
+ }
+ pos += sub_size;
+ } else {
+ auto* inner = static_cast<InitListNode*>(item.get());
+ for (auto& elem : inner->items) {
+ if (pos < total) {
+ out.push_back(elem.get());
+ pos++;
+ }
+ }
+ }
+ } else {
+ out.push_back(item.get());
+ pos++;
+ }
+ }
+
+ while (pos < total) {
+ out.push_back(nullptr);
+ pos++;
+ }
+}
+
+// Array address calculation
+
+Ref IRGen::compute_array_addr(LValNode* node) {
+ Symbol* sym = sym_.lookup(node->name);
+ Ref base = get_var_addr_ref(sym);
+
+ if (sym->is_param && sym->is_array)
+ base = read_variable(sym->uid, cur_blk_, Kl);
+
+ const auto& dims = sym->dims;
+ size_t ndims = dims.size();
+
+ Ref offset = Ref::none();
+ bool offset_valid = false;
+
+ for (size_t i = 0; i < node->indices.size(); i++) {
+ Ref idx = visit_expr(node->indices[i].get());
+
+ int stride = 1;
+ size_t dim_idx = sym->is_param ? i : (i + 1);
+ for (size_t j = dim_idx; j < ndims; j++)
+ stride *= dims[j];
+
+ // Extend index to 'l' type for pointer arithmetic
+ int idx_cls = Kw;
+ if (rtype(idx) == RTmp && idx.val >= Tmp0)
+ idx_cls = cur_fn_->tmp[idx.val].cls;
+ if (idx_cls == Kw)
+ idx = emit_ins(Oextsw, Kl, idx, Ref::none());
+
+ Ref byte_off = emit_ins(Omul, Kl, idx, make_int_ref(stride * 4));
+
+ if (offset_valid) {
+ offset = emit_ins(Oadd, Kl, offset, byte_off);
+ } else {
+ offset = byte_off;
+ offset_valid = true;
+ }
+ }
+
+ if (offset_valid)
+ return emit_ins(Oadd, Kl, base, offset);
+ return base;
+}
+
+} // namespace rcc
diff --git a/src/ir/print.cc b/src/ir/print.cc
new file mode 100644
index 0000000..d452a6b
--- /dev/null
+++ b/src/ir/print.cc
@@ -0,0 +1,388 @@
+/* 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 print.cc
+/// @brief IR text output: Fn / Blk / Phi / Ins / Jmp / Con / Ref
+
+#include "rcc/all.h"
+#include "rcc/irgen.h"
+
+#include <cinttypes>
+
+namespace rcc {
+
+namespace {
+
+/// K class characters: Kw='w' Kl='l' Ks='s' Kd='d'. Others are treated as errors.
+char cls_char(int k) {
+ switch (k) {
+ case Kw:
+ return 'w';
+ case Kl:
+ return 'l';
+ case Ks:
+ return 's';
+ case Kd:
+ return 'd';
+ default:
+ return '?';
+ }
+}
+
+const char* jmp_name(int j) {
+ static constexpr auto tab = [] {
+ std::array<const char*, NJmp> a{};
+#define X(n) a[J##n] = #n;
+ JMPS(X)
+#undef X
+ return a;
+ }();
+ if (j <= Jxxx || j >= NJmp)
+ return "??";
+ return tab[static_cast<size_t>(j)];
+}
+
+/// Mem (= Addr) internal addressing component output: [ offset + base + scale*index ]
+void print_mem(Mem* m, Fn* fn, FILE* f);
+
+bool op_needs_cls_suffix(int op) {
+ switch (op) {
+ case Oarg:
+ case Oswap:
+ case Oxcmp:
+ case Oacmp:
+ case Oacmn:
+ case Oafcmp:
+ case Oxtest:
+ case Oxdiv:
+ case Oxidiv:
+ return true;
+ default:
+ return false;
+ }
+}
+
+} // namespace
+
+// printcon
+
+static void printcon(Con* c, FILE* f) {
+ switch (c->type) {
+ case Con::CUndef:
+ return;
+ case Con::CAddr:
+ if (c->sym.type & Sym::SExt)
+ fputs("extern ", f);
+ if (c->sym.type & Sym::SThr)
+ fputs("thread ", f);
+ fprintf(f, "$%s", str(c->sym.id));
+ if (c->bits.i)
+ fprintf(f, "%+" PRId64, c->bits.i);
+ return;
+ case Con::CBits:
+ if (c->flt == 1)
+ fprintf(f, "s_%f", static_cast<double>(c->bits.s));
+ else if (c->flt == 2)
+ fprintf(f, "d_%lf", c->bits.d);
+ else
+ fprintf(f, "%" PRId64, c->bits.i);
+ return;
+ }
+}
+
+// printref
+
+void printref(Ref r, Fn* fn, FILE* f) {
+ switch (rtype(r)) {
+ case RTmp:
+ if (r.val < Tmp0)
+ fprintf(f, "R%u", r.val);
+ else
+ fprintf(f, "%%%s", fn->tmp[r.val].name);
+ return;
+
+ case RCon:
+ if (req(r, Ref::undef()))
+ fputs("UNDEF", f);
+ else
+ printcon(&fn->con[r.val], f);
+ return;
+
+ case RSlot:
+ fprintf(f, "S%d", rsval(r));
+ return;
+
+ case RCall:
+ fprintf(f, "%04x", r.val);
+ return;
+
+ case RType:
+ fprintf(f, ":%s", typ[r.val].name);
+ return;
+
+ case RMem:
+ print_mem(&fn->mem[r.val], fn, f);
+ return;
+
+ case RInt:
+ fprintf(f, "%d", rsval(r));
+ return;
+
+ case -1:
+ fputc('R', f);
+ return;
+ }
+}
+
+namespace {
+
+void print_mem(Mem* m, Fn* fn, FILE* f) {
+ fputc('[', f);
+ bool need_plus = false;
+ if (m->offset.type != Con::CUndef) {
+ printcon(&m->offset, f);
+ need_plus = true;
+ }
+ if (!req(m->base, Ref::none())) {
+ if (need_plus)
+ fputs(" + ", f);
+ printref(m->base, fn, f);
+ need_plus = true;
+ }
+ if (!req(m->index, Ref::none())) {
+ if (need_plus)
+ fputs(" + ", f);
+ fprintf(f, "%d * ", m->scale);
+ printref(m->index, fn, f);
+ }
+ fputc(']', f);
+}
+
+// Phi / Ins / Jmp single-line output
+
+void print_phi_line(Phi* p, Fn* fn, FILE* f) {
+ fputc('\t', f);
+ printref(p->to, fn, f);
+ fprintf(f, " =%c phi ", cls_char(p->cls));
+ assert(p->narg && "phi has no arg");
+ for (uint n = 0; n < p->narg; n++) {
+ fprintf(f, "@%s ", p->blk[n]->name);
+ printref(p->arg[n], fn, f);
+ if (n + 1 < p->narg)
+ fputs(", ", f);
+ }
+ fputc('\n', f);
+}
+
+void print_ins_line(Ins* i, Fn* fn, FILE* f) {
+ fputc('\t', f);
+ if (!req(i->to, Ref::none())) {
+ printref(i->to, fn, f);
+ fprintf(f, " =%c ", cls_char(static_cast<int>(i->cls)));
+ }
+ assert(optab[i->op].name && "missing op name in optab");
+ fputs(optab[i->op].name, f);
+
+ if (req(i->to, Ref::none()) && op_needs_cls_suffix(i->op))
+ fputc(cls_char(static_cast<int>(i->cls)), f);
+
+ if (!req(i->arg[0], Ref::none())) {
+ fputc(' ', f);
+ printref(i->arg[0], fn, f);
+ }
+ if (!req(i->arg[1], Ref::none())) {
+ fputs(", ", f);
+ printref(i->arg[1], fn, f);
+ }
+ fputc('\n', f);
+}
+
+/// Terminating instructions: ret / jmp / jnz / floating-point conditional jump / hlt
+void print_jmp_line(Blk* b, Fn* fn, FILE* f) {
+ int jt = b->jmp.type;
+
+ if (isret(jt)) {
+ fputs("\tret", f);
+ if (jt != Jret0 || !req(b->jmp.arg, Ref::none())) {
+ fputc(' ', f);
+ printref(b->jmp.arg, fn, f);
+ }
+ fputc('\n', f);
+ return;
+ }
+
+ if (jt == Jhlt) {
+ fputs("\thlt\n", f);
+ return;
+ }
+
+ if (jt == Jjmp) {
+ // If fall-through to next block, jmp can be omitted (QBE output style)
+ if (b->s1 != b->link)
+ fprintf(f, "\tjmp @%s\n", b->s1->name);
+ return;
+ }
+
+ // jnz or jf* series conditional jumps: two successors
+ fprintf(f, "\t%s ", jmp_name(jt));
+ if (jt == Jjnz) {
+ printref(b->jmp.arg, fn, f);
+ fputs(", ", f);
+ }
+ assert(b->s1 && b->s2 && "branch missing successor");
+ fprintf(f, "@%s, @%s\n", b->s1->name, b->s2->name);
+}
+
+void print_call(Ins* call, Ins** args, size_t nargs, Fn* fn, FILE* f) {
+ fputc('\t', f);
+ if (!req(call->to, Ref::none())) {
+ printref(call->to, fn, f);
+ fprintf(f, " =%c ", cls_char(static_cast<int>(call->cls)));
+ }
+ fputs("call ", f);
+ printref(call->arg[0], fn, f);
+ fputc('(', f);
+ for (size_t k = 0; k < nargs; k++) {
+ if (k)
+ fputs(", ", f);
+ Ins* a = args[k];
+ fprintf(f, "%c ", cls_char(static_cast<int>(a->cls)));
+ printref(a->arg[0], fn, f);
+ }
+ fputs(")\n", f);
+}
+
+void print_block(Blk* b, Fn* fn, FILE* f) {
+ fprintf(f, "@%s\n", b->name);
+ for (Phi* p = b->phi; p; p = p->link)
+ print_phi_line(p, fn, f);
+
+ // Scan: skip Opar at entry block start (already in signature); Oarg sequence is merged and
+ // printed when encountering Ocall, and call itself is output at the merge point.
+ Ins* pending_args[NIns];
+ size_t npending = 0;
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (i->op == Onop)
+ continue; // promote/coalesce marks instructions as nop to "delete" them, QBE text
+ // doesn't accept nop keyword
+ if (i->op == Opar)
+ continue;
+ if (i->op == Oarg) {
+ assert(npending < NIns && "too many call args");
+ pending_args[npending++] = i;
+ continue;
+ }
+ if (i->op == Ocall) {
+ print_call(i, pending_args, npending, fn, f);
+ npending = 0;
+ continue;
+ }
+ // There should be no dangling Oarg before normal instructions
+ assert(npending == 0 && "orphan arg before non-call op");
+ print_ins_line(i, fn, f);
+ }
+ assert(npending == 0 && "orphan arg at end of block");
+
+ print_jmp_line(b, fn, f);
+}
+
+void print_signature(Fn* fn, FILE* f) {
+ fputs("export\nfunction ", f);
+
+ int ret_cls = Kx;
+ for (Blk* b = fn->start; b; b = b->link) {
+ int jt = b->jmp.type;
+ if (!isret(jt))
+ continue;
+ if (jt >= Jretw && jt <= Jretd)
+ ret_cls = jt - Jretw; // Jretw..Jretd align with Kw..Kd
+ break;
+ }
+ if (ret_cls >= 0)
+ fprintf(f, "%c ", cls_char(ret_cls));
+ fprintf(f, "$%s(", fn->name);
+
+ // Scan Opar in entry to collect parameters
+ Blk* entry = fn->start;
+ bool first = true;
+ if (entry) {
+ for (uint k = 0; k < entry->nins; k++) {
+ Ins* i = &entry->ins[k];
+ if (i->op != Opar) // Opar are contiguous at entry head, break on first non-Opar
+ break;
+ if (!first)
+ fputs(", ", f);
+ fprintf(f, "%c ", cls_char(static_cast<int>(i->cls)));
+ printref(i->to, fn, f);
+ first = false;
+ }
+ }
+ fputs(") {\n", f);
+}
+
+} // namespace
+
+// printfn - Top-level assembly
+
+void printfn(Fn* fn, FILE* f) {
+ print_signature(fn, f);
+ for (Blk* b = fn->start; b; b = b->link)
+ print_block(b, fn, f);
+ fputs("}\n", f);
+}
+
+// print_globals - Data segment
+
+namespace {
+
+constexpr size_t ELEM_BYTES = 4; // Kw / Ks are both 4 bytes
+
+void print_global_cell(int elem_cls, const std::variant<int32_t, float>& v, FILE* f) {
+ if (elem_cls == Ks) {
+ // QBE floating-point constant literal s_<float>
+ float fv = std::holds_alternative<float>(v) ? std::get<float>(v)
+ : static_cast<float>(std::get<int32_t>(v));
+ fprintf(f, "s s_%f", static_cast<double>(fv));
+ } else {
+ int32_t iv = std::holds_alternative<int32_t>(v) ? std::get<int32_t>(v)
+ : static_cast<int32_t>(std::get<float>(v));
+ fprintf(f, "w %d", iv);
+ }
+}
+
+} // namespace
+
+void print_globals(const std::vector<GlobalData>& globals, FILE* f) {
+ for (const auto& g : globals) {
+ fprintf(f, "data $%s = align 4 { ", g.name.c_str());
+ if (g.is_zeroinit) {
+ fprintf(f, "z %zu", g.total_elems * ELEM_BYTES);
+ } else {
+ for (size_t i = 0; i < g.init_vals.size(); i++) {
+ if (i)
+ fputs(", ", f);
+ print_global_cell(g.elem_cls, g.init_vals[i], f);
+ }
+ }
+ fputs(" }\n", f);
+ }
+}
+
+} // namespace rcc
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..a4b88ec
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,386 @@
+/* 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 main.cc
+/// @brief RCC compiler entry point.
+
+#include "rcc/all.h"
+#include "rcc/ast.h"
+#include "rcc/irgen.h"
+#include "rcc/rv64.h"
+
+#include <cctype>
+#include <cstdio>
+#include <cstring>
+#include <string>
+
+// External Flex/Bison declarations
+extern FILE* yyin;
+extern int yyparse();
+extern rcc::CompUnitNode* g_ast_root;
+
+namespace {
+
+struct Args {
+ std::string input;
+ std::string output;
+ int opt_level = 0;
+ bool dump_ast = false;
+ bool dump_ir = false;
+ bool emit_il = false; // Output QBE-compatible SSA text to -o (or stdout)
+ bool dbg = false; // Set if any -d flag is enabled; triggers skipping emit
+};
+
+Args parse_args(int argc, char* argv[]) {
+ Args args;
+ for (int i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "-S") == 0) {
+ continue;
+ } else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
+ args.output = argv[++i];
+ } else if (strcmp(argv[i], "-O1") == 0) {
+ args.opt_level = 1;
+ } else if (strcmp(argv[i], "-O2") == 0) {
+ args.opt_level = 2;
+ } else if (strcmp(argv[i], "--dump-ast") == 0) {
+ args.dump_ast = true;
+ } else if (strcmp(argv[i], "--dump-ir") == 0) {
+ args.dump_ir = true;
+ } else if (strcmp(argv[i], "--emit-il") == 0) {
+ args.emit_il = true;
+ } else if (strcmp(argv[i], "-d") == 0 && i + 1 < argc) {
+ for (char* p = argv[++i]; *p; ++p) {
+ unsigned char c = static_cast<unsigned char>(*p);
+ if (std::isalpha(c)) {
+ rcc::debug[std::toupper(c)] = 1;
+ args.dbg = true;
+ }
+ }
+ } else if (argv[i][0] != '-') {
+ args.input = argv[i];
+ }
+ }
+
+ if (args.input.empty()) {
+ fprintf(stderr, "rcc: error: no input file\n");
+ fprintf(stderr, "usage: compiler input.sy -S -o output.s [-O1] [--emit-il] [-d <flags>]\n");
+ exit(1);
+ }
+ if (args.output.empty()) {
+ args.output = args.emit_il ? "-" : "a.s";
+ }
+ return args;
+}
+
+// globals → Dat* → emitdat
+
+constexpr size_t kElemBytes = 4;
+
+void emit_one_global(const rcc::GlobalData& g, FILE* f) {
+ rcc::Lnk lnk{};
+ lnk.align = 4;
+ // Do not export: SysY globals are not visible externally.
+
+ std::string name = g.name;
+ rcc::Dat d{};
+ d.lnk = &lnk;
+ d.name = name.data();
+
+ d.type = rcc::Dat::DStart;
+ rcc::emitdat(&d, f);
+
+ if (g.is_zeroinit) {
+ d.type = rcc::Dat::DZ;
+ d.u.num = static_cast<int64_t>(g.total_elems * kElemBytes);
+ rcc::emitdat(&d, f);
+ } else {
+ for (const auto& v : g.init_vals) {
+ int32_t bits32 = 0;
+ if (std::holds_alternative<int32_t>(v)) {
+ int32_t iv = std::get<int32_t>(v);
+ if (g.elem_cls == rcc::Ks) {
+ float fv = static_cast<float>(iv);
+ std::memcpy(&bits32, &fv, sizeof(bits32));
+ } else {
+ bits32 = iv;
+ }
+ } else {
+ float fv = std::get<float>(v);
+ if (g.elem_cls == rcc::Kw) {
+ bits32 = static_cast<int32_t>(fv);
+ } else {
+ std::memcpy(&bits32, &fv, sizeof(bits32));
+ }
+ }
+ d.type = rcc::Dat::DW;
+ d.isref = 0;
+ d.isstr = 0;
+ d.u.num = static_cast<int64_t>(static_cast<uint32_t>(bits32));
+ rcc::emitdat(&d, f);
+ }
+ }
+
+ d.type = rcc::Dat::DEnd;
+ rcc::emitdat(&d, f);
+}
+
+void emit_globals_asm(const std::vector<rcc::GlobalData>& globals, FILE* f) {
+ for (const auto& g : globals)
+ emit_one_global(g, f);
+}
+
+// Mid-end pass chain
+// ssacheck after gcm; ifconvert only if '-d K' is set.
+void run_midend(rcc::Ctx* ctx, rcc::Fn* fn) {
+ ctx->fn = fn;
+ ctx->spill_fp_live_peak = 0;
+ rcc::T.abi0(fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::promote(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssa(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::filldom(fn);
+ rcc::loopinterchange(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::ssacheck(fn);
+ rcc::lsr(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::loop_idiom(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::ssacheck(fn);
+ rcc::dce(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::reduction_unroll(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::ssacheck(fn);
+ rcc::dce(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::dead_loop(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::unroll(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::unswitch(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::tre(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::fillalias(fn);
+ rcc::loadopt(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::sccp(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::ssacheck(fn);
+ rcc::fillalias(fn);
+ rcc::licm(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::fillalias(fn);
+ rcc::scalar_repl(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::coalesce(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::ssacheck(fn);
+ rcc::sccp(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::ssacheck(fn);
+ rcc::reassoc(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ rcc::gvn(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::simplcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::gcm(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::divopt(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::mulopt(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::storemrg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::dce(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::ssacheck(fn);
+ if (rcc::T.cansel) {
+ rcc::ifconvert(ctx, fn);
+ rcc::fillcfg(ctx, fn);
+ rcc::filluse(ctx, fn);
+ rcc::filldom(fn);
+ rcc::ssacheck(fn);
+ }
+}
+
+// Backend pass chain
+void run_backend(rcc::Ctx* ctx, rcc::Fn* fn) {
+#define STAGE(call) \
+ do { \
+ call; \
+ if (rcc::debug['B']) { \
+ fprintf(stderr, "\n>>>> after %s (%s):\n", #call, fn->name); \
+ rcc::printfn(fn, stderr); \
+ } \
+ } while (0)
+ STAGE(rcc::T.abi1(ctx, fn));
+ STAGE(rcc::simpl(ctx, fn));
+ STAGE(rcc::fillcfg(ctx, fn));
+ STAGE(rcc::filluse(ctx, fn));
+ STAGE(rcc::T.isel(ctx, fn));
+ STAGE(rcc::fillcfg(ctx, fn));
+ STAGE(rcc::filllive(ctx, fn));
+ STAGE(rcc::fillloop(fn));
+ STAGE(rcc::fillcost(ctx, fn));
+ STAGE(rcc::spill(ctx, fn));
+ STAGE(rcc::rega(ctx, fn));
+ STAGE(rcc::peephole(ctx, fn));
+ STAGE(rcc::fillcfg(ctx, fn));
+ STAGE(rcc::simpljmp(ctx, fn));
+ STAGE(rcc::fillcfg(ctx, fn));
+#undef STAGE
+
+ // Convert rpo[] to linked list for rv64_emitfn
+ assert(fn->rpo[0] == fn->start);
+ for (uint n = 0;; ++n) {
+ if (n == fn->nblk - 1) {
+ fn->rpo[n]->link = nullptr;
+ break;
+ }
+ fn->rpo[n]->link = fn->rpo[n + 1];
+ }
+}
+
+} // namespace
+
+int main(int argc, char* argv[]) {
+ auto args = parse_args(argc, argv);
+
+ // Front-end: Lexical + Syntax Analysis → AST
+ yyin = fopen(args.input.c_str(), "r");
+ if (!yyin) {
+ fprintf(stderr, "rcc: error: cannot open '%s'\n", args.input.c_str());
+ return 1;
+ }
+
+ int parse_ret = yyparse();
+ fclose(yyin);
+
+ if (parse_ret != 0 || !g_ast_root) {
+ fprintf(stderr, "rcc: error: parsing failed\n");
+ return 1;
+ }
+
+ if (args.dump_ast) {
+ rcc::ast_dump(g_ast_root);
+ }
+
+ // Target descriptor: rv64 lp64d. Assigned before worker creation, read-only afterwards.
+ rcc::T = rcc::T_rv64;
+
+ rcc::Ctx ctx_main;
+ ctx_main.init();
+ ctx_main.subpool = new rcc::SubPool();
+ ctx_main.dbg_buf = stderr; // Dump destination for if (debug['X'])
+
+ // IR Generation: AST → SSA IR (Braun algorithm)
+ rcc::IRGen irgen(&ctx_main);
+ auto ir = irgen.generate(g_ast_root);
+
+ if (args.dump_ir) {
+ fprintf(stderr, "[%zu functions, %zu globals]\n", ir.functions.size(), ir.globals.size());
+ }
+
+ rcc::inline_module(&ctx_main, ir.functions);
+
+ if (args.emit_il) {
+ for (rcc::Fn* fn : ir.functions)
+ run_midend(&ctx_main, fn);
+
+ FILE* out = (args.output == "-") ? stdout : fopen(args.output.c_str(), "w");
+ if (!out) {
+ fprintf(stderr, "rcc: error: cannot open '%s' for write\n", args.output.c_str());
+ return 1;
+ }
+ rcc::print_globals(ir.globals, out);
+ for (rcc::Fn* fn : ir.functions)
+ rcc::printfn(fn, out);
+ if (out != stdout)
+ fclose(out);
+ } else {
+ FILE* outf = nullptr;
+ if (!args.dbg) {
+ outf = (args.output == "-") ? stdout : fopen(args.output.c_str(), "w");
+ if (!outf) {
+ fprintf(stderr, "rcc: error: cannot open '%s' for write\n", args.output.c_str());
+ return 1;
+ }
+ emit_globals_asm(ir.globals, outf);
+ }
+
+ for (rcc::Fn* fn : ir.functions) {
+ if (args.dbg)
+ fprintf(stderr, "**** Function %s ****", fn->name);
+ run_midend(&ctx_main, fn);
+ run_backend(&ctx_main, fn);
+ if (!args.dbg) {
+ rcc::set_die_ctx(&ctx_main);
+ rcc::T.emitfn(fn, outf);
+ fprintf(outf, "/* end function %s */\n\n", fn->name);
+ } else {
+ fprintf(stderr, "\n");
+ }
+ }
+
+ if (!args.dbg) {
+ rcc::T.emitfin(&ctx_main, outf);
+ if (outf != stdout)
+ fclose(outf);
+ }
+ }
+
+ // Clean AST
+ delete g_ast_root;
+ g_ast_root = nullptr;
+
+ // Release SubPool; ~Ctx automatically frees insb
+ ctx_main.release();
+
+ return 0;
+}
diff --git a/src/mid/abi.cc b/src/mid/abi.cc
new file mode 100644
index 0000000..c4924da
--- /dev/null
+++ b/src/mid/abi.cc
@@ -0,0 +1,45 @@
+/* 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 elimsb - Eliminate ABI-level sub-word op variants (sb/ub/sh/uh for arg/par/ret).
+///
+/// See docs/abi.md for architectural notes.
+
+#include "rcc/all.h"
+
+namespace rcc {
+
+void elimsb(Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ Ins* ie = b->ins + b->nins;
+ for (Ins* i = b->ins; i < ie; ++i) {
+ if (isargbh(i->op)) {
+ i->op = Oarg;
+ } else if (isparbh(i->op)) {
+ i->op = Opar;
+ }
+ }
+ if (isretbh(b->jmp.type)) {
+ b->jmp.type = Jretw;
+ }
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/alias.cc b/src/mid/alias.cc
new file mode 100644
index 0000000..8dc6041
--- /dev/null
+++ b/src/mid/alias.cc
@@ -0,0 +1,445 @@
+/* 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 alias.cc
+/// @brief Alias analysis: computes a conservative Alias descriptor for each tmp, serving downstream
+/// load/mem optimizations.
+///
+/// See docs/alias.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cstring>
+
+namespace rcc {
+
+void getalias(Alias* a, Ref r, Fn* fn) {
+ switch (rtype(r)) {
+ case RTmp:
+ *a = fn->tmp[r.val].alias;
+ // astack: follow to root slot to get latest type (slot might have been upgraded to AEsc
+ // by esc())
+ if (astack(a->type))
+ a->type = a->slot->type;
+ assert(a->type != Alias::ABot);
+ return;
+ case RCon: {
+ Con* c = &fn->con[r.val];
+ if (c->type == Con::CAddr) {
+ a->type = Alias::ASym;
+ a->u.sym = c->sym;
+ } else {
+ a->type = Alias::ACon;
+ }
+ a->offset = c->bits.i;
+ a->offset_unknown = false;
+ a->slot = nullptr;
+ a->base = 0;
+ return;
+ }
+ default:
+ die("getalias: unreachable ref kind");
+ }
+}
+
+int alias(Ref p, int op, int sp, Ref q, int sq, int* delta, Fn* fn) {
+ Alias ap, aq;
+ getalias(&ap, p, fn);
+ getalias(&aq, q, fn);
+ ap.offset += op;
+
+ // sp, sq are guaranteed by caller to be <= 2^28, offset difference won't overflow int
+ *delta = static_cast<int>(ap.offset - aq.offset);
+ bool ovlap = ap.offset < aq.offset + sq && aq.offset < ap.offset + sp;
+ // When offset_unknown, overlap is untrustworthy, all "same base + overlap" judgements must
+ // degrade
+ bool ovlap_known = !ap.offset_unknown && !aq.offset_unknown;
+
+ bool p_stack = astack(ap.type);
+ bool q_stack = astack(aq.type);
+
+ if (p_stack && q_stack)
+ return (ap.base == aq.base && ovlap) ? MustAlias : NoAlias;
+
+ if (ap.type == Alias::ASym && aq.type == Alias::ASym) {
+ if (!symeq(ap.u.sym, aq.u.sym))
+ return NoAlias;
+ if (!ovlap_known)
+ return MayAlias;
+ return ovlap ? MustAlias : NoAlias;
+ }
+
+ bool both_con = (ap.type == Alias::ACon && aq.type == Alias::ACon);
+ bool same_base = (ap.type == aq.type && ap.base == aq.base);
+ if (both_con || same_base) {
+ assert(ap.type == Alias::ACon || ap.type == Alias::AUnk);
+ if (!ovlap_known)
+ return MayAlias;
+ return ovlap ? MustAlias : NoAlias;
+ }
+
+ if (ap.type == Alias::AUnk && aq.type != Alias::ALoc)
+ return MayAlias;
+ if (aq.type == Alias::AUnk && ap.type != Alias::ALoc)
+ return MayAlias;
+
+ return NoAlias;
+}
+
+int escapes(Ref r, Fn* fn) {
+ if (rtype(r) != RTmp)
+ return 1;
+ Alias* a = &fn->tmp[r.val].alias;
+ // astack and slot is still ALoc -> didn't escape; otherwise (not astack or slot is AEsc) it
+ // escapes
+ return !astack(a->type) || a->slot->type == Alias::AEsc;
+}
+
+namespace {
+
+void esc(Ref r, Fn* fn) {
+ assert(rtype(r) <= RType);
+ if (rtype(r) != RTmp)
+ return;
+ Alias* a = &fn->tmp[r.val].alias;
+ if (astack(a->type))
+ a->slot->type = Alias::AEsc;
+}
+
+void store(Ref r, int sz, Fn* fn) {
+ if (rtype(r) != RTmp)
+ return;
+ Alias* a = &fn->tmp[r.val].alias;
+ if (!a->slot)
+ return;
+ assert(astack(a->type));
+ int64_t off = a->offset;
+ bits m;
+ if (sz >= static_cast<int>(NBit) || off < 0 || off >= static_cast<int64_t>(NBit))
+ m = static_cast<bits>(-1);
+ else
+ m = (BIT(sz) - 1) << off;
+ a->slot->u.loc.m |= m;
+}
+
+bool getalias_safe(Alias* a, Ref r, Fn* fn) {
+ switch (rtype(r)) {
+ case RTmp:
+ *a = fn->tmp[r.val].alias;
+ if (a->type == Alias::ABot)
+ return false;
+ if (astack(a->type))
+ a->type = a->slot->type;
+ return true;
+ case RCon: {
+ Con* c = &fn->con[r.val];
+ if (c->type == Con::CAddr) {
+ a->type = Alias::ASym;
+ a->u.sym = c->sym;
+ } else {
+ a->type = Alias::ACon;
+ }
+ a->offset = c->bits.i;
+ a->offset_unknown = false;
+ a->slot = nullptr;
+ a->base = 0;
+ return true;
+ }
+ default:
+ die("getalias_safe: unreachable ref kind");
+ }
+}
+
+bool alias_eq(const Alias& a, const Alias& b) {
+ if (a.type != b.type)
+ return false;
+ if (a.offset != b.offset)
+ return false;
+ if (a.offset_unknown != b.offset_unknown)
+ return false;
+ if (a.slot != b.slot)
+ return false;
+ if (a.base != b.base)
+ return false;
+ if (a.type == Alias::ASym) {
+ if (a.u.sym.type != b.u.sym.type)
+ return false;
+ if (a.u.sym.id != b.u.sym.id)
+ return false;
+ } else if (astack(a.type)) {
+ if (a.u.loc.sz != b.u.loc.sz)
+ return false;
+ if (a.u.loc.m != b.u.loc.m)
+ return false;
+ }
+ return true;
+}
+
+bool alias_meet(Alias* a, const Alias& b) {
+ Alias old = *a;
+
+ if (a->type == Alias::ABot) {
+ *a = b;
+ } else if (b.type == Alias::ABot) {
+ // Unchanged
+ } else {
+ // Same type + same identity: offset compatible
+ bool same_id = false;
+ if (a->type == b.type) {
+ if (astack(a->type))
+ same_id = (a->slot == b.slot);
+ else if (a->type == Alias::ASym)
+ same_id = symeq(a->u.sym, b.u.sym);
+ else if (a->type == Alias::ACon)
+ same_id = true; // ACon has no base/sym, purely based on offset
+ else if (a->type == Alias::AUnk)
+ same_id = (a->base == b.base);
+ }
+ if (same_id) {
+ // ASym path legitimately accepts offset_unknown; ALoc/AEsc do not (store-mask
+ // assumes offset is known), degrade to AUnk on offset mismatch.
+ bool both_offset_known = !a->offset_unknown && !b.offset_unknown;
+ bool offset_match = both_offset_known && a->offset == b.offset;
+ if (astack(a->type) && !offset_match) {
+ a->type = Alias::AUnk;
+ a->slot = nullptr;
+ a->offset = 0;
+ a->offset_unknown = false;
+ // base fixed by caller
+ } else {
+ if (!offset_match)
+ a->offset_unknown = true;
+ }
+ } else {
+ // Type or identity mismatch -> AUnk
+ a->type = Alias::AUnk;
+ a->slot = nullptr;
+ a->offset = 0;
+ a->offset_unknown = false;
+ // base fixed by caller
+ }
+ }
+
+ return !alias_eq(*a, old);
+}
+
+bool compute_phi(Phi* p, Fn* fn) {
+ assert(rtype(p->to) == RTmp);
+ Alias* a = &fn->tmp[p->to.val].alias;
+ Alias old = *a;
+
+ Alias acc;
+ std::memset(&acc, 0, sizeof acc);
+ acc.type = Alias::ABot;
+
+ for (uint k = 0; k < p->narg; k++) {
+ Alias arg;
+ if (!getalias_safe(&arg, p->arg[k], fn))
+ continue; // This arg is still ABot, skip and wait for next round
+ alias_meet(&acc, arg);
+ }
+
+ if (acc.type == Alias::ABot) {
+ // All args still ABot: retain ABot, doesn't count as change
+ return false;
+ }
+
+ // If meet degrades to AUnk, set base to phi's own tmp id (uniqueness, avoids sharing base with
+ // other AUnks)
+ if (acc.type == Alias::AUnk)
+ acc.base = p->to.val;
+
+ if (!alias_eq(*a, acc)) {
+ *a = acc;
+ return true;
+ }
+ (void)old;
+ return false;
+}
+
+struct InsResult {
+ bool consumed_next;
+ bool changed;
+};
+
+InsResult process_ins(Ins* i, Ins* end, Fn* fn) {
+ InsResult res{false, false};
+ Alias* a = nullptr;
+ Alias old;
+ bool has_def = !req(i->to, Ref::none());
+
+ if (has_def) {
+ assert(rtype(i->to) == RTmp);
+ a = &fn->tmp[i->to.val].alias;
+ old = *a;
+
+ if (isalloc(i->op)) {
+ // alloc: install only on first time (ABot), afterwards type might upgrade to AEsc via
+ // esc, but slot pointer, loc.sz, loc.m must be preserved (loc.m accumulates with OR
+ // across rounds).
+ if (a->type == Alias::ABot) {
+ a->type = Alias::ALoc;
+ a->slot = a; // Self reference = this tmp is the root slot
+ a->u.loc.sz = -1; // Unknown size placeholder
+ a->u.loc.m = 0;
+ if (rtype(i->arg[0]) == RCon) {
+ Con* c = &fn->con[i->arg[0].val];
+ int64_t x = c->bits.i;
+ if (c->type == Con::CBits && 0 <= x && x <= static_cast<int64_t>(NBit))
+ a->u.loc.sz = static_cast<int>(x);
+ }
+ a->base = i->to.val;
+ a->offset = 0;
+ a->offset_unknown = false;
+ }
+ } else if (i->op == Ocopy || i->op == Oaddr) {
+ // Oaddr passes through alias: IRGen wraps `$sym` as `%t = addr $sym`, here
+ // we must propagate ASym identity from RCon CAddr to %t, otherwise alias() sees
+ // load/store addr as AUnk Tmp, losing "different globals don't alias".
+ Alias src;
+ if (getalias_safe(&src, i->arg[0], fn)) {
+ *a = src;
+ }
+ // src still ABot: don't touch *a, wait for next round
+ } else if (i->op == Oadd) {
+ Alias a0, a1;
+ bool ok0 = getalias_safe(&a0, i->arg[0], fn);
+ bool ok1 = getalias_safe(&a1, i->arg[1], fn);
+ if (ok0 && ok1) {
+ Alias newval;
+ std::memset(&newval, 0, sizeof newval);
+ newval.type = Alias::AUnk; // Default
+ newval.base = i->to.val;
+
+ if (a0.type == Alias::ACon) {
+ newval = a1;
+ newval.offset += a0.offset;
+ } else if (a1.type == Alias::ACon) {
+ newval = a0;
+ newval.offset += a1.offset;
+ } else if (a0.type == Alias::ASym) {
+ newval = a0;
+ newval.offset_unknown = true;
+ } else if (a1.type == Alias::ASym) {
+ newval = a1;
+ newval.offset_unknown = true;
+ }
+ // Both are neither ACon nor ASym -> keep default AUnk
+ *a = newval;
+ }
+ // Any operand still ABot: don't touch *a, wait for next round
+ } else {
+ // Other ops: def is always AUnk, one-time init
+ if (a->type == Alias::ABot) {
+ a->type = Alias::AUnk;
+ a->slot = nullptr;
+ a->base = i->to.val;
+ a->offset = 0;
+ a->offset_unknown = false;
+ }
+ }
+
+ if (!alias_eq(*a, old))
+ res.changed = true;
+ }
+
+ // 3. esc mark: when no def or def is already AUnk, arg is treated as escaping local
+ // abstraction.
+ // Skip on ABot -- def undetermined, decide next round.
+ // Multiple iterations: esc is monotonically accumulated (slot type one-way ALoc->AEsc),
+ // idempotent.
+ bool def_known_unk = has_def && a->type == Alias::AUnk;
+ if (!has_def || def_known_unk) {
+ if (i->op != Oblit0) {
+ if (!isload(i->op))
+ esc(i->arg[0], fn);
+ if (!isstore(i->op) && i->op != Oargc)
+ esc(i->arg[1], fn);
+ }
+ }
+
+ // 4. Memory write accounting: Oblit0/Oblit1 pair / normal store (multiple rounds OR accumulate
+ // idempotent)
+ if (i->op == Oblit0) {
+ Ins* i1 = i + 1;
+ assert(i1 < end && i1->op == Oblit1);
+ (void)end;
+ assert(rtype(i1->arg[0]) == RInt);
+ int sz = std::abs(rsval(i1->arg[0]));
+ store(i->arg[1], sz, fn);
+ res.consumed_next = true;
+ }
+ if (isstore(i->op))
+ store(i->arg[1], storesz(i), fn);
+
+ return res;
+}
+
+void escape_phi_args(Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ for (Phi* p = b->phi; p; p = p->link)
+ for (uint n = 0; n < p->narg; n++)
+ esc(p->arg[n], fn);
+}
+
+} // namespace
+
+void fillalias(Fn* fn) {
+ for (int t = 0; t < fn->ntmp; t++) {
+ std::memset(&fn->tmp[t].alias, 0, sizeof(Alias));
+ fn->tmp[t].alias.type = Alias::ABot;
+ }
+
+ int iter = 0;
+ bool changed = true;
+ for (; iter < 16 && changed; ++iter) {
+ changed = false;
+ for (uint n = 0; n < fn->nblk; ++n) {
+ Blk* b = fn->rpo[n];
+ for (Phi* p = b->phi; p; p = p->link)
+ if (compute_phi(p, fn))
+ changed = true;
+ for (Ins* i = b->ins; i < &b->ins[b->nins];) {
+ InsResult r = process_ins(i, &b->ins[b->nins], fn);
+ if (r.changed)
+ changed = true;
+ i += r.consumed_next ? 2 : 1;
+ }
+ // Block tail jump arg considered escaped (Jretc exception: ret-by-copy pointer doesn't
+ // escape). Multiple rounds esc idempotent.
+ if (b->jmp.type != Jretc)
+ esc(b->jmp.arg, fn);
+ }
+ }
+
+ for (int t = 0; t < fn->ntmp; t++) {
+ Alias* a = &fn->tmp[t].alias;
+ if (a->type == Alias::ABot) {
+ a->type = Alias::AUnk;
+ a->base = t;
+ a->offset = 0;
+ a->offset_unknown = false;
+ a->slot = nullptr;
+ }
+ }
+
+ escape_phi_args(fn);
+}
+
+} // namespace rcc
diff --git a/src/mid/cfg.cc b/src/mid/cfg.cc
new file mode 100644
index 0000000..e03b1e1
--- /dev/null
+++ b/src/mid/cfg.cc
@@ -0,0 +1,582 @@
+/* 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 cfg.cc
+/// @brief CFG structure analysis and simplification passes.
+///
+/// See docs/cfg.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <utility>
+
+namespace rcc {
+
+// Block construction
+
+Blk* newblk(Ctx* ctx) {
+ auto* b = static_cast<Blk*>(alloc(ctx, sizeof(Blk)));
+ memset(b, 0, sizeof *b);
+ b->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn));
+ b->pred = static_cast<Blk**>(vnew(ctx, 0, sizeof(Blk*), PFn));
+ return b;
+}
+
+// Reverse Post-Order (RPO) numbering + dead block elimination
+
+namespace {
+
+void post_visit(Blk* b, uint* counter) {
+ if (!b || b->visit)
+ return;
+ b->visit = 1;
+ // Heuristic: recurse outer branch first.
+ Blk* a = b->s1;
+ Blk* c = b->s2;
+ if (a && c && a->loop > c->loop)
+ std::swap(a, c);
+ post_visit(a, counter);
+ post_visit(c, counter);
+ b->id = (*counter)++;
+}
+
+void fillrpo(Ctx* ctx, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = 0;
+
+ uint cnt = 0;
+ post_visit(fn->start, &cnt);
+ fn->nblk = cnt;
+
+ vensure(ctx, &fn->rpo, fn->nblk, sizeof(Blk*), PFn);
+
+ // Second pass: remove unvisited, invert post-order.
+ Blk** slot = &fn->start;
+ while (Blk* b = *slot) {
+ if (!b->visit) {
+ b->id = ~0u;
+ *slot = b->link;
+ } else {
+ b->id = fn->nblk - 1 - b->id;
+ fn->rpo[b->id] = b;
+ slot = &b->link;
+ }
+ }
+}
+
+} // namespace
+
+// Predecessor sets
+
+void fillpreds(Ctx* ctx, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ b->npred = 0;
+
+ for (Blk* src = fn->start; src; src = src->link) {
+ if (src->s1)
+ src->s1->npred++;
+ if (src->s2 && src->s2 != src->s1)
+ src->s2->npred++;
+ }
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (b->npred)
+ vensure(ctx, &b->pred, b->npred, sizeof(Blk*), PFn);
+ b->npred = 0;
+ }
+
+ for (Blk* src = fn->start; src; src = src->link) {
+ if (src->s1)
+ src->s1->pred[src->s1->npred++] = src;
+ if (src->s2 && src->s2 != src->s1)
+ src->s2->pred[src->s2->npred++] = src;
+ }
+}
+
+// phi cleanup.
+
+namespace {
+
+void fixphis(Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Phi* p = b->phi; p; p = p->link) {
+ uint keep = 0;
+ for (uint i = 0; i < p->narg; i++) {
+ Blk* src = p->blk[i];
+ if (src->id == ~0u)
+ continue;
+ if (src->s1 != b && src->s2 != b)
+ continue;
+ if (keep != i) {
+ p->blk[keep] = src;
+ p->arg[keep] = p->arg[i];
+ }
+ keep++;
+ }
+ assert(keep > 0 && "phi has no live incoming edge");
+ p->narg = keep;
+ }
+ }
+}
+
+} // namespace
+
+void fillcfg(Ctx* ctx, Fn* fn) {
+ fillrpo(ctx, fn);
+ fillpreds(ctx, fn);
+ fixphis(fn);
+}
+
+// Dominator Tree (Cooper-Harvey-Kennedy)
+
+namespace {
+
+/// Walk up idom chain to find the lowest common ancestor of x and y in dom-tree.
+/// Invariant: Larger RPO id means a deeper node in the tree, move the deeper one up by one step
+/// each time.
+Blk* dom_meet(Blk* x, Blk* y) {
+ if (!x)
+ return y;
+ while (x != y) {
+ if (x->id < y->id)
+ std::swap(x, y);
+ x = x->idom;
+ assert(x);
+ }
+ return x;
+}
+
+} // namespace
+
+void filldom(Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ b->idom = nullptr;
+ b->dom = nullptr;
+ b->dlink = nullptr;
+ }
+
+ // Fixpoint iteration.
+ for (bool changed = true; changed;) {
+ changed = false;
+ for (uint i = 1; i < fn->nblk; i++) {
+ Blk* b = fn->rpo[i];
+ Blk* d = nullptr;
+ for (uint k = 0; k < b->npred; k++) {
+ Blk* p = b->pred[k];
+ if (p->idom || p == fn->start)
+ d = dom_meet(d, p);
+ }
+ if (d != b->idom) {
+ b->idom = d;
+ changed = true;
+ }
+ }
+ }
+
+ // Reverse chain: attach dom-children.
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (Blk* d = b->idom) {
+ assert(d != b);
+ b->dlink = d->dom;
+ d->dom = b;
+ }
+ }
+}
+
+int sdom(Blk* a, Blk* b) {
+ assert(a && b);
+ if (a == b)
+ return 0;
+ while (b->id > a->id)
+ b = b->idom;
+ return a == b;
+}
+
+int dom(Blk* a, Blk* b) {
+ return a == b || sdom(a, b);
+}
+
+// Dominance Frontier
+
+namespace {
+
+void push_fron(Ctx* ctx, Blk* host, Blk* node) {
+ for (uint i = 0; i < host->nfron; i++)
+ if (host->fron[i] == node)
+ return;
+ vensure(ctx, &host->fron, host->nfron + 1, sizeof(Blk*), PFn);
+ host->fron[host->nfron++] = node;
+}
+
+/// For edge (src -> dst): walk up idom chain from src, collect dst into the dominance frontier of
+/// all ancestors that do not strictly dominate dst.
+void contribute_fron(Ctx* ctx, Blk* src, Blk* dst) {
+ for (Blk* a = src; a && !sdom(a, dst); a = a->idom)
+ push_fron(ctx, a, dst);
+}
+
+} // namespace
+
+void fillfron(Ctx* ctx, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ b->nfron = 0;
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (b->s1)
+ contribute_fron(ctx, b, b->s1);
+ if (b->s2)
+ contribute_fron(ctx, b, b->s2);
+ }
+}
+
+// Natural loop identification / Loop weights
+
+namespace {
+
+void mark_loop_body(Blk* hd, Blk* node, void (*cb)(Blk*, Blk*)) {
+ if (node->id < hd->id || node->visit == hd->id)
+ return;
+ node->visit = hd->id;
+ cb(hd, node);
+ for (uint k = 0; k < node->npred; k++)
+ mark_loop_body(hd, node->pred[k], cb);
+}
+
+void loop_x10(Blk* hd, Blk* b) {
+ (void)hd;
+ b->loop *= 10;
+}
+
+} // namespace
+
+void loopiter(Fn* fn, void (*cb)(Blk*, Blk*)) {
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = ~0u;
+ for (uint i = 0; i < fn->nblk; i++) {
+ Blk* hd = fn->rpo[i];
+ for (uint k = 0; k < hd->npred; k++)
+ if (hd->pred[k]->id >= i)
+ mark_loop_body(hd, hd->pred[k], cb);
+ }
+}
+
+void fillloop(Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ b->loop = 1;
+ loopiter(fn, loop_x10);
+}
+
+// dom-tree depth + lowest common ancestor
+
+void filldepth(Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ b->depth = -1;
+ fn->start->depth = 0;
+
+ // Walk up idom chain.
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (b->depth != -1)
+ continue;
+ Blk* d = b->idom;
+ int steps = 1;
+ while (d->depth == -1) {
+ d = d->idom;
+ steps++;
+ }
+ steps += d->depth;
+ b->depth = steps;
+ for (Blk* a = b->idom; a->depth == -1; a = a->idom)
+ a->depth = --steps;
+ }
+}
+
+Blk* lca(Blk* a, Blk* b) {
+ if (!a)
+ return b;
+ if (!b)
+ return a;
+ while (a->depth > b->depth)
+ a = a->idom;
+ while (b->depth > a->depth)
+ b = b->idom;
+ while (a != b) {
+ a = a->idom;
+ b = b->idom;
+ }
+ return a;
+}
+
+// simpljmp: fold empty transit blocks.
+
+namespace {
+
+/// Union-find query with path compression: find root along uf[], redirecting each node on the chain
+/// to root.
+Blk* uf_find(Blk** uf, Blk* x) {
+ Blk* root = x;
+ while (uf[root->id])
+ root = uf[root->id];
+ while (uf[x->id] && uf[x->id] != root) {
+ Blk* nxt = uf[x->id];
+ uf[x->id] = root;
+ x = nxt;
+ }
+ return root;
+}
+
+} // namespace
+
+void simpljmp(Ctx* ctx, Fn* fn) {
+ Blk* exit_blk = newblk(ctx);
+ exit_blk->id = fn->nblk++;
+ exit_blk->jmp.type = Jret0;
+ exit_blk->name = strf(ctx, PFn, "ret");
+
+ Blk** uf = static_cast<Blk**>(emalloc(fn->nblk * sizeof(Blk*)));
+ memset(uf, 0, fn->nblk * sizeof(Blk*));
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ assert(!b->phi && "simpljmp requires phi-free CFG");
+ if (b->jmp.type == Jret0) {
+ b->jmp.type = Jjmp;
+ b->s1 = exit_blk;
+ }
+ if (b->nins == 0 && b->jmp.type == Jjmp) {
+ Blk* r = uf_find(uf, b->s1);
+ if (r != b)
+ uf[b->id] = r;
+ }
+ }
+
+ Blk** tail = &fn->start;
+ for (Blk* b; (b = *tail); tail = &b->link) {
+ if (b->s1)
+ b->s1 = uf_find(uf, b->s1);
+ if (b->s2)
+ b->s2 = uf_find(uf, b->s2);
+ if (b->s1 && b->s1 == b->s2) {
+ b->jmp.type = Jjmp;
+ b->s2 = nullptr;
+ }
+ }
+ *tail = exit_blk;
+
+ free(uf);
+}
+
+// Reachability / if-graph pattern recognition
+
+namespace {
+
+int dfs_can_reach(Blk* from, Blk* to) {
+ if (from == to)
+ return 1;
+ if (!from || from->visit)
+ return 0;
+ from->visit = 1;
+ if (dfs_can_reach(from->s1, to))
+ return 1;
+ if (dfs_can_reach(from->s2, to))
+ return 1;
+ return 0;
+}
+
+} // namespace
+
+int reaches(Fn* fn, Blk* from, Blk* to) {
+ assert(to);
+ int r = dfs_can_reach(from, to);
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = 0;
+ return r;
+}
+
+int reachesnotvia(Fn* fn, Blk* from, Blk* to, Blk* excl) {
+ excl->visit = 1;
+ return reaches(fn, from, to);
+}
+
+int ifgraph(Blk* head, Blk** pthen, Blk** pelse, Blk** pjoin) {
+ if (head->jmp.type != Jjnz)
+ return 0;
+
+ Blk* lo = head->s1;
+ Blk* hi = head->s2;
+ bool swapped = false;
+ if (lo->id > hi->id) {
+ std::swap(lo, hi);
+ swapped = true;
+ }
+ if (lo == hi)
+ return 0;
+ if (lo->jmp.type != Jjmp || lo->npred != 1)
+ return 0;
+
+ Blk** out_then = swapped ? pelse : pthen;
+ Blk** out_else = swapped ? pthen : pelse;
+
+ if (lo->s1 == hi) {
+ // if-then (no else branch: head directly serves as else)
+ if (hi->npred != 2)
+ return 0;
+ *out_then = lo;
+ *out_else = head;
+ *pjoin = hi;
+ return 1;
+ }
+
+ if (hi->jmp.type != Jjmp || hi->npred != 1)
+ return 0;
+ if (lo->s1 != hi->s1 || lo->s1->npred != 2)
+ return 0;
+
+ assert(lo->s1 != head);
+ *out_then = lo;
+ *out_else = hi;
+ *pjoin = lo->s1;
+ return 1;
+}
+
+// simplcfg
+
+namespace {
+
+struct JmpSnap {
+ short type;
+ Ref arg;
+ Blk* s1;
+ Blk* s2;
+};
+
+bool snap_eq(const JmpSnap& a, const JmpSnap& b) {
+ return a.type == b.type && req(a.arg, b.arg) && a.s1 == b.s1 && a.s2 == b.s2;
+}
+
+bool target_phi_free(const JmpSnap& j) {
+ if (j.s1 && j.s1->phi)
+ return false;
+ if (j.s2 && j.s2->phi)
+ return false;
+ return true;
+}
+
+bool block_is_empty(Blk* b) {
+ if (b->phi)
+ return false;
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (i->op != Onop && i->op != Odbgloc)
+ return false;
+ return true;
+}
+
+void phi_to_copies(Ctx* ctx, Blk* b) {
+ assert(b->npred == 1);
+ Blk* p = b->pred[0];
+ Ins copy{};
+ copy.op = Ocopy;
+ for (Phi* phi = b->phi; phi; phi = phi->link) {
+ copy.cls = static_cast<uint>(phi->cls);
+ copy.to = phi->to;
+ copy.arg[0] = phiarg(phi, p);
+ copy.arg[1] = Ref::none();
+ addins(ctx, &p->ins, &p->nins, &copy);
+ }
+ b->phi = nullptr;
+}
+
+} // namespace
+
+void simplcfg(Ctx* ctx, Fn* fn) {
+ if (debug['C']) {
+ fprintf(ctx->dbg_buf, "\n> Before CFG simplification:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+
+ for (Blk* b = fn->start; b; b = b->link)
+ if (b->npred == 1)
+ phi_to_copies(ctx, b);
+
+ auto* snap = static_cast<JmpSnap*>(emalloc(fn->nblk * sizeof(JmpSnap)));
+ auto* empty = static_cast<bool*>(emalloc(fn->nblk * sizeof(bool)));
+ for (Blk* b = fn->start; b; b = b->link) {
+ snap[b->id] = {b->jmp.type, b->jmp.arg, b->s1, b->s2};
+ empty[b->id] = block_is_empty(b);
+ }
+
+ bool dirty;
+ do {
+ dirty = false;
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (b->id == ~0u)
+ continue;
+ JmpSnap* j = &snap[b->id];
+
+ if (j->type == Jjmp && j->s1->npred == 1) {
+ Blk* s = j->s1;
+ assert(!s->phi);
+ addbins(ctx, &b->ins, &b->nins, s);
+ empty[b->id] = empty[b->id] && empty[s->id];
+
+ JmpSnap* sj = &snap[s->id];
+ Blk* succs[2] = {sj->s1, sj->s2};
+ for (Blk* sb : succs) {
+ if (!sb)
+ continue;
+ for (Phi* p = sb->phi; p; p = p->link) {
+ uint n = phiargn(p, s);
+ if (n != ~0u)
+ p->blk[n] = b;
+ }
+ }
+
+ s->id = ~0u;
+ *j = *sj;
+ dirty = true;
+ } else if (j->type == Jjnz && empty[j->s1->id] && empty[j->s2->id] &&
+ snap_eq(snap[j->s1->id], snap[j->s2->id]) &&
+ target_phi_free(snap[j->s1->id])) {
+ *j = snap[j->s1->id];
+ dirty = true;
+ }
+ }
+ } while (dirty);
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (b->id == ~0u)
+ continue;
+ JmpSnap* j = &snap[b->id];
+ b->jmp.type = j->type;
+ b->jmp.arg = j->arg;
+ b->s1 = j->s1;
+ b->s2 = j->s2;
+ assert(!j->s1 || j->s1->id != ~0u);
+ assert(!j->s2 || j->s2->id != ~0u);
+ }
+
+ fillcfg(ctx, fn);
+
+ free(empty);
+ free(snap);
+
+ if (debug['C']) {
+ fprintf(ctx->dbg_buf, "\n> After CFG simplification:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/copy.cc b/src/mid/copy.cc
new file mode 100644
index 0000000..8a35498
--- /dev/null
+++ b/src/mid/copy.cc
@@ -0,0 +1,360 @@
+/* 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 copy.cc
+/// @brief copy equivalence check + narrow parameter sinking - for GVN, no independent driver.
+///
+/// See docs/copy.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <algorithm>
+
+namespace rcc {
+
+namespace {
+
+struct Ext {
+ char zext;
+ char nopw;
+ char usew;
+};
+
+bool decode_ext(int op, Ext* e) {
+ switch (op) {
+ case Oextsb:
+ *e = Ext{0, 7, 8};
+ return true;
+ case Oextub:
+ *e = Ext{1, 8, 8};
+ return true;
+ case Oextsh:
+ *e = Ext{0, 15, 16};
+ return true;
+ case Oextuh:
+ *e = Ext{1, 16, 16};
+ return true;
+ case Oextsw:
+ *e = Ext{0, 31, 32};
+ return true;
+ case Oextuw:
+ *e = Ext{1, 32, 32};
+ return true;
+ }
+ return false;
+}
+
+int bitwidth(uint64_t v) {
+ return v ? 64 - __builtin_clzll(v) : 0;
+}
+
+template <typename Walk>
+int walk_phi_memo(Fn* fn, Ref r, int w, Walk walk) {
+ int ret = walk(fn, r, w);
+ for (Blk* b = fn->start; b; b = b->link)
+ for (Phi* p = b->phi; p; p = p->link)
+ p->visit = 0;
+ return ret;
+}
+
+int uwl(Fn* fn, Ref r, int w);
+
+bool phi_use_ok(Fn* fn, Phi* p, int w) {
+ if (p->visit || req(p->to, Ref::none()))
+ return true;
+ p->visit = 1;
+ return uwl(fn, p->to, w) != 0;
+}
+
+bool ins_use_ok(Fn* fn, Ins* i, Ref r, int w) {
+ Ext e;
+ if (i->op == Ocopy && uwl(fn, i->to, w))
+ return true;
+ if (decode_ext(i->op, &e)) {
+ if (e.usew <= w)
+ return true;
+ return uwl(fn, i->to, w) != 0;
+ }
+ if (i->op == Oand) {
+ Ref rc;
+ if (req(r, i->arg[0])) {
+ rc = i->arg[1];
+ } else {
+ assert(req(r, i->arg[1]));
+ rc = i->arg[0];
+ }
+ int64_t v;
+ return isconbits(fn, rc, &v) && bitwidth(static_cast<uint64_t>(v)) <= w;
+ }
+ return false;
+}
+
+int uwl(Fn* fn, Ref r, int w) {
+ assert(rtype(r) == RTmp);
+ Tmp* t = &fn->tmp[r.val];
+ for (Use* u = &t->use[0]; u < &t->use[t->nuse]; ++u) {
+ bool ok;
+ switch (u->type) {
+ case Use::UPhi:
+ ok = phi_use_ok(fn, u->u.phi, w);
+ break;
+ case Use::UIns:
+ assert(u->u.ins);
+ ok = ins_use_ok(fn, u->u.ins, r, w);
+ break;
+ default:
+ ok = false;
+ break;
+ }
+ if (!ok)
+ return 0;
+ }
+ return 1;
+}
+
+/* no more than w bits are used */
+int usewidthle(Fn* fn, Ref r, int w) {
+ return walk_phi_memo(fn, r, w, uwl);
+}
+
+int dwl(Fn* fn, Ref r, int w) {
+ int64_t v;
+ if (isconbits(fn, r, &v) && bitwidth(static_cast<uint64_t>(v)) <= w)
+ return 1;
+ if (w <= 0)
+ return 0;
+ if (rtype(r) != RTmp)
+ return 0;
+ Tmp* t = &fn->tmp[r.val];
+ if (t->cls != Kw)
+ return 0;
+
+ if (!t->def) {
+ Phi* p = fn->rpo[t->bid]->phi;
+ for (; p; p = p->link)
+ if (req(p->to, r))
+ break;
+ assert(p);
+ if (p->visit && p->visit <= w)
+ return 1;
+ p->visit = w;
+ for (uint n = 0; n < p->narg; ++n)
+ if (!dwl(fn, p->arg[n], w))
+ return 0;
+ return 1;
+ }
+
+ Ins* i = t->def;
+ if (i->op == Ocopy)
+ return dwl(fn, i->arg[0], w);
+
+ if (i->op == Oshr || i->op == Osar) {
+ if (isconbits(fn, i->arg[1], &v) && 0 < v && v <= 32) {
+ if (i->op == Oshr && w + v >= 32)
+ return 1;
+ if (w < 32) {
+ int cap = (i->op == Osar) ? 31 : 32;
+ w = std::min(cap, static_cast<int>(w + v));
+ }
+ }
+ return dwl(fn, i->arg[0], w);
+ }
+
+ int x;
+ if (iscmp(i->op, &x, &x))
+ return w >= 1;
+
+ if (i->op == Oand)
+ return (dwl(fn, i->arg[0], w) || dwl(fn, i->arg[1], w)) ? 1 : 0;
+
+ if (i->op == Oor || i->op == Oxor)
+ return (dwl(fn, i->arg[0], w) && dwl(fn, i->arg[1], w)) ? 1 : 0;
+
+ Ext e;
+ if (decode_ext(i->op, &e)) {
+ if (e.zext && e.usew <= w)
+ return 1;
+ w = std::min(w, static_cast<int>(e.nopw));
+ return dwl(fn, i->arg[0], w);
+ }
+
+ return 0;
+}
+
+/* is the ref narrower than w bits */
+int defwidthle(Fn* fn, Ref r, int w) {
+ return walk_phi_memo(fn, r, w, dwl);
+}
+
+int isw1(Fn* fn, Ref r) {
+ return defwidthle(fn, r, 1);
+}
+
+bool phieq(Phi* pa, Phi* pb) {
+ assert(pa->narg == pb->narg);
+ for (uint n = 0; n < pa->narg; ++n)
+ if (!req(pa->arg[n], phiarg(pb, pa->blk[n])))
+ return false;
+ return true;
+}
+
+} // namespace
+
+Ref copyref(Ctx* ctx, Fn* fn, Blk* b, Ins* i) {
+ static const bits extcpy[] = {
+ /*WFull*/ 0,
+ /*Wsb*/ BIT(Tmp::Wsb) | BIT(Tmp::Wsh) | BIT(Tmp::Wsw),
+ /*Wub*/ BIT(Tmp::Wub) | BIT(Tmp::Wuh) | BIT(Tmp::Wuw),
+ /*Wsh*/ BIT(Tmp::Wsh) | BIT(Tmp::Wsw),
+ /*Wuh*/ BIT(Tmp::Wuh) | BIT(Tmp::Wuw),
+ /*Wsw*/ BIT(Tmp::Wsw),
+ /*Wuw*/ BIT(Tmp::Wuw),
+ };
+ MAKESURE(extcpy_covers_all_widths, sizeof(extcpy) / sizeof(extcpy[0]) == Tmp::Wuw + 1);
+
+ if (i->op == Ocopy)
+ return i->arg[0];
+
+ const Op& od = optab[i->op];
+
+ if (od.hasid && KBASE(i->cls) == 0 && req(i->arg[1], ctx->gvn_con01[od.idval]) &&
+ (!od.cmpeqwl || isw1(fn, i->arg[0])))
+ return i->arg[0];
+
+ if (od.idemp && req(i->arg[0], i->arg[1]))
+ return i->arg[0];
+
+ if ((od.cmpeqwl || od.cmplgtewl) && req(i->arg[0], i->arg[1]))
+ return ctx->gvn_con01[od.eqval];
+
+ int z;
+ if (od.cmpeqwl && req(i->arg[1], Ref::con_z()) && zeroval(fn, b, i->arg[0], argcls(i, 0), &z))
+ return ctx->gvn_con01[od.eqval ^ z ^ 1];
+
+ int64_t v;
+ if (i->op == Oand && isconbits(fn, i->arg[1], &v) && v > 0 && ((v + 1) & v) == 0 &&
+ defwidthle(fn, i->arg[0], bitwidth(static_cast<uint64_t>(v))))
+ return i->arg[0];
+
+ if (i->cls == Kw && (i->op == Oextsw || i->op == Oextuw))
+ return i->arg[0];
+
+ Ext e;
+ if (decode_ext(i->op, &e) && rtype(i->arg[0]) == RTmp) {
+ Tmp* t = &fn->tmp[i->arg[0].val];
+ assert(KBASE(t->cls) == 0);
+
+ if (KWIDE(i->cls) > KWIDE(t->cls))
+ return Ref::none();
+
+ int w = Tmp::Wsb + (static_cast<int>(i->op) - Oextsb);
+ if (BIT(w) & extcpy[t->width])
+ return i->arg[0];
+
+ if ((!t->def || !ispar(t->def->op)) && usewidthle(fn, i->to, e.usew))
+ return i->arg[0];
+
+ if (defwidthle(fn, i->arg[0], e.nopw))
+ return i->arg[0];
+ }
+
+ return Ref::none();
+}
+
+Ref phicopyref(Ctx* ctx, Fn* fn, Blk* b, Phi* p) {
+ Ref r = Ref::none();
+ uint n = 0;
+ for (; n < p->narg; ++n)
+ if (!req(p->arg[n], p->to)) {
+ if (req(r, Ref::none()))
+ r = p->arg[n];
+ else if (!req(p->arg[n], r))
+ break;
+ }
+ if (n == p->narg)
+ return r;
+
+ for (Phi* p1 = b->phi; p1 != p; p1 = p1->link) {
+ assert(p1);
+ if (phieq(p1, p))
+ return p1->to;
+ }
+
+ Blk* d = b->idom;
+ if (p->narg != 2 || d->jmp.type != Jjnz || !isw1(fn, d->jmp.arg))
+ return Ref::none();
+
+ Blk* s[2] = {nullptr, nullptr};
+ for (uint k = 0; k < 2; ++k)
+ for (uint c = 0; c < 2; ++c)
+ if (req(p->arg[k], ctx->gvn_con01[c]))
+ s[c] = p->blk[k];
+
+ if (d->s1 == s[1] && d->s2 == s[0] && d->s1->jmp.type == Jjmp && d->s2->jmp.type == Jjmp)
+ return d->jmp.arg;
+
+ return Ref::none();
+}
+
+void narrowpars(Ctx* ctx, Fn* fn) {
+ bool has_loop = false;
+ for (Blk* b = fn->start; b; b = b->link)
+ if (b->loop > 1) {
+ has_loop = true;
+ break;
+ }
+ if (!has_loop)
+ return;
+
+ Blk* b = fn->start;
+
+ uint npar = 0;
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) {
+ if (!ispar(i->op))
+ break;
+ ++npar;
+ }
+ if (npar == 0)
+ return;
+
+ uint nins = b->nins + npar;
+ Ins* ins = static_cast<Ins*>(vnew(ctx, nins, sizeof(Ins), PFn));
+ icpy(ins, b->ins, npar);
+ icpy(ins + 2 * npar, b->ins + npar, b->nins - npar);
+ b->ins = ins;
+ b->nins = nins;
+
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) {
+ if (!ispar(i->op))
+ break;
+ Ins extins = {};
+ extins.op = Onop;
+ if (i->cls == Kw && usewidthle(fn, i->to, 16)) {
+ extins.op = usewidthle(fn, i->to, 8) ? Oextub : Oextuh;
+ Ref r = newtmp(ctx, "vw", i->cls, fn);
+ extins.cls = i->cls;
+ extins.to = i->to;
+ extins.arg[0] = r;
+ i->to = r;
+ }
+ *(i + npar) = extins;
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/dce.cc b/src/mid/dce.cc
new file mode 100644
index 0000000..6ee631a
--- /dev/null
+++ b/src/mid/dce.cc
@@ -0,0 +1,149 @@
+/* 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 dce.cc
+/// @brief Dead Code Elimination: mark-and-sweep starting from side-effect / control flow roots.
+///
+/// See docs/dce.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+bool is_root(uint op) {
+ if (isstore(op))
+ return true;
+ if (isalloc(op))
+ return true;
+ if (ispar(op))
+ return true;
+ if (isarg(op))
+ return true;
+ if (op == Ocall)
+ return true;
+ if (op == Oblit0 || op == Oblit1)
+ return true;
+ if (op == Ovastart || op == Ovaarg)
+ return true;
+ if (op == Odbgloc)
+ return true;
+
+ if (op == Osel0 || op == Osel1)
+ return true;
+ return false;
+}
+
+} // namespace
+
+void dce(Ctx*, Fn* fn) {
+ int ntmp = fn->ntmp;
+ std::vector<Phi*> phi_def(static_cast<size_t>(ntmp), nullptr);
+ std::vector<Ins*> ins_def(static_cast<size_t>(ntmp), nullptr);
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Phi* p = b->phi; p; p = p->link)
+ if (rtype(p->to) == RTmp && p->to.val < static_cast<uint>(ntmp))
+ phi_def[p->to.val] = p;
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (rtype(i->to) == RTmp && i->to.val < static_cast<uint>(ntmp))
+ ins_def[i->to.val] = i;
+ }
+ }
+
+ std::vector<char> alive(static_cast<size_t>(ntmp), 0);
+ std::vector<uint> wl;
+ auto mark = [&](Ref r) {
+ if (rtype(r) == RTmp && r.val < static_cast<uint>(ntmp) && !alive[r.val]) {
+ alive[r.val] = 1;
+ wl.push_back(r.val);
+ }
+ };
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (is_root(i->op)) {
+ mark(i->arg[0]);
+ mark(i->arg[1]);
+ }
+ }
+ mark(b->jmp.arg);
+ }
+
+ while (!wl.empty()) {
+ uint t = wl.back();
+ wl.pop_back();
+ if (Ins* d = ins_def[t]) {
+ mark(d->arg[0]);
+ mark(d->arg[1]);
+ continue;
+ }
+ if (Phi* p = phi_def[t]) {
+ for (uint a = 0; a < p->narg; a++)
+ mark(p->arg[a]);
+ }
+ }
+
+ int killed_ins = 0, killed_phi = 0;
+ for (Blk* b = fn->start; b; b = b->link) {
+ Phi** pp = &b->phi;
+ while (*pp) {
+ Phi* p = *pp;
+ bool dead =
+ (rtype(p->to) == RTmp && p->to.val < static_cast<uint>(ntmp) && !alive[p->to.val]);
+ if (dead) {
+ *pp = p->link;
+ killed_phi++;
+ } else {
+ pp = &p->link;
+ }
+ }
+
+ uint w = 0;
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (i->op == Onop) {
+ killed_ins++;
+ continue;
+ }
+ bool root = is_root(i->op);
+ bool has_to = (rtype(i->to) == RTmp && i->to.val < static_cast<uint>(ntmp));
+ if (!root && has_to && !alive[i->to.val]) {
+ killed_ins++;
+ continue;
+ }
+ if (w != k)
+ b->ins[w] = *i;
+ w++;
+ }
+ b->nins = w;
+ }
+
+ if (debug['D']) {
+ fprintf(stderr, "\n> DCE for %s: killed %d ins, %d phi\n", fn->name, killed_ins,
+ killed_phi);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/dead_loop.cc b/src/mid/dead_loop.cc
new file mode 100644
index 0000000..26b915a
--- /dev/null
+++ b/src/mid/dead_loop.cc
@@ -0,0 +1,227 @@
+/* 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 dead_loop.cc
+/// @brief Dead Loop Elimination: removal of entire loops that have no side-effects, are provably
+/// terminating, and have no live-outs.
+///
+/// See docs/dead_loop.md for architectural notes.
+
+#include "rcc/all.h"
+#include "rcc/loopinfo.h"
+
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+bool has_side_effect_op(uint op) {
+ if (isstore(op))
+ return true;
+ if (isalloc(op))
+ return true;
+ if (ispar(op))
+ return true;
+ if (isarg(op))
+ return true;
+ if (op == Ocall)
+ return true;
+ if (op == Oblit0 || op == Oblit1)
+ return true;
+ if (op == Ovastart || op == Ovaarg)
+ return true;
+ if (op == Odbgloc)
+ return true;
+ if (op == Osel0 || op == Osel1)
+ return true;
+ return false;
+}
+
+bool defined_outside(Fn* fn, const Loop& L, Ref r) {
+ if (rtype(r) == RCon)
+ return true;
+ if (rtype(r) != RTmp)
+ return false;
+ if (r.val >= static_cast<uint>(fn->ntmp))
+ return false;
+ uint bid = fn->tmp[r.val].bid;
+ return L.body_bid.find(bid) == L.body_bid.end();
+}
+
+Ins* find_cmp_in(Blk* b, Ref cond) {
+ for (uint i = 0; i < b->nins; i++)
+ if (req(b->ins[i].to, cond))
+ return &b->ins[i];
+ return nullptr;
+}
+
+int find_biv_idx(const std::vector<Biv>& bivs, Ref r) {
+ for (size_t k = 0; k < bivs.size(); k++)
+ if (req(bivs[k].to, r))
+ return static_cast<int>(k);
+ return -1;
+}
+
+void collect_body_defs(const Loop& L, std::vector<Ref>& defs) {
+ for (Blk* b : L.body) {
+ for (Phi* p = b->phi; p; p = p->link)
+ if (rtype(p->to) == RTmp)
+ defs.push_back(p->to);
+ for (uint k = 0; k < b->nins; k++)
+ if (rtype(b->ins[k].to) == RTmp)
+ defs.push_back(b->ins[k].to);
+ }
+}
+
+bool all_uses_inside(Fn* fn, const Loop& L, Ref tmp_ref) {
+ if (rtype(tmp_ref) != RTmp)
+ return true;
+ if (tmp_ref.val >= static_cast<uint>(fn->ntmp))
+ return true;
+ Tmp* t = &fn->tmp[tmp_ref.val];
+ for (uint u = 0; u < t->nuse; u++) {
+ const Use& uu = t->use[u];
+ if (L.body_bid.find(uu.bid) == L.body_bid.end())
+ return false;
+ }
+ return true;
+}
+
+bool delete_loop_if_dead(Fn* fn, const Loop& L) {
+ // Topology: unique exit_block + unique exiting_block
+ Blk* exit_block = nullptr;
+ Blk* exiting_block = nullptr;
+ for (Blk* b : L.body) {
+ Blk* succs[2] = {b->s1, b->s2};
+ for (Blk* s : succs) {
+ if (!s)
+ continue;
+ if (L.body_bid.find(s->id) != L.body_bid.end())
+ continue; // Internal edge
+ // Exit loop edge
+ if (!exit_block) {
+ exit_block = s;
+ exiting_block = b;
+ } else if (exit_block != s || exiting_block != b) {
+ return false; // Multiple exits or multiple exiting blocks
+ }
+ }
+ }
+ if (!exit_block)
+ return false; // No exit -- infinite loop
+
+ // Termination: at least one constant BIV + header Jjnz cmp(BIV, invariant)
+ std::vector<Biv> bivs;
+ detect_bivs(fn, L, bivs);
+ if (bivs.empty())
+ return false;
+ if (L.header->jmp.type != Jjnz)
+ return false;
+ Ref cond = L.header->jmp.arg;
+ if (rtype(cond) != RTmp)
+ return false;
+ Ins* cmp = find_cmp_in(L.header, cond);
+ if (!cmp)
+ return false;
+ int bi = find_biv_idx(bivs, cmp->arg[0]);
+ Ref other = cmp->arg[1];
+ if (bi < 0) {
+ bi = find_biv_idx(bivs, cmp->arg[1]);
+ other = cmp->arg[0];
+ }
+ if (bi < 0)
+ return false;
+ if (!defined_outside(fn, L, other))
+ return false;
+ if (exiting_block != L.header)
+ return false;
+
+ // Side-effect scan
+ for (Blk* b : L.body) {
+ for (uint k = 0; k < b->nins; k++) {
+ if (has_side_effect_op(b->ins[k].op))
+ return false;
+ }
+ }
+
+ // Live-out scan
+ for (Phi* p = exit_block->phi; p; p = p->link) {
+ for (uint k = 0; k < p->narg; k++) {
+ if (p->blk[k] != exiting_block)
+ continue;
+ if (!defined_outside(fn, L, p->arg[k]))
+ return false;
+ }
+ }
+ std::vector<Ref> defs;
+ collect_body_defs(L, defs);
+ for (Ref r : defs) {
+ if (!all_uses_inside(fn, L, r))
+ return false;
+ }
+
+ for (Phi* p = exit_block->phi; p; p = p->link) {
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == L.preheader)
+ return false;
+ }
+
+ // Rewrite
+ // preheader's jmp outgoing edge: the one pointing to header is changed to point to exit_block;
+ if (L.preheader->s1 == L.header)
+ L.preheader->s1 = exit_block;
+ if (L.preheader->s2 == L.header)
+ L.preheader->s2 = exit_block;
+ for (Phi* p = exit_block->phi; p; p = p->link) {
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == exiting_block)
+ p->blk[k] = L.preheader;
+ }
+
+ return true;
+}
+
+} // namespace
+
+void dead_loop(Ctx* ctx, Fn* fn) {
+ fillpreds(ctx, fn);
+ fillcfg(ctx, fn);
+ filluse(ctx, fn);
+
+ std::vector<Loop> loops;
+ find_loops(fn, loops);
+ if (loops.empty())
+ return;
+
+ int deleted = 0;
+ for (const Loop& L : loops) {
+ if (delete_loop_if_dead(fn, L))
+ deleted++;
+ }
+
+ if (debug['L']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "\n> dead_loop %s: deleted %d of %zu loop(s)\n", fn->name, deleted,
+ loops.size());
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/divopt.cc b/src/mid/divopt.cc
new file mode 100644
index 0000000..6e9269f
--- /dev/null
+++ b/src/mid/divopt.cc
@@ -0,0 +1,189 @@
+/* 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 divopt.cc
+/// @brief Constant integer division/modulo strength reduction: rewrite `x / c`, `x % c`
+/// into mul + shift + sign correction sequence when c is a compile-time constant.
+///
+/// See docs/divopt.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cstdint>
+
+namespace rcc {
+
+namespace {
+
+struct Magic {
+ int32_t M;
+ int s;
+ bool need_add;
+};
+
+Magic compute_magic_s32(uint32_t ad) {
+ const uint32_t t = 0x80000000u; // 2^31
+ uint32_t anc = t - 1 - t % ad;
+ uint32_t p = 31;
+ uint32_t q1 = t / anc, r1 = t - q1 * anc;
+ uint32_t q2 = t / ad, r2 = t - q2 * ad;
+ uint32_t delta;
+ do {
+ ++p;
+ q1 *= 2;
+ r1 *= 2;
+ if (r1 >= anc) {
+ ++q1;
+ r1 -= anc;
+ }
+ q2 *= 2;
+ r2 *= 2;
+ if (r2 >= ad) {
+ ++q2;
+ r2 -= ad;
+ }
+ delta = ad - r2;
+ } while (q1 < delta || (q1 == delta && r1 == 0));
+
+ Magic m;
+ uint32_t M_u = q2 + 1;
+ m.M = static_cast<int32_t>(M_u);
+ m.s = static_cast<int>(p - 32);
+ m.need_add = (M_u >= 0x80000000u);
+ return m;
+}
+
+void emit_pow2_div_into(Ctx* ctx, Fn* fn, Ref dst, Ref x, int k) {
+ Ref sign = newtmp(ctx, "divopt", Kw, fn);
+ Ref mask = newtmp(ctx, "divopt", Kw, fn);
+ Ref tmp = newtmp(ctx, "divopt", Kw, fn);
+
+ emit(ctx, Osar, Kw, dst, tmp, getcon(ctx, k, fn));
+ emit(ctx, Oadd, Kw, tmp, x, mask);
+ emit(ctx, Oshr, Kw, mask, sign, getcon(ctx, 32 - k, fn));
+ emit(ctx, Osar, Kw, sign, x, getcon(ctx, 31, fn));
+}
+
+void emit_magic_div_into(Ctx* ctx, Fn* fn, Ref dst, Ref x, int32_t d_abs, bool d_neg) {
+ Magic m = compute_magic_s32(static_cast<uint32_t>(d_abs));
+ int64_t M64 = static_cast<int64_t>(m.M); // Sign extend i32 to i64
+
+ Ref xl = newtmp(ctx, "divopt", Kl, fn);
+ Ref prod = newtmp(ctx, "divopt", Kl, fn);
+ Ref hi = newtmp(ctx, "divopt", Kl, fn);
+ Ref hi2 = m.need_add ? newtmp(ctx, "divopt", Kl, fn) : hi;
+ Ref q1 = (m.s > 0) ? newtmp(ctx, "divopt", Kl, fn) : hi2;
+ Ref sg = newtmp(ctx, "divopt", Kl, fn);
+ Ref qpre = d_neg ? newtmp(ctx, "divopt", Kw, fn) : dst;
+
+ // Reverse emit:
+ if (d_neg)
+ emit(ctx, Oneg, Kw, dst, qpre, Ref::none());
+ emit(ctx, Osub, Kw, qpre, q1, sg);
+ emit(ctx, Osar, Kl, sg, xl, getcon(ctx, 63, fn));
+ if (m.s > 0)
+ emit(ctx, Osar, Kl, q1, hi2, getcon(ctx, m.s, fn));
+ if (m.need_add)
+ emit(ctx, Oadd, Kl, hi2, hi, xl);
+ emit(ctx, Osar, Kl, hi, prod, getcon(ctx, 32, fn));
+ emit(ctx, Omul, Kl, prod, xl, getcon(ctx, M64, fn));
+ emit(ctx, Oextsw, Kl, xl, x, Ref::none());
+}
+
+bool lower_div(Ctx* ctx, Fn* fn, Ins* i, Blk* b, bool* rebuilt) {
+ if (i->op != Odiv && i->op != Orem)
+ return false;
+ if (i->cls != Kw)
+ return false;
+ if (rtype(i->arg[1]) != RCon)
+ return false;
+ Con* c = &fn->con[i->arg[1].val];
+ if (c->type != Con::CBits)
+ return false;
+ int32_t d = static_cast<int32_t>(c->bits.i);
+ if (d == 0 || d == static_cast<int32_t>(0x80000000))
+ return false; // Skip d=0 (leave to hardware) and d=INT_MIN (|d| overflows i32)
+
+ bool is_rem = (i->op == Orem);
+ Ref dst = i->to;
+ Ref x = i->arg[0];
+
+ // First hit: move unprocessed tail to end of insb (same as simpl.cc)
+ if (!*rebuilt) {
+ ctx->curi = ctx->insb + NIns;
+ ulong tail = static_cast<ulong>((b->ins + b->nins) - (i + 1));
+ ctx->curi -= tail;
+ icpy(ctx->curi, i + 1, tail);
+ *rebuilt = true;
+ }
+
+ // d = +-1 degenerate: rem -> 0; div(1)=x; div(-1)=-x.
+ if (d == 1 || d == -1) {
+ if (is_rem)
+ emit(ctx, Ocopy, Kw, dst, getcon(ctx, 0, fn), Ref::none());
+ else if (d == 1)
+ emit(ctx, Ocopy, Kw, dst, x, Ref::none());
+ else
+ emit(ctx, Oneg, Kw, dst, x, Ref::none());
+ return true;
+ }
+
+ // For rem: emit tail r = x - q*d first (reverse order), then div sequence.
+ Ref q_dst = is_rem ? newtmp(ctx, "divopt", Kw, fn) : dst;
+ if (is_rem) {
+ Ref qd = newtmp(ctx, "divopt", Kw, fn);
+ emit(ctx, Osub, Kw, dst, x, qd);
+ emit(ctx, Omul, Kw, qd, q_dst, getcon(ctx, static_cast<int64_t>(d), fn));
+ }
+
+ int32_t ad = (d < 0) ? -d : d;
+ if ((ad & (ad - 1)) == 0) {
+ // |d| is power of 2, k >= 1
+ int k = __builtin_ctz(static_cast<uint32_t>(ad));
+ if (d < 0) {
+ Ref q_pos = newtmp(ctx, "divopt", Kw, fn);
+ emit(ctx, Oneg, Kw, q_dst, q_pos, Ref::none());
+ emit_pow2_div_into(ctx, fn, q_pos, x, k);
+ } else {
+ emit_pow2_div_into(ctx, fn, q_dst, x, k);
+ }
+ } else {
+ emit_magic_div_into(ctx, fn, q_dst, x, ad, d < 0);
+ }
+ return true;
+}
+
+} // namespace
+
+void divopt(Ctx* ctx, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ bool rebuilt = false;
+ for (Ins* i = b->ins + b->nins; i != b->ins;) {
+ --i;
+ if (lower_div(ctx, fn, i, b, &rebuilt))
+ continue;
+ if (rebuilt)
+ emiti(ctx, *i);
+ }
+ if (rebuilt)
+ idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi));
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/fold.cc b/src/mid/fold.cc
new file mode 100644
index 0000000..df88694
--- /dev/null
+++ b/src/mid/fold.cc
@@ -0,0 +1,423 @@
+/* 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 fold.cc
+/// @brief Constant folding utilities -- called by GVN (dedupins / assoccon), no standalone driver.
+///
+/// See docs/fold.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cstdint>
+#include <cstring>
+
+namespace rcc {
+
+namespace {
+
+inline bool iscon(const Con* c, int w, uint64_t k) {
+ if (c->type != Con::CBits)
+ return false;
+ int64_t v = w ? c->bits.i : static_cast<int32_t>(c->bits.i);
+ return static_cast<uint64_t>(v) == (w ? k : static_cast<uint32_t>(k));
+}
+
+union Bits64 {
+ int64_t s;
+ uint64_t u;
+ float fs;
+ double fd;
+};
+
+inline Bits64 read_bits(const Con* c) {
+ Bits64 b;
+ b.s = c->bits.i;
+ return b;
+}
+
+int addr_combine(int op, const Con* cl, const Con* cr, int* typ_out, Sym* sym_out) {
+ int ctyp = Con::CBits;
+ Sym sym;
+ std::memset(&sym, 0, sizeof sym);
+
+ bool al = (cl->type == Con::CAddr);
+ bool ar = (cr->type == Con::CAddr);
+
+ if (op == Oadd) {
+ if (al && ar)
+ return 1;
+ if (al || ar) {
+ ctyp = Con::CAddr;
+ sym = al ? cl->sym : cr->sym;
+ }
+ } else if (op == Osub) {
+ if (al && ar) {
+ if (!symeq(cl->sym, cr->sym))
+ return 1;
+ // Subtract same sym.
+ } else if (al) {
+ ctyp = Con::CAddr;
+ sym = cl->sym;
+ } else if (ar) {
+ return 1; // Number - address is invalid
+ }
+ } else if (op == Ocast) {
+ if (al) {
+ ctyp = Con::CAddr;
+ sym = cl->sym;
+ }
+ } else if (al || ar) {
+ return 1;
+ }
+ *typ_out = ctyp;
+ *sym_out = sym;
+ return 0;
+}
+
+uint64_t foldcmp_int(int cmp, int w, Bits64 l, Bits64 r) {
+ if (!w) {
+ l.u = static_cast<uint64_t>(static_cast<int32_t>(l.u));
+ r.u = static_cast<uint64_t>(static_cast<int32_t>(r.u));
+ }
+ switch (cmp) {
+ case Cieq:
+ return l.u == r.u;
+ case Cine:
+ return l.u != r.u;
+ case Ciule:
+ return l.u <= r.u;
+ case Ciult:
+ return l.u < r.u;
+ case Ciuge:
+ return l.u >= r.u;
+ case Ciugt:
+ return l.u > r.u;
+ case Cisle:
+ return l.s <= r.s;
+ case Cislt:
+ return l.s < r.s;
+ case Cisge:
+ return l.s >= r.s;
+ case Cisgt:
+ return l.s > r.s;
+ default:
+ die("foldcmp_int: bad cmp %d", cmp);
+ }
+}
+
+template <typename F>
+uint64_t foldcmp_flt(int cmp, F l, F r) {
+ switch (cmp) {
+ case Cfeq:
+ return l == r;
+ case Cfne:
+ return l != r;
+ case Cfle:
+ return l <= r;
+ case Cflt:
+ return l < r;
+ case Cfge:
+ return l >= r;
+ case Cfgt:
+ return l > r;
+ case Cfo:
+ return (l < r) || (l >= r);
+ case Cfuo:
+ return !((l < r) || (l >= r));
+ default:
+ die("foldcmp_flt: bad cmp %d", cmp);
+ }
+}
+
+void foldflt(Con* res, int op, int w, const Con* cl, const Con* cr) {
+ if (cl->type != Con::CBits || cr->type != Con::CBits)
+ err(const_cast<char*>("invalid address operand for '%s'"), optab[op].name);
+
+ res->type = Con::CBits;
+ res->sym = Sym{};
+ std::memset(&res->bits, 0, sizeof res->bits);
+ res->flt = 0;
+
+ if (w) {
+ double ld = cl->bits.d, rd = cr->bits.d, xd;
+ switch (op) {
+ case Oadd:
+ xd = ld + rd;
+ break;
+ case Osub:
+ xd = ld - rd;
+ break;
+ case Oneg:
+ xd = -ld;
+ break;
+ case Odiv:
+ xd = ld / rd;
+ break;
+ case Omul:
+ xd = ld * rd;
+ break;
+ case Oswtof:
+ xd = static_cast<double>(static_cast<int32_t>(cl->bits.i));
+ break;
+ case Ouwtof:
+ xd = static_cast<double>(static_cast<uint32_t>(cl->bits.i));
+ break;
+ case Osltof:
+ xd = static_cast<double>(static_cast<int64_t>(cl->bits.i));
+ break;
+ case Oultof:
+ xd = static_cast<double>(static_cast<uint64_t>(cl->bits.i));
+ break;
+ case Oexts:
+ xd = static_cast<double>(cl->bits.s);
+ break;
+ case Ocast:
+ xd = ld;
+ break;
+ default:
+ die("foldflt: bad op Kd %d", op);
+ }
+ res->bits.d = xd;
+ res->flt = 2;
+ } else {
+ float ls = cl->bits.s, rs = cr->bits.s, xs;
+ switch (op) {
+ case Oadd:
+ xs = ls + rs;
+ break;
+ case Osub:
+ xs = ls - rs;
+ break;
+ case Oneg:
+ xs = -ls;
+ break;
+ case Odiv:
+ xs = ls / rs;
+ break;
+ case Omul:
+ xs = ls * rs;
+ break;
+ case Oswtof:
+ xs = static_cast<float>(static_cast<int32_t>(cl->bits.i));
+ break;
+ case Ouwtof:
+ xs = static_cast<float>(static_cast<uint32_t>(cl->bits.i));
+ break;
+ case Osltof:
+ xs = static_cast<float>(static_cast<int64_t>(cl->bits.i));
+ break;
+ case Oultof:
+ xs = static_cast<float>(static_cast<uint64_t>(cl->bits.i));
+ break;
+ case Otruncd:
+ xs = static_cast<float>(cl->bits.d);
+ break;
+ case Ocast:
+ xs = ls;
+ break;
+ default:
+ die("foldflt: bad op Ks %d", op);
+ }
+ res->bits.s = xs;
+ res->flt = 1;
+ }
+}
+
+} // namespace
+
+int foldint(Con* res, int op, int w, Con* cl, Con* cr) {
+ // Address combination check.
+ int ctyp;
+ Sym sym;
+ if (addr_combine(op, cl, cr, &ctyp, &sym))
+ return 1;
+
+ Bits64 l = read_bits(cl);
+ Bits64 r = read_bits(cr);
+
+ // Div / rem.
+ if (op == Odiv || op == Orem || op == Oudiv || op == Ourem) {
+ if (iscon(cr, w, 0))
+ return 1;
+ if (op == Odiv || op == Orem) {
+ uint64_t intmin = w ? static_cast<uint64_t>(INT64_MIN)
+ : static_cast<uint64_t>(static_cast<uint32_t>(INT32_MIN));
+ if (iscon(cr, w, static_cast<uint64_t>(-1)) && iscon(cl, w, intmin))
+ return 1;
+ }
+ }
+
+ // Dispatch.
+ uint64_t x;
+
+ if (op >= Ocmpw && op <= Ocmpw1) {
+ x = foldcmp_int(op - Ocmpw, /*w=*/0, l, r);
+ } else if (op >= Ocmpl && op <= Ocmpl1) {
+ x = foldcmp_int(op - Ocmpl, /*w=*/1, l, r);
+ } else if (op >= Ocmps && op <= Ocmps1) {
+ x = foldcmp_flt<float>(op - Ocmps, cl->bits.s, cr->bits.s);
+ } else if (op >= Ocmpd && op <= Ocmpd1) {
+ x = foldcmp_flt<double>(op - Ocmpd, cl->bits.d, cr->bits.d);
+ } else {
+ // Shift count mask.
+ const uint64_t shmask = static_cast<uint64_t>(31 | (w << 5));
+ switch (op) {
+ case Oadd:
+ x = l.u + r.u;
+ break;
+ case Osub:
+ x = l.u - r.u;
+ break;
+ case Oneg:
+ x = -l.u;
+ break;
+ case Omul:
+ x = l.u * r.u;
+ break;
+ case Odiv:
+ x = w ? static_cast<uint64_t>(l.s / r.s)
+ : static_cast<uint64_t>(static_cast<int32_t>(l.s) /
+ static_cast<int32_t>(r.s));
+ break;
+ case Orem:
+ x = w ? static_cast<uint64_t>(l.s % r.s)
+ : static_cast<uint64_t>(static_cast<int32_t>(l.s) %
+ static_cast<int32_t>(r.s));
+ break;
+ case Oudiv:
+ x = w ? l.u / r.u : static_cast<uint32_t>(l.u) / static_cast<uint32_t>(r.u);
+ break;
+ case Ourem:
+ x = w ? l.u % r.u : static_cast<uint32_t>(l.u) % static_cast<uint32_t>(r.u);
+ break;
+ case Oand:
+ x = l.u & r.u;
+ break;
+ case Oor:
+ x = l.u | r.u;
+ break;
+ case Oxor:
+ x = l.u ^ r.u;
+ break;
+ // Signed right shift.
+ case Osar:
+ x = static_cast<uint64_t>((w ? l.s : static_cast<int32_t>(l.s)) >> (r.u & shmask));
+ break;
+ case Oshr:
+ x = (w ? l.u : static_cast<uint32_t>(l.u)) >> (r.u & shmask);
+ break;
+ case Oshl:
+ x = l.u << (r.u & shmask);
+ break;
+
+ case Oextsb:
+ x = static_cast<uint64_t>(static_cast<int8_t>(l.u));
+ break;
+ case Oextub:
+ x = static_cast<uint8_t>(l.u);
+ break;
+ case Oextsh:
+ x = static_cast<uint64_t>(static_cast<int16_t>(l.u));
+ break;
+ case Oextuh:
+ x = static_cast<uint16_t>(l.u);
+ break;
+ case Oextsw:
+ x = static_cast<uint64_t>(static_cast<int32_t>(l.u));
+ break;
+ case Oextuw:
+ x = static_cast<uint32_t>(l.u);
+ break;
+
+ case Ostosi:
+ x = w ? static_cast<uint64_t>(static_cast<int64_t>(cl->bits.s))
+ : static_cast<uint64_t>(static_cast<int32_t>(cl->bits.s));
+ break;
+ case Ostoui:
+ x = w ? static_cast<uint64_t>(cl->bits.s)
+ : static_cast<uint64_t>(static_cast<uint32_t>(cl->bits.s));
+ break;
+ case Odtosi:
+ x = w ? static_cast<uint64_t>(static_cast<int64_t>(cl->bits.d))
+ : static_cast<uint64_t>(static_cast<int32_t>(cl->bits.d));
+ break;
+ case Odtoui:
+ x = w ? static_cast<uint64_t>(cl->bits.d)
+ : static_cast<uint64_t>(static_cast<uint32_t>(cl->bits.d));
+ break;
+ case Ocast:
+ x = l.u;
+ break;
+ default:
+ die("foldint: unsupported op %d", op);
+ }
+ }
+
+ // Write back.
+ res->type = static_cast<decltype(res->type)>(ctyp);
+ res->sym = sym;
+ std::memset(&res->bits, 0, sizeof res->bits);
+ res->bits.i = static_cast<int64_t>(x);
+ res->flt = 0;
+ return 0;
+}
+
+namespace {
+
+Ref opfold(Ctx* ctx, int op, int cls, Con* cl, Con* cr, Fn* fn) {
+ Con c;
+ std::memset(&c, 0, sizeof c);
+
+ if (cls == Kw || cls == Kl) {
+ if (foldint(&c, op, cls == Kl, cl, cr))
+ return Ref::none();
+ } else {
+ foldflt(&c, op, cls == Kd, cl, cr);
+ }
+
+ if (!KWIDE(cls))
+ c.bits.i &= 0xffffffff;
+
+ Ref r = newcon(ctx, &c, fn);
+ assert((cls != Ks && cls != Kd) || c.flt);
+ return r;
+}
+
+} // namespace
+
+Ref foldref(Ctx* ctx, Fn* fn, Ins* i) {
+ if (rtype(i->to) != RTmp)
+ return Ref::none();
+ if (!optab[i->op].canfold)
+ return Ref::none();
+ if (rtype(i->arg[0]) != RCon)
+ return Ref::none();
+
+ Ref rr = i->arg[1];
+ if (req(rr, Ref::none()))
+ rr = Ref::con_z();
+ if (rtype(rr) != RCon)
+ return Ref::none();
+
+ Con* cl = &fn->con[i->arg[0].val];
+ Con* cr = &fn->con[rr.val];
+ return opfold(ctx, i->op, i->cls, cl, cr, fn);
+}
+
+} // namespace rcc
diff --git a/src/mid/gcm.cc b/src/mid/gcm.cc
new file mode 100644
index 0000000..cb3c1df
--- /dev/null
+++ b/src/mid/gcm.cc
@@ -0,0 +1,373 @@
+/* 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 gcm.cc
+/// @brief Global Code Motion (GCM, Click's algorithm) -- rearranges instructions into the
+/// "shallowest loop" legal block on the dominator tree.
+///
+/// See docs/gcm.md for architectural notes.
+
+#include "rcc/all.h"
+
+namespace rcc {
+
+namespace {
+
+constexpr uint NOBID = ~0u;
+
+bool isdivwl(Ins* i) {
+ return INRANGE(i->op, Odiv, Ourem) && KBASE(i->cls) == 0;
+}
+
+bool canelim(Ins* i) {
+ return isload(i->op) || isalloc(i->op) || isdivwl(i);
+}
+
+Ins nopins() {
+ Ins i = {};
+ i.op = Onop;
+ return i;
+}
+
+bool cheap(Ins* i) {
+ if (KBASE(i->cls) != 0)
+ return false;
+ switch (i->op) {
+ case Oneg:
+ case Oadd:
+ case Osub:
+ case Omul:
+ case Oand:
+ case Oor:
+ case Oxor:
+ case Osar:
+ case Oshr:
+ case Oshl:
+ return true;
+ default: {
+ int x;
+ return iscmp(i->op, &x, &x) != 0;
+ }
+ }
+}
+
+// schedearly
+
+uint earlyins(Fn*, Blk*, Ins*);
+
+uint schedearly(Fn* fn, Ref r) {
+ if (rtype(r) != RTmp)
+ return 0;
+
+ Tmp* t = &fn->tmp[r.val];
+ if (t->gcmbid != NOBID)
+ return t->gcmbid;
+
+ if (!t->def)
+ return t->gcmbid = t->bid; // phi doesn't move, pinned to def block
+
+ Blk* b = fn->rpo[t->bid];
+ assert(b->ins <= t->def && t->def < &b->ins[b->nins]);
+ t->gcmbid = 0; // Visiting sentinel (recursive backedge gets 0 = start)
+ return t->gcmbid = earlyins(fn, b, t->def);
+}
+
+uint earlyins(Fn* fn, Blk* b, Ins* i) {
+ uint ba = schedearly(fn, i->arg[0]);
+ uint bb = schedearly(fn, i->arg[1]);
+ assert(ba != NOBID && bb != NOBID);
+
+ // Take the deeper operand block in dominator tree (shallower dominates deeper)
+ if (fn->rpo[ba]->depth < fn->rpo[bb]->depth) {
+ assert(dom(fn->rpo[ba], fn->rpo[bb]));
+ ba = bb;
+ }
+ return pinned(i) ? b->id : ba;
+}
+
+void earlyblk(Fn* fn, uint bid) {
+ Blk* b = fn->rpo[bid];
+
+ for (Phi* p = b->phi; p; p = p->link)
+ for (uint n = 0; n < p->narg; n++)
+ schedearly(fn, p->arg[n]);
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (pinned(i)) {
+ schedearly(fn, i->arg[0]);
+ schedearly(fn, i->arg[1]);
+ }
+ schedearly(fn, b->jmp.arg);
+}
+
+// schedlate + bestbid
+
+uint lcabid(Fn* fn, uint b1, uint b2) {
+ if (b1 == NOBID)
+ return b2;
+ if (b2 == NOBID)
+ return b1;
+ Blk* b = lca(fn->rpo[b1], fn->rpo[b2]);
+ assert(b);
+ return b->id;
+}
+
+uint bestbid(Fn* fn, uint earlybid, uint latebid) {
+ if (latebid == NOBID)
+ return NOBID; // Unused
+ assert(earlybid != NOBID);
+
+ Blk* earlyb = fn->rpo[earlybid];
+ Blk* bestb = fn->rpo[latebid];
+ assert(dom(earlyb, bestb));
+
+ for (Blk* curb = bestb; curb != earlyb;) {
+ curb = curb->idom;
+ if (curb->loop < bestb->loop)
+ bestb = curb;
+ }
+ return bestb->id;
+}
+
+uint lateins(Fn*, Blk*, Ins*, Ref);
+uint latephi(Fn*, Phi*, Ref);
+uint latejmp(Blk*, Ref);
+
+uint schedlate(Fn* fn, Ref r) {
+ if (rtype(r) != RTmp)
+ return NOBID;
+
+ Tmp* t = &fn->tmp[r.val];
+ if (t->visit)
+ return t->gcmbid;
+ t->visit = 1;
+
+ uint earlybid = t->gcmbid;
+ if (earlybid == NOBID)
+ return NOBID; // Unused
+
+ t->gcmbid = t->bid; // Reuse gcmbid field to temporarily store late
+ uint latebid = NOBID;
+ for (Use* u = t->use; u < &t->use[t->nuse]; u++) {
+ assert(u->bid < fn->nblk);
+ Blk* b = fn->rpo[u->bid];
+ uint usebid;
+ switch (u->type) {
+ case Use::UPhi:
+ usebid = latephi(fn, u->u.phi, r);
+ break;
+ case Use::UIns:
+ usebid = lateins(fn, b, u->u.ins, r);
+ break;
+ case Use::UJmp:
+ usebid = latejmp(b, r);
+ break;
+ default:
+ die("unreachable");
+ }
+ latebid = lcabid(fn, latebid, usebid);
+ }
+ // latebid might still be NOBID: temp is only indirectly used by "deletable pinned instructions"
+
+ if (t->def && !pinned(t->def))
+ t->gcmbid = bestbid(fn, earlybid, latebid);
+ // Otherwise keep early
+
+ return t->gcmbid;
+}
+
+uint lateins(Fn* fn, Blk* b, Ins* i, Ref r) {
+ assert(b->ins <= i && i < &b->ins[b->nins]);
+ assert(req(i->arg[0], r) || req(i->arg[1], r));
+
+ uint latebid = schedlate(fn, i->to);
+ if (pinned(i)) {
+ if (latebid == NOBID && canelim(i))
+ return NOBID;
+ return b->id;
+ }
+ return latebid;
+}
+
+uint latephi(Fn* fn, Phi* p, Ref r) {
+ if (!p->narg)
+ return NOBID; // Marked unused
+
+ uint latebid = NOBID;
+ for (uint n = 0; n < p->narg; n++)
+ if (req(p->arg[n], r))
+ latebid = lcabid(fn, latebid, p->blk[n]->id);
+
+ assert(latebid != NOBID);
+ return latebid;
+}
+
+uint latejmp(Blk* b, Ref r) {
+ if (req(b->jmp.arg, Ref::none()))
+ return NOBID;
+ assert(req(b->jmp.arg, r));
+ return b->id;
+}
+
+void lateblk(Fn* fn, uint bid) {
+ Blk* b = fn->rpo[bid];
+
+ for (Phi** pp = &b->phi; *pp;) {
+ if (schedlate(fn, (*pp)->to) == NOBID) {
+ (*pp)->narg = 0; // Mark unused
+ *pp = (*pp)->link; // Remove this phi
+ } else {
+ pp = &(*pp)->link;
+ }
+ }
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (pinned(i))
+ schedlate(fn, i->to);
+}
+
+// gcmmove
+
+void addgcmins(Ctx* ctx, Fn* fn, Ins* vins, uint nins) {
+ for (Ins* i = vins; i < &vins[nins]; i++) {
+ assert(rtype(i->to) == RTmp);
+ Tmp* t = &fn->tmp[i->to.val];
+ Blk* b = fn->rpo[t->gcmbid];
+ addins(ctx, &b->ins, &b->nins, i);
+ }
+}
+
+void gcmmove(Ctx* ctx, Fn* fn) {
+ uint nins = 0;
+ Ins* vins = static_cast<Ins*>(vnew(ctx, nins, sizeof vins[0], PFn));
+
+ for (Tmp* t = fn->tmp; t < &fn->tmp[fn->ntmp]; t++) {
+ if (!t->def || t->bid == t->gcmbid)
+ continue;
+ Ins* i = t->def;
+ if (pinned(i) && !canelim(i))
+ continue;
+ assert(rtype(i->to) == RTmp && t == &fn->tmp[i->to.val]);
+ if (t->gcmbid != NOBID)
+ addins(ctx, &vins, &nins, i);
+ *i = nopins();
+ }
+ addgcmins(ctx, fn, vins, nins);
+}
+
+// schedblk (intra-block DFS ordering)
+
+Ins* schedins(Ctx* ctx, Fn* fn, Blk* b, Ins* i, Ins** pvins, uint* pnins) {
+ Ins *i0, *i1;
+ igroup(b, i, &i0, &i1);
+
+ // First recursively order the defs in this block that are depended on by operands in this group
+ for (i = i0; i < i1; i++)
+ for (uint n = 0; n < 2; n++) {
+ if (rtype(i->arg[n]) != RTmp)
+ continue;
+ Tmp* t = &fn->tmp[i->arg[n].val];
+ if (t->bid != b->id || !t->def)
+ continue;
+ schedins(ctx, fn, b, t->def, pvins, pnins);
+ }
+ // Then emit this group, wipe original to nop (prevent double emission)
+ for (i = i0; i < i1; i++) {
+ addins(ctx, pvins, pnins, i);
+ *i = nopins();
+ }
+ return i1;
+}
+
+void schedblk(Ctx* ctx, Fn* fn) {
+ Ins* vins = static_cast<Ins*>(vnew(ctx, 0, sizeof vins[0], PHeap));
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ uint nins = 0;
+ for (Ins* i = b->ins; i < &b->ins[b->nins];)
+ i = schedins(ctx, fn, b, i, &vins, &nins);
+ idup(ctx, b, vins, nins);
+ }
+ vfree(vins);
+}
+
+// sink (trivial op sinking)
+
+void sinkref(Ctx* ctx, Fn* fn, Blk* b, Ref* pr) {
+ if (rtype(*pr) != RTmp)
+ return;
+ Tmp* t = &fn->tmp[pr->val];
+ if (!t->def || t->bid == b->id || pinned(t->def) || !cheap(t->def))
+ return;
+
+ // Copy t->def and sink to b
+ Ins i = *t->def;
+ Ref r = newtmp(ctx, "snk", t->cls, fn);
+ t = nullptr; // newtmp might realloc fn->tmp, old pointer invalid
+ *pr = r;
+ i.to = r;
+ fn->tmp[r.val].gcmbid = b->id;
+ emiti(ctx, i);
+ sinkref(ctx, fn, b, &i.arg[0]);
+ sinkref(ctx, fn, b, &i.arg[1]);
+}
+
+void sink(Ctx* ctx, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (isload(i->op))
+ sinkref(ctx, fn, b, &i->arg[0]);
+ else if (isstore(i->op))
+ sinkref(ctx, fn, b, &i->arg[1]);
+ sinkref(ctx, fn, b, &b->jmp.arg);
+ }
+ addgcmins(ctx, fn, ctx->curi, static_cast<uint>(&ctx->insb[NIns] - ctx->curi));
+}
+
+} // namespace
+
+int pinned(Ins* i) {
+ return optab[i->op].pinned || isdivwl(i);
+}
+
+void gcm(Ctx* ctx, Fn* fn) {
+ filldepth(fn);
+ fillloop(fn);
+
+ for (Tmp* t = fn->tmp; t < &fn->tmp[fn->ntmp]; t++) {
+ t->visit = 0;
+ t->gcmbid = NOBID;
+ }
+ for (uint bid = 0; bid < fn->nblk; bid++)
+ earlyblk(fn, bid);
+ for (uint bid = 0; bid < fn->nblk; bid++)
+ lateblk(fn, bid);
+
+ gcmmove(ctx, fn);
+ filluse(ctx, fn);
+ ctx->curi = &ctx->insb[NIns];
+ sink(ctx, fn);
+ filluse(ctx, fn);
+ schedblk(ctx, fn);
+
+ if (debug['G']) {
+ fprintf(ctx->dbg_buf, "\n> After GCM:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/gvn.cc b/src/mid/gvn.cc
new file mode 100644
index 0000000..8e029da
--- /dev/null
+++ b/src/mid/gvn.cc
@@ -0,0 +1,414 @@
+/* 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 gvn.cc
+/// @brief Global Value Numbering (GVN) - Cross-block deduplication + jnz simplification + dead
+/// block rescue.
+///
+/// See docs/gvn.md for Global Value Numbering architectural notes.
+
+#include "rcc/all.h"
+
+namespace rcc {
+
+namespace {
+
+// Instruction hashing.
+
+inline uint mix(uint x0, uint x1) {
+ return x0 + 31 * x1;
+}
+
+inline uint rhash(Ref r) {
+ return mix(r.type, r.val);
+}
+
+uint ihash(Ins* i) {
+ uint h = mix(i->op, i->cls);
+ h = mix(h, rhash(i->arg[0]));
+ h = mix(h, rhash(i->arg[1]));
+ return h;
+}
+
+bool ieq(Ins* a, Ins* b) {
+ return a->op == b->op && a->cls == b->cls && req(a->arg[0], b->arg[0]) &&
+ req(a->arg[1], b->arg[1]);
+}
+
+// Lookup instruction.
+Ins* gvndup(Ctx* ctx, Ins* i, int insert) {
+ uint idx = ihash(i) % ctx->gvn_tbln;
+ for (Ins* ii; (ii = ctx->gvn_tbl[idx]);) {
+ if (ieq(i, ii))
+ return ii;
+ if (++idx >= ctx->gvn_tbln)
+ idx = 0;
+ }
+ if (insert)
+ ctx->gvn_tbl[idx] = i;
+ return nullptr;
+}
+
+// Use replacement.
+
+void replaceuse(Ctx* ctx, Fn* fn, Use* u, Ref r1, Ref r2) {
+ Tmp* t2 = (rtype(r2) == RTmp) ? &fn->tmp[r2.val] : nullptr;
+ Blk* b = fn->rpo[u->bid];
+ switch (u->type) {
+ case Use::UPhi: {
+ Phi* p = u->u.phi;
+ for (Ref* pr = p->arg; pr < &p->arg[p->narg]; ++pr)
+ if (req(*pr, r1))
+ *pr = r2;
+ if (t2)
+ adduse_phi(ctx, t2, b, p);
+ break;
+ }
+ case Use::UIns: {
+ Ins* i = u->u.ins;
+ for (int n = 0; n < 2; ++n)
+ if (req(i->arg[n], r1))
+ i->arg[n] = r2;
+ if (t2)
+ adduse_ins(ctx, t2, b, i);
+ break;
+ }
+ case Use::UJmp:
+ if (req(b->jmp.arg, r1))
+ b->jmp.arg = r2;
+ if (t2)
+ adduse_jmp(ctx, t2, b);
+ break;
+ case Use::UXXX:
+ die("unreachable");
+ }
+}
+
+void replaceuses(Ctx* ctx, Fn* fn, Ref r1, Ref r2) {
+ assert(rtype(r1) == RTmp);
+ Tmp* t1 = &fn->tmp[r1.val];
+ for (Use* u = t1->use; u < &t1->use[t1->nuse]; ++u)
+ replaceuse(ctx, fn, u, r1, r2);
+ t1->nuse = 0;
+}
+
+// Nullify instruction.
+void killins(Ctx* ctx, Fn* fn, Ins* i, Ref r) {
+ replaceuses(ctx, fn, i->to, r);
+ *i = {};
+ i->op = Onop;
+}
+
+// Handle degraded phi.
+void dedupphi(Ctx* ctx, Fn* fn, Blk* b) {
+ Phi** pp = &b->phi;
+ for (Phi* p; (p = *pp);) {
+ Ref r = phicopyref(ctx, fn, b, p);
+ if (req(r, Ref::none())) {
+ pp = &p->link;
+ } else {
+ replaceuses(ctx, fn, p->to, r);
+ p->to = Ref::none();
+ *pp = p->link;
+ }
+ }
+}
+
+// Instruction normalization.
+
+int rcmp(Ref a, Ref b) {
+ if (rtype(a) != rtype(b))
+ return rtype(a) - rtype(b);
+ return static_cast<int>(a.val) - static_cast<int>(b.val);
+}
+
+void normins(Ctx* ctx, Fn* fn, Ins* i) {
+ int64_t v;
+ // Truncate to 32 bits.
+ for (int n = 0; n < 2; ++n)
+ if (!KWIDE(argcls(i, n)) && isconbits(fn, i->arg[n], &v) && (v & 0xffffffffLL) != v)
+ i->arg[n] = getcon(ctx, v & 0xffffffffLL, fn);
+ // Commutative op sort.
+ if (optab[i->op].commutes && rcmp(i->arg[0], i->arg[1]) > 0) {
+ Ref r = i->arg[1];
+ i->arg[1] = i->arg[0];
+ i->arg[0] = r;
+ }
+}
+
+// Negate constant.
+int negcon(int cls, Con* c) {
+ Con z = {};
+ z.type = Con::CBits;
+ z.bits.i = 0;
+ return foldint(c, Osub, cls, &z, c);
+}
+
+// Merge constants.
+void assoccon(Ctx* ctx, Fn* fn, Blk* b, Ins* i1) {
+ int op = (i1->op == Osub) ? Oadd : static_cast<int>(i1->op);
+
+ if (!optab[op].assoc || KBASE(i1->cls) != 0 || rtype(i1->arg[0]) != RTmp ||
+ rtype(i1->arg[1]) != RCon)
+ return;
+ Con c1 = fn->con[i1->arg[1].val];
+
+ Tmp* t2 = &fn->tmp[i1->arg[0].val];
+ if (!t2->def)
+ return;
+ Ins* i2 = t2->def;
+
+ if (op != (i2->op == Osub ? Oadd : static_cast<int>(i2->op)) || rtype(i2->arg[1]) != RCon)
+ return;
+ Con c2 = fn->con[i2->arg[1].val];
+
+ assert(KBASE(i2->cls) == 0);
+ assert(KWIDE(i2->cls) >= KWIDE(i1->cls));
+
+ // Unify as add.
+ if (i1->op == Osub && negcon(i1->cls, &c1))
+ return;
+ if (i2->op == Osub && negcon(i2->cls, &c2))
+ return;
+ Con c;
+ if (foldint(&c, op, i1->cls, &c1, &c2))
+ return;
+
+ // Revert to sub.
+ if (op == Oadd && c.type == Con::CBits &&
+ ((i1->cls == Kl && c.bits.i < 0) ||
+ (i1->cls == Kw && static_cast<int32_t>(c.bits.i) < 0))) {
+ int fail = negcon(i1->cls, &c);
+ assert(fail == 0);
+ (void)fail;
+ op = Osub;
+ }
+
+ i1->op = static_cast<uint>(op);
+ i1->arg[0] = i2->arg[0];
+ i1->arg[1] = newcon(ctx, &c, fn);
+ adduse_ins(ctx, &fn->tmp[i1->arg[0].val], b, i1);
+}
+
+// Deduplication.
+void dedupins(Ctx* ctx, Fn* fn, Blk* b, Ins* i) {
+ normins(ctx, fn, i);
+ if (i->op == Onop || pinned(i))
+ return;
+ // sel skip.
+ assert(i->op != Osel0);
+ assert(!req(i->to, Ref::none()));
+ assoccon(ctx, fn, b, i);
+
+ Ref r = copyref(ctx, fn, b, i);
+ if (!req(r, Ref::none())) {
+ killins(ctx, fn, i, r);
+ return;
+ }
+ r = foldref(ctx, fn, i);
+ if (!req(r, Ref::none())) {
+ killins(ctx, fn, i, r);
+ return;
+ }
+ Ins* i1 = gvndup(ctx, i, 1);
+ if (i1)
+ killins(ctx, fn, i, i1->to);
+}
+
+// jnz simplification.
+
+// Check cmp eq/ne 0.
+int cmpeqz(Fn* fn, Ref r, Ref* arg, int* cls, int* eqval) {
+ if (rtype(r) != RTmp)
+ return 0;
+ Ins* i = fn->tmp[r.val].def;
+ if (i && optab[i->op].cmpeqwl && req(i->arg[1], Ref::con_z())) {
+ *arg = i->arg[0];
+ *cls = argcls(i, 0);
+ *eqval = optab[i->op].eqval;
+ return 1;
+ }
+ return 0;
+}
+
+// Check branch dominance.
+int branchdom(Fn* fn, Blk* bif, Blk* bbr1, Blk* bbr2, Blk* b) {
+ assert(bif->jmp.type == Jjnz);
+ return b != bif && dom(bbr1, b) && !reachesnotvia(fn, bbr2, b, bif);
+}
+
+// Check 0-branch.
+int domzero(Fn* fn, Blk* d, Blk* b, int* z) {
+ if (branchdom(fn, d, d->s1, d->s2, b)) {
+ *z = 0;
+ return 1;
+ }
+ if (branchdom(fn, d, d->s2, d->s1, b)) {
+ *z = 1;
+ return 1;
+ }
+ return 0;
+}
+
+// Actual width class.
+int usecls(Use* u, Ref r, int cls) {
+ switch (u->type) {
+ case Use::UIns: {
+ int k = Kx; // widest
+ if (req(u->u.ins->arg[0], r))
+ k = argcls(u->u.ins, 0);
+ if (req(u->u.ins->arg[1], r) && (k == Kx || !KWIDE(k)))
+ k = argcls(u->u.ins, 1);
+ return k == Kx ? cls : k;
+ }
+ case Use::UPhi:
+ if (req(u->u.phi->to, Ref::none()))
+ return cls; // eliminated
+ return u->u.phi->cls;
+ case Use::UJmp:
+ return Kw;
+ default:
+ break;
+ }
+ die("unreachable");
+}
+
+// Replace with 0.
+void propjnz0(Ctx* ctx, Fn* fn, Blk* bif, Blk* s0, Blk* snon0, Ref r, int cls) {
+ if (s0->npred != 1 || rtype(r) != RTmp)
+ return;
+ Tmp* t = &fn->tmp[r.val];
+ for (Use* u = t->use; u < &t->use[t->nuse]; ++u) {
+ Blk* b = fn->rpo[u->bid];
+ // Match width.
+ if (usecls(u, r, cls) == cls && branchdom(fn, bif, s0, snon0, b))
+ replaceuse(ctx, fn, u, r, Ref::con_z());
+ }
+}
+
+void dedupjmp(Ctx* ctx, Fn* fn, Blk* b) {
+ if (b->jmp.type != Jjnz)
+ return;
+
+ // Propagate jmp arg.
+ propjnz0(ctx, fn, b, b->s2, b->s1, b->jmp.arg, Kw);
+ // Propagate underlying arg.
+ Ref arg;
+ int cls, eqval;
+ if (cmpeqz(fn, b->jmp.arg, &arg, &cls, &eqval)) {
+ Blk* ps[2] = {b->s1, b->s2};
+ propjnz0(ctx, fn, b, ps[eqval ^ 1], ps[eqval], arg, cls);
+ }
+
+ // Collapse jnz.
+ int64_t v = 1;
+ int z = 0;
+ if (b->s1 == b->s2 || isconbits(fn, b->jmp.arg, &v) || zeroval(fn, b, b->jmp.arg, Kw, &z)) {
+ if (v == 0 || z)
+ b->s1 = b->s2;
+ b->s2 = nullptr; // Dead branch live instructions are rescued later in rebuildcfg
+ b->jmp.type = Jjmp;
+ b->jmp.arg = Ref::none();
+ }
+}
+
+// Rebuild CFG.
+
+void rebuildcfg(Ctx* ctx, Fn* fn) {
+ uint nblk = fn->nblk;
+ Blk** rpo = static_cast<Blk**>(emalloc(nblk * sizeof rpo[0]));
+ memcpy(rpo, fn->rpo, nblk * sizeof rpo[0]);
+
+ fillcfg(ctx, fn); // Dead blocks are marked with id == ~0u here
+
+ Blk* s = fn->start;
+ for (uint n = 0; n < nblk; ++n) {
+ Blk* b = rpo[n];
+ if (b->id != ~0u)
+ continue; // Still reachable after GVN, skip
+ assert(b != s);
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i)
+ if (!optab[i->op].pinned && gvndup(ctx, i, 0) == i)
+ addins(ctx, &s->ins, &s->nins, i);
+ }
+ free(rpo);
+}
+
+} // namespace
+
+// zeroval infer.
+int zeroval(Fn* fn, Blk* b, Ref r, int cls, int* z) {
+ Ref arg;
+ int cls1, eqval;
+ for (Blk* d = b->idom; d; d = d->idom) {
+ if (d->jmp.type != Jjnz)
+ continue;
+ if (req(r, d->jmp.arg) && cls == Kw && domzero(fn, d, b, z))
+ return 1;
+ if (cmpeqz(fn, d->jmp.arg, &arg, &cls1, &eqval) && req(r, arg) && cls == cls1 &&
+ domzero(fn, d, b, z)) {
+ *z ^= eqval;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+// gvn entry.
+void gvn(Ctx* ctx, Fn* fn) {
+ ctx->gvn_con01[0] = getcon(ctx, 0, fn);
+ ctx->gvn_con01[1] = getcon(ctx, 1, fn);
+
+ // Zero out width memo.
+ for (Blk* b = fn->start; b; b = b->link)
+ for (Phi* p = b->phi; p; p = p->link)
+ p->visit = 0;
+
+ fillloop(fn);
+ narrowpars(ctx, fn);
+ filluse(ctx, fn);
+ ssacheck(fn);
+
+ uint nins = 0;
+ for (Blk* b = fn->start; b; b = b->link) {
+ b->visit = 0;
+ nins += b->nins;
+ }
+
+ ctx->gvn_tbln = nins + nins / 2;
+ ctx->gvn_tbl = static_cast<Ins**>(emalloc(ctx->gvn_tbln * sizeof ctx->gvn_tbl[0]));
+ for (uint n = 0; n < fn->nblk; ++n) {
+ Blk* b = fn->rpo[n];
+ dedupphi(ctx, fn, b);
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i)
+ dedupins(ctx, fn, b, i);
+ dedupjmp(ctx, fn, b);
+ }
+ rebuildcfg(ctx, fn);
+ free(ctx->gvn_tbl);
+ ctx->gvn_tbl = nullptr;
+ ctx->gvn_tbln = 0;
+
+ if (debug['G']) {
+ fprintf(ctx->dbg_buf, "\n> After GVN:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/ifopt.cc b/src/mid/ifopt.cc
new file mode 100644
index 0000000..c82fb4c
--- /dev/null
+++ b/src/mid/ifopt.cc
@@ -0,0 +1,127 @@
+/* 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 ifopt.cc
+/// @brief If-conversion: collapses if-then[-else] diamond CFG into straight-line block + sel
+/// instructions.
+///
+/// See docs/ifopt.md for architectural notes.
+
+#include "rcc/all.h"
+
+namespace rcc {
+
+namespace {
+
+enum {
+ MaxIns = 2,
+ MaxPhis = 2,
+};
+
+bool okbranch(Blk* b) {
+ int n = 0;
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) {
+ if (i->op == Odbgloc)
+ continue;
+ if (pinned(i))
+ return false;
+ if (i->op != Onop)
+ ++n;
+ }
+ return n <= MaxIns;
+}
+
+bool okjoin(Blk* b) {
+ int n = 0;
+ for (Phi* p = b->phi; p; p = p->link) {
+ if (KBASE(p->cls) != 0)
+ return false;
+ ++n;
+ }
+ return n <= MaxPhis;
+}
+
+bool okgraph(Blk* ifb, Blk* thenb, Blk* elseb, Blk* joinb) {
+ assert(thenb != elseb);
+ return joinb->npred == 2 && okjoin(joinb) && (thenb == ifb || okbranch(thenb)) &&
+ (elseb == ifb || okbranch(elseb));
+}
+
+void convert(Ctx* ctx, Blk* ifb, Blk* thenb, Blk* elseb, Blk* joinb) {
+ Ins* ins = static_cast<Ins*>(vnew(ctx, 0, sizeof ins[0], PHeap));
+ uint nins = 0;
+
+ addbins(ctx, &ins, &nins, ifb);
+ if (thenb != ifb)
+ addbins(ctx, &ins, &nins, thenb);
+ if (elseb != ifb)
+ addbins(ctx, &ins, &nins, elseb);
+
+ assert(joinb->npred == 2);
+ Ins sel = {};
+ if (joinb->phi) {
+ sel.op = Osel0;
+ sel.cls = static_cast<uint>(Kw);
+ sel.arg[0] = ifb->jmp.arg;
+ addins(ctx, &ins, &nins, &sel);
+ }
+
+ sel = {};
+ sel.op = Osel1;
+ for (Phi* p = joinb->phi; p; p = p->link) {
+ sel.to = p->to;
+ sel.cls = static_cast<uint>(p->cls);
+ sel.arg[0] = phiarg(p, thenb);
+ sel.arg[1] = phiarg(p, elseb);
+ addins(ctx, &ins, &nins, &sel);
+ }
+
+ idup(ctx, ifb, ins, nins);
+ ifb->jmp.type = Jjmp;
+ ifb->jmp.arg = Ref::none();
+ ifb->s1 = joinb;
+ ifb->s2 = nullptr;
+ joinb->npred = 1;
+ joinb->pred[0] = ifb;
+ joinb->phi = nullptr;
+ vfree(ins);
+}
+
+} // namespace
+
+void ifconvert(Ctx* ctx, Fn* fn) {
+ if (debug['K'])
+ fputs("\n> If-conversion:\n", ctx->dbg_buf);
+
+ Blk *thenb, *elseb, *joinb;
+ for (Blk* ifb = fn->start; ifb; ifb = ifb->link)
+ if (ifgraph(ifb, &thenb, &elseb, &joinb) && okgraph(ifb, thenb, elseb, joinb)) {
+ if (debug['K'])
+ fprintf(ctx->dbg_buf, " @%s -> @%s, @%s -> @%s\n", ifb->name, thenb->name,
+ elseb->name, joinb->name);
+ convert(ctx, ifb, thenb, elseb, joinb);
+ }
+
+ if (debug['K']) {
+ fprintf(ctx->dbg_buf, "\n> After if-conversion:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/inline.cc b/src/mid/inline.cc
new file mode 100644
index 0000000..60e2dc1
--- /dev/null
+++ b/src/mid/inline.cc
@@ -0,0 +1,484 @@
+/* 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 inline.cc
+/// @brief Function inlining (with SCC check to prevent recursion + fixpoint iteration).
+///
+/// See docs/inline.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <algorithm>
+#include <cstring>
+#include <functional>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+constexpr int kInlineLeafThreshold = 64;
+constexpr int kInlineSingleCallerThreshold = 256;
+constexpr int kInlineNonLeafThreshold = 48;
+constexpr int kFixpointMaxIters = 16;
+constexpr int kConstArgDiscount = 8;
+
+// Metrics
+
+bool is_leaf(Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link)
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (i->op == Ocall)
+ return false;
+ return true;
+}
+
+int count_real_ins(Fn* fn) {
+ int n = 0;
+ for (Blk* b = fn->start; b; b = b->link)
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (i->op != Onop)
+ n++;
+ // phi also counts towards cost, one phi equals one ins
+ for (Blk* b = fn->start; b; b = b->link)
+ for (Phi* p = b->phi; p; p = p->link)
+ n++;
+ return n;
+}
+
+Fn* lookup_callee(Ref fn_ref, Fn* caller, const std::vector<Fn*>& fns) {
+ if (rtype(fn_ref) != RCon)
+ return nullptr;
+ Con* c = &caller->con[fn_ref.val];
+ if (c->type != Con::CAddr)
+ return nullptr;
+ const char* name = str(c->sym.id);
+ for (Fn* f : fns)
+ if (std::strcmp(f->name, name) == 0)
+ return f;
+ return nullptr;
+}
+
+std::unordered_map<std::string, int> collect_call_counts(const std::vector<Fn*>& fns) {
+ std::unordered_map<std::string, int> cnt;
+ for (Fn* caller : fns) {
+ for (Blk* b = caller->start; b; b = b->link) {
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ if (i->op != Ocall)
+ continue;
+ if (rtype(i->arg[0]) != RCon)
+ continue;
+ Con* c = &caller->con[i->arg[0].val];
+ if (c->type != Con::CAddr)
+ continue;
+ cnt[str(c->sym.id)]++;
+ }
+ }
+ }
+ return cnt;
+}
+
+// Call Graph SCC
+
+std::unordered_map<Fn*, std::unordered_set<Fn*>> build_call_graph(const std::vector<Fn*>& fns) {
+ std::unordered_map<Fn*, std::unordered_set<Fn*>> g;
+ for (Fn* caller : fns) {
+ g[caller]; // Ensure node exists (even without outgoing edges)
+ for (Blk* b = caller->start; b; b = b->link) {
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ if (i->op != Ocall)
+ continue;
+ Fn* callee = lookup_callee(i->arg[0], caller, fns);
+ if (callee)
+ g[caller].insert(callee);
+ }
+ }
+ }
+ return g;
+}
+
+std::unordered_set<Fn*> find_cycle_members(
+ const std::vector<Fn*>& fns, const std::unordered_map<Fn*, std::unordered_set<Fn*>>& g) {
+ std::unordered_set<Fn*> cyclic;
+ std::unordered_map<Fn*, int> idx_of, lowlink;
+ std::unordered_set<Fn*> on_stack;
+ std::vector<Fn*> stk;
+ int counter = 0;
+
+ std::function<void(Fn*)> strongconnect = [&](Fn* v) {
+ idx_of[v] = counter;
+ lowlink[v] = counter;
+ counter++;
+ stk.push_back(v);
+ on_stack.insert(v);
+
+ auto it = g.find(v);
+ if (it != g.end()) {
+ for (Fn* w : it->second) {
+ if (idx_of.find(w) == idx_of.end()) {
+ strongconnect(w);
+ lowlink[v] = std::min(lowlink[v], lowlink[w]);
+ } else if (on_stack.count(w)) {
+ lowlink[v] = std::min(lowlink[v], idx_of[w]);
+ }
+ }
+ }
+
+ if (lowlink[v] == idx_of[v]) {
+ std::vector<Fn*> scc;
+ Fn* w;
+ do {
+ w = stk.back();
+ stk.pop_back();
+ on_stack.erase(w);
+ scc.push_back(w);
+ } while (w != v);
+
+ if (scc.size() >= 2) {
+ for (Fn* f : scc)
+ cyclic.insert(f);
+ } else {
+ // Single-node SCC: only count if it has self-loop (direct recursion)
+ Fn* f = scc[0];
+ auto it2 = g.find(f);
+ if (it2 != g.end() && it2->second.count(f))
+ cyclic.insert(f);
+ }
+ }
+ };
+
+ for (Fn* f : fns) {
+ if (idx_of.find(f) == idx_of.end())
+ strongconnect(f);
+ }
+ return cyclic;
+}
+
+// Splice
+
+struct RemapTables {
+ std::vector<Ref> tmp; // callee tmp id → caller Ref (RTmp)
+ std::vector<Ref> con; // callee con id → caller Ref (RCon)
+ std::vector<Blk*> blk; // callee blk id → cloned Blk* in caller
+};
+
+Ref remap_ref(Ref r, const RemapTables& m) {
+ switch (rtype(r)) {
+ case RTmp:
+ if (r.val == 0)
+ return r; // Ref::none()
+ assert(r.val < m.tmp.size());
+ return m.tmp[r.val];
+ case RCon:
+ assert(r.val < m.con.size());
+ return m.con[r.val];
+ default:
+ return r; // RInt/RType/RSlot/RMem/RCall do not need remap
+ }
+}
+
+struct RetEntry {
+ Blk* blk; // cloned ret block in caller
+ Ref val; // ret value (remapped), Ref::none() if void
+};
+
+void clone_blk_body(Ctx* ctx, Fn* caller, Blk* src, Blk* dst, const RemapTables& m,
+ const std::vector<Ref>& par_args, Blk* b_post, std::vector<RetEntry>& rets) {
+ // phi
+ Phi** ptail = &dst->phi;
+ *ptail = nullptr;
+ for (Phi* sp = src->phi; sp; sp = sp->link) {
+ Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ np->to = remap_ref(sp->to, m);
+ np->cls = sp->cls;
+ np->visit = 0;
+ np->narg = sp->narg;
+ np->arg = static_cast<Ref*>(vnew(ctx, sp->narg, sizeof(Ref), PFn));
+ np->blk = static_cast<Blk**>(vnew(ctx, sp->narg, sizeof(Blk*), PFn));
+ for (uint k = 0; k < sp->narg; k++) {
+ np->arg[k] = remap_ref(sp->arg[k], m);
+ np->blk[k] = m.blk[sp->blk[k]->id];
+ }
+ np->link = nullptr;
+ *ptail = np;
+ ptail = &np->link;
+ }
+
+ // ins: Opar -> Ocopy from par_args[k]
+ dst->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn));
+ dst->nins = 0;
+ int par_idx = 0;
+ for (Ins* si = src->ins; si < &src->ins[src->nins]; si++) {
+ Ins ni;
+ if (ispar(si->op)) {
+ // Replace all uses of par tmp with arg ref in caller. Produces an Ocopy to maintain SSA
+ // def, later copy/gvn will eliminate.
+ assert(par_idx < static_cast<int>(par_args.size()));
+ ni.op = Ocopy;
+ ni.cls = si->cls;
+ ni.to = remap_ref(si->to, m);
+ ni.arg[0] = par_args[static_cast<size_t>(par_idx++)];
+ ni.arg[1] = Ref::none();
+ } else {
+ ni.op = si->op;
+ ni.cls = si->cls;
+ ni.to = remap_ref(si->to, m);
+ ni.arg[0] = remap_ref(si->arg[0], m);
+ ni.arg[1] = remap_ref(si->arg[1], m);
+ }
+ addins(ctx, &dst->ins, &dst->nins, &ni);
+ }
+
+ // jmp: change isret to Jjmp pointing to b_post; pass through other jmps (s1/s2 updated by
+ // m.blk)
+ if (isret(src->jmp.type)) {
+ rets.push_back({dst, src->jmp.type == Jret0 ? Ref::none() : remap_ref(src->jmp.arg, m)});
+ dst->jmp.type = Jjmp;
+ dst->jmp.arg = Ref::none();
+ dst->s1 = b_post;
+ dst->s2 = nullptr;
+ } else {
+ dst->jmp.type = src->jmp.type;
+ dst->jmp.arg = remap_ref(src->jmp.arg, m);
+ dst->s1 = src->s1 ? m.blk[src->s1->id] : nullptr;
+ dst->s2 = src->s2 ? m.blk[src->s2->id] : nullptr;
+ }
+ // pred/rpo/dom etc. left for fillcfg to rebuild
+ (void)caller;
+}
+
+void splice_one(Ctx* ctx, Fn* caller, Blk* b, int call_idx, Fn* callee) {
+ Ins* call_ins = &b->ins[call_idx];
+ Ref call_to = call_ins->to;
+ int call_cls = static_cast<int>(call_ins->cls);
+ (void)call_cls;
+
+ // Collect arg_refs: scan backwards until non-isarg
+ int arg_start = call_idx;
+ while (arg_start > 0 && isarg(b->ins[arg_start - 1].op))
+ arg_start--;
+ std::vector<Ref> arg_refs;
+ arg_refs.reserve(static_cast<size_t>(call_idx - arg_start));
+ for (int k = arg_start; k < call_idx; k++)
+ arg_refs.push_back(b->ins[k].arg[0]);
+
+ // Three remap tables
+ RemapTables m;
+ m.tmp.assign(static_cast<size_t>(callee->ntmp), Ref::none());
+ m.con.assign(static_cast<size_t>(callee->ncon), Ref::none());
+ m.blk.assign(static_cast<size_t>(callee->nblk), nullptr);
+
+ // Tmp: 0=none, [1..R] skipped (reg slots), use newtmp. User tmp starts at 32.
+ constexpr int kFirstUserTmp =
+ 0; // rcc IRGen does not use reg-tmp reservations, starts at 0; tmp[0]=R
+ for (int t = 1; t < callee->ntmp; t++) {
+ const char* prfx = callee->tmp[t].name ? callee->tmp[t].name : "il";
+ m.tmp[static_cast<size_t>(t)] = newtmp(ctx, prfx, callee->tmp[t].cls, caller);
+ }
+ (void)kFirstUserTmp;
+ // Con: 0=undef (fixed), 1=const_zero (rcc convention), others deep-copy
+ m.con[0] = Ref::con(0);
+ for (int c = 1; c < callee->ncon; c++)
+ m.con[static_cast<size_t>(c)] = newcon(ctx, &callee->con[c], caller);
+ // Blk: all newblk. inline_module is called before run_midend, callee->rpo not built yet,
+ // traverse start->link chain; blk->id from IRGen used as remap key.
+ static int splice_seq = 0;
+ std::vector<Blk*> callee_blks; // Align with traversal order for phase 4
+ callee_blks.reserve(callee->nblk);
+ for (Blk* cb = callee->start; cb; cb = cb->link) {
+ Blk* nb = newblk(ctx);
+ char buf[256];
+ std::snprintf(buf, sizeof(buf), "i%d.%s", splice_seq, cb->name ? cb->name : "blk");
+ nb->name = strf(ctx, PFn, "%s", buf);
+ nb->id = caller->nblk++;
+ assert(cb->id < callee->nblk);
+ m.blk[cb->id] = nb;
+ callee_blks.push_back(cb);
+ }
+ ++splice_seq;
+
+ // Cut b: new b_post, takes over b->ins[call_idx+1..] / b->jmp / s1/s2 / link
+ Blk* b_post = newblk(ctx);
+ {
+ static int post_seq = 0;
+ char buf[64];
+ std::snprintf(buf, sizeof(buf), "iret.%d", post_seq++);
+ b_post->name = strf(ctx, PFn, "%s", buf);
+ b_post->id = caller->nblk++;
+ }
+ b_post->phi = nullptr;
+ // Take over tail ins
+ int tail_n = static_cast<int>(b->nins) - call_idx - 1;
+ if (tail_n > 0) {
+ for (int k = call_idx + 1; k < static_cast<int>(b->nins); k++)
+ addins(ctx, &b_post->ins, &b_post->nins, &b->ins[k]);
+ }
+ b_post->jmp = b->jmp;
+ b_post->s1 = b->s1;
+ b_post->s2 = b->s2;
+ Blk* original_b_link = b->link;
+ // Truncate b to b_pre (up to arg_start), jmp = Jjmp to callee.start_copy
+ b->nins = static_cast<uint>(arg_start);
+ b->jmp.type = Jjmp;
+ b->jmp.arg = Ref::none();
+ b->s1 = m.blk[callee->start->id];
+ b->s2 = nullptr;
+
+ // Deep copy each callee blk
+ std::vector<RetEntry> rets;
+ for (Blk* src : callee_blks) {
+ Blk* dst = m.blk[src->id];
+ clone_blk_body(ctx, caller, src, dst, m, arg_refs, b_post, rets);
+ }
+
+ // If not void, insert phi at head of b_post to merge ret value
+ if (!req(call_to, Ref::none()) && !rets.empty()) {
+ Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ np->to = call_to;
+ np->cls = static_cast<short>(call_ins->cls);
+ np->visit = 0;
+ np->narg = static_cast<uint>(rets.size());
+ np->arg = static_cast<Ref*>(vnew(ctx, np->narg, sizeof(Ref), PFn));
+ np->blk = static_cast<Blk**>(vnew(ctx, np->narg, sizeof(Blk*), PFn));
+ for (size_t k = 0; k < rets.size(); k++) {
+ np->arg[k] = rets[k].val;
+ np->blk[k] = rets[k].blk;
+ }
+ np->link = b_post->phi;
+ b_post->phi = np;
+ }
+
+ // Fix external phi refs: phi inputs in original b's successors from b -> b_post
+ auto fix_phis_in = [&](Blk* succ) {
+ if (!succ)
+ return;
+ for (Phi* p = succ->phi; p; p = p->link)
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == b)
+ p->blk[k] = b_post;
+ };
+ fix_phis_in(b_post->s1);
+ fix_phis_in(b_post->s2);
+
+ // Link chain: b -> callee blks -> b_post -> original_b_link
+ Blk* last_callee_blk = nullptr;
+ b->link = m.blk[callee->start->id]; // start_copy immediately follows b
+ for (Blk* src : callee_blks) {
+ Blk* cur = m.blk[src->id];
+ if (last_callee_blk)
+ last_callee_blk->link = cur;
+ last_callee_blk = cur;
+ }
+ assert(last_callee_blk);
+ last_callee_blk->link = b_post;
+ b_post->link = original_b_link;
+}
+
+// Pass driver
+
+bool inline_pass(Ctx* ctx, std::vector<Fn*>& fns, const std::unordered_set<Fn*>& in_cycle) {
+ auto call_counts = collect_call_counts(fns);
+ bool any_change = false;
+
+ for (Fn* caller : fns) {
+ for (Blk* b = caller->start; b; b = b->link) {
+ // Splice modifies b->ins and b->link. After a splice, b becomes b_pre, and tail ins go
+ // to b_post. We only scan each b once, breaking on hit (remaining calls handled in next
+ // fixpoint round, ensuring b->ins pointer stability).
+ bool spliced_in_b = false;
+ for (int k = 0; k < static_cast<int>(b->nins); k++) {
+ if (b->ins[k].op != Ocall)
+ continue;
+ Fn* callee = lookup_callee(b->ins[k].arg[0], caller, fns);
+ if (!callee)
+ continue; // extern (sylib)
+ if (callee == caller)
+ continue; // Direct self-call
+ if (callee->vararg)
+ continue;
+ if (in_cycle.count(callee))
+ continue; // Direct/indirect recursion: cannot be callee
+ int cost = count_real_ins(callee);
+ // const-arg discount: scan backward [arg_start..k-1] for Oarg with RCon
+ int arg_start = k;
+ while (arg_start > 0 && isarg(b->ins[arg_start - 1].op))
+ arg_start--;
+ int const_args = 0;
+ for (int j = arg_start; j < k; j++)
+ if (rtype(b->ins[j].arg[0]) == RCon)
+ const_args++;
+ cost = std::max(0, cost - const_args * kConstArgDiscount);
+ bool single = false;
+ auto it = call_counts.find(callee->name);
+ if (it != call_counts.end() && it->second == 1)
+ single = true;
+ int threshold;
+ if (single)
+ threshold = kInlineSingleCallerThreshold;
+ else if (is_leaf(callee))
+ threshold = kInlineLeafThreshold;
+ else
+ threshold = kInlineNonLeafThreshold;
+ if (cost > threshold)
+ continue;
+
+ splice_one(ctx, caller, b, k, callee);
+ any_change = true;
+ spliced_in_b = true;
+ if (debug['I']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> inline %s into %s (cost=%d, threshold=%d)\n", callee->name,
+ caller->name, cost, threshold);
+ }
+ break;
+ }
+ if (spliced_in_b) {
+ // b is now b_pre, followed by callee blks and b_post. Continue safely handles this.
+ continue;
+ }
+ }
+ }
+ return any_change;
+}
+
+} // namespace
+
+void inline_module(Ctx* ctx, std::vector<Fn*>& fns) {
+ auto graph = build_call_graph(fns);
+ auto in_cycle = find_cycle_members(fns, graph);
+ if (debug['I']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> inline_module: call-graph %zu fn, %zu in cycle\n", fns.size(),
+ in_cycle.size());
+ }
+ int iter = 0;
+ for (; iter < kFixpointMaxIters; iter++) {
+ if (!inline_pass(ctx, fns, in_cycle))
+ break;
+ }
+ if (debug['I']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> inline_module: %d round%s\n", iter, iter == 1 ? "" : "s");
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/licm.cc b/src/mid/licm.cc
new file mode 100644
index 0000000..87d61a3
--- /dev/null
+++ b/src/mid/licm.cc
@@ -0,0 +1,213 @@
+/* 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 licm.cc
+/// @brief Phase A Loop Invariant Code Motion (LICM).
+///
+/// See docs/licm.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+struct LoopInfo {
+ Blk* header;
+ std::vector<Blk*> body; // includes header
+ Blk* preheader; // nullptr if no unique non-back-edge predecessor
+};
+
+Ins make_nop() {
+ Ins i = {};
+ i.op = Onop;
+ return i;
+}
+
+// Loop Identification.
+void build_loops(Fn* fn, std::vector<LoopInfo>& loops, std::vector<int>& blk_loop) {
+ loops.clear();
+ blk_loop.assign(static_cast<size_t>(fn->nblk), -1);
+
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = ~0u;
+
+ auto refine_inner = [&](uint bid, int lidx) {
+ int cur = blk_loop[bid];
+ if (cur < 0 || loops[static_cast<size_t>(cur)].header->id <
+ loops[static_cast<size_t>(lidx)].header->id)
+ blk_loop[bid] = lidx;
+ };
+
+ for (uint i = 0; i < fn->nblk; i++) {
+ Blk* hd = fn->rpo[i];
+ bool has_back = false;
+ for (uint k = 0; k < hd->npred; k++) {
+ if (hd->pred[k]->id >= i) {
+ has_back = true;
+ break;
+ }
+ }
+ if (!has_back)
+ continue;
+
+ int lidx = static_cast<int>(loops.size());
+ loops.push_back({hd, {hd}, nullptr});
+ refine_inner(hd->id, lidx);
+
+ // DFS body collect.
+ std::vector<Blk*> stack;
+ for (uint k = 0; k < hd->npred; k++)
+ if (hd->pred[k]->id >= i)
+ stack.push_back(hd->pred[k]);
+
+ while (!stack.empty()) {
+ Blk* n = stack.back();
+ stack.pop_back();
+ if (n->id < hd->id || n->visit == hd->id)
+ continue;
+ n->visit = hd->id;
+ loops[static_cast<size_t>(lidx)].body.push_back(n);
+ refine_inner(n->id, lidx);
+ for (uint k = 0; k < n->npred; k++)
+ stack.push_back(n->pred[k]);
+ }
+ }
+
+ // Find preheader.
+ for (auto& L : loops) {
+ Blk* phdr = nullptr;
+ bool unique = true;
+ for (uint k = 0; k < L.header->npred; k++) {
+ Blk* p = L.header->pred[k];
+ bool in_body = false;
+ for (Blk* b : L.body)
+ if (b == p) {
+ in_body = true;
+ break;
+ }
+ if (in_body)
+ continue; // back-edge
+ if (phdr) {
+ unique = false;
+ break;
+ }
+ phdr = p;
+ }
+ L.preheader = unique ? phdr : nullptr;
+ }
+}
+
+// Invariance
+bool blk_in(Blk* x, const std::vector<Blk*>& body) {
+ for (Blk* b : body)
+ if (b == x)
+ return true;
+ return false;
+}
+
+bool is_invariant(Fn* fn, Ref r, const LoopInfo& L) {
+ if (rtype(r) == RCon)
+ return true;
+ if (rtype(r) != RTmp)
+ return false;
+ Tmp* t = &fn->tmp[r.val];
+ if (t->bid == ~0u)
+ return true; // Undefined (treat as param/global-like) - equivalent to "outside loop"
+ return !blk_in(fn->rpo[t->bid], L.body);
+}
+
+// Check safety.
+bool clobbered_in_loop(Fn* fn, Ins* load_ins, Ref addr, int sz, const LoopInfo& L) {
+ for (Blk* b : L.body) {
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ if (i == load_ins)
+ continue;
+ if (i->op == Ocall)
+ return true; // Conservative: call is treated as writing all escapable memory
+ if (isstore(i->op)) {
+ int ssz = storesz(i);
+ int delta;
+ if (alias(addr, 0, sz, i->arg[1], ssz, &delta, fn) != NoAlias)
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+} // namespace
+
+void licm(Ctx* ctx, Fn* fn) {
+ fillloop(fn);
+
+ // Phase A hoisted load.
+ int total_hoisted = 0;
+ int iter = 0;
+ for (; iter < 16; iter++) {
+ std::vector<LoopInfo> loops;
+ std::vector<int> blk_loop;
+ build_loops(fn, loops, blk_loop);
+ if (loops.empty())
+ break;
+
+ int round_hoisted = 0;
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (b->id >= blk_loop.size())
+ continue;
+ int lidx = blk_loop[b->id];
+ if (lidx < 0)
+ continue;
+ LoopInfo& L = loops[static_cast<size_t>(lidx)];
+ if (!L.preheader)
+ continue;
+
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ if (!isload(i->op))
+ continue;
+ if (!is_invariant(fn, i->arg[0], L))
+ continue;
+ int sz = loadsz(i);
+ if (clobbered_in_loop(fn, i, i->arg[0], sz, L))
+ continue;
+
+ // Hit: move to preheader.
+ addins(ctx, &L.preheader->ins, &L.preheader->nins, i);
+ *i = make_nop();
+ round_hoisted++;
+ }
+ }
+
+ total_hoisted += round_hoisted;
+ if (round_hoisted == 0)
+ break;
+ // Reset loop state.
+ filluse(ctx, fn);
+ }
+
+ if (debug['L']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "\n> LICM hoisted %d load(s) in %s (in %d round%s)\n", total_hoisted, fn->name,
+ iter, iter == 1 ? "" : "s");
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/live.cc b/src/mid/live.cc
new file mode 100644
index 0000000..ee718ad
--- /dev/null
+++ b/src/mid/live.cc
@@ -0,0 +1,198 @@
+/* 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 live.cc
+/// @brief Backward dataflow liveness analysis.
+///
+/// See docs/live.md for architectural notes.
+
+#include "rcc/all.h"
+
+namespace rcc {
+
+void liveon(BSet* v, Blk* b, Blk* s) {
+ bscopy(v, s->in);
+
+ // s's phi-defs.
+ for (Phi* p = s->phi; p; p = p->link)
+ if (rtype(p->to) == RTmp)
+ bsclr(v, p->to.val);
+
+ // phi-arg edge check.
+ for (Phi* p = s->phi; p; p = p->link) {
+ for (uint n = 0; n < p->narg; n++) {
+ if (p->blk[n] != b)
+ continue;
+ if (rtype(p->arg[n]) != RTmp)
+ continue;
+ uint t = p->arg[n].val;
+ bsset(v, t);
+ bsset(b->gen, t);
+ }
+ }
+}
+
+namespace {
+
+/// Add ref (if tmp) to b->gen and b->in; on first entry to b->in, increment live count for its K
+/// bucket.
+inline void use_ref(Ref r, Blk* b, int nlv[2], Tmp* tmp) {
+ if (rtype(r) != RTmp)
+ return;
+ uint t = r.val;
+ bsset(b->gen, t);
+ if (!bshas(b->in, t)) {
+ nlv[KBASE(tmp[t].cls)]++;
+ bsset(b->in, t);
+ }
+}
+
+/// Get maximum of two elements.
+inline int imax(int a, int b) {
+ return a > b ? a : b;
+}
+
+} // namespace
+
+void filllive(Ctx* ctx, Fn* fn) {
+ BSet prev_out[1];
+ BSet edge_set[1];
+
+ bsinit(ctx, prev_out, static_cast<uint>(fn->ntmp));
+ bsinit(ctx, edge_set, static_cast<uint>(fn->ntmp));
+
+ // Initialize sets.
+ for (Blk* b = fn->start; b; b = b->link) {
+ bsinit(ctx, b->in, static_cast<uint>(fn->ntmp));
+ bsinit(ctx, b->out, static_cast<uint>(fn->ntmp));
+ bsinit(ctx, b->gen, static_cast<uint>(fn->ntmp));
+ }
+
+ // Fixpoint loop.
+ bool changed = true;
+ while (changed) {
+ changed = false;
+
+ // Reverse rpo scan.
+ for (int n = static_cast<int>(fn->nblk) - 1; n >= 0; n--) {
+ Blk* b = fn->rpo[n];
+
+ // Merge successors liveon.
+ bscopy(prev_out, b->out);
+
+ if (b->s1) {
+ liveon(edge_set, b, b->s1);
+ bsunion(b->out, edge_set);
+ }
+ if (b->s2) {
+ liveon(edge_set, b, b->s2);
+ bsunion(b->out, edge_set);
+ }
+ if (!bsequal(b->out, prev_out))
+ changed = true;
+
+ // Initial in.
+ int nlv[2] = {0, 0};
+ b->out->t[0] |= T.rglob;
+ bscopy(b->in, b->out);
+ for (int t = 0; bsiter(b->in, &t); t++)
+ nlv[KBASE(fn->tmp[t].cls)]++;
+
+ // Terminal jump args.
+ if (rtype(b->jmp.arg) == RCall) {
+ assert(static_cast<int>(bscount(b->in)) == T.nrglob && b->in->t[0] == T.rglob);
+ b->in->t[0] |= T.retregs(b->jmp.arg, nlv);
+ } else {
+ use_ref(b->jmp.arg, b, nlv, fn->tmp);
+ }
+ // Fused cmp+jnz check.
+ use_ref(b->jmp.arg2, b, nlv, fn->tmp);
+
+ // Initial nlive.
+ for (int k = 0; k < 2; k++)
+ b->nlive[k] = nlv[k];
+
+ // Reverse scan instructions.
+ for (int idx = static_cast<int>(b->nins) - 1; idx >= 0; idx--) {
+ Ins* i = &b->ins[idx];
+
+ // Post-ABI lowering call.
+ if (i->op == Ocall && rtype(i->arg[1]) == RCall) {
+ int call_kill[2];
+ b->in->t[0] &= ~T.retregs(i->arg[1], call_kill);
+ for (int k = 0; k < 2; k++) {
+ nlv[k] -= call_kill[k];
+ // Midpoint call.
+ nlv[k] += T.nrsave[k];
+ b->nlive[k] = imax(b->nlive[k], nlv[k]);
+ }
+ int call_arg[2];
+ b->in->t[0] |= T.argregs(i->arg[1], call_arg);
+ for (int k = 0; k < 2; k++) {
+ nlv[k] -= T.nrsave[k];
+ nlv[k] += call_arg[k];
+ }
+ }
+
+ // def kill.
+ if (!req(i->to, Ref::none())) {
+ assert(rtype(i->to) == RTmp);
+ uint t = i->to.val;
+ if (bshas(b->in, t))
+ nlv[KBASE(fn->tmp[t].cls)]--;
+ bsset(b->gen, t);
+ bsclr(b->in, t);
+ }
+
+ // args gen.
+ for (int k = 0; k < 2; k++) {
+ Ref a = i->arg[k];
+ if (rtype(a) == RMem) {
+ Mem* m = &fn->mem[a.val];
+ use_ref(m->base, b, nlv, fn->tmp);
+ use_ref(m->index, b, nlv, fn->tmp);
+ } else {
+ use_ref(a, b, nlv, fn->tmp);
+ }
+ }
+
+ // Update peak.
+ for (int k = 0; k < 2; k++)
+ b->nlive[k] = imax(b->nlive[k], nlv[k]);
+ }
+ }
+ }
+
+ // Debug dump.
+ if (debug['L']) {
+ FILE* f = ctx->dbg_buf;
+ fprintf(f, "\n> Liveness analysis:\n");
+ for (Blk* b = fn->start; b; b = b->link) {
+ fprintf(f, "\t%-10sin: ", b->name);
+ dumpts(b->in, fn->tmp, f);
+ fprintf(f, "\t out: ");
+ dumpts(b->out, fn->tmp, f);
+ fprintf(f, "\t gen: ");
+ dumpts(b->gen, fn->tmp, f);
+ fprintf(f, "\t live: %d %d\n", b->nlive[0], b->nlive[1]);
+ }
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/load.cc b/src/mid/load.cc
new file mode 100644
index 0000000..a26f847
--- /dev/null
+++ b/src/mid/load.cc
@@ -0,0 +1,556 @@
+/* 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 load.cc
+/// @brief Load/store meta information + load-forwarding optimization (loadopt).
+///
+/// See docs/load.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cstdlib>
+
+namespace rcc {
+
+// loadsz / storesz - also used by alias.cc / mem.cc
+
+int loadsz(Ins* l) {
+ switch (l->op) {
+ case Oloadsb:
+ case Oloadub:
+ return 1;
+ case Oloadsh:
+ case Oloaduh:
+ return 2;
+ case Oloadsw:
+ case Oloaduw:
+ return 4;
+ case Oload:
+ return KWIDE(l->cls) ? 8 : 4;
+ }
+ die("loadsz: not a load op");
+}
+
+int storesz(Ins* s) {
+ switch (s->op) {
+ case Ostoreb:
+ return 1;
+ case Ostoreh:
+ return 2;
+ case Ostorew:
+ case Ostores:
+ return 4;
+ case Ostorel:
+ case Ostored:
+ return 8;
+ }
+ die("storesz: not a store op");
+}
+
+namespace {
+
+struct Slice {
+ Ref ref;
+ int off;
+ short sz;
+ short cls;
+};
+
+struct Loc {
+ enum Type { LRoot, LLoad, LNoLoad } type;
+ uint off;
+ Blk* blk;
+};
+
+} // namespace
+
+struct Insert {
+ uint isphi : 1;
+ uint num : 31;
+ uint bid;
+ uint off;
+ Ins ins; // valid when !isphi
+ Slice slice; // valid when isphi
+ Phi* phi_node; // valid when isphi
+};
+
+namespace {
+
+// MASK(w): All-1 mask of width w bytes. Relies on unsigned overflow for w==8.
+inline bits mskbits(int w) {
+ return (BIT(8 * w - 1) << 1) - 1;
+}
+
+// Insert a deferred instruction, returning new tmp
+Ref iins(Ctx* ctx, int cls, int op, Ref a0, Ref a1, Loc* l) {
+ vgrow(ctx, &ctx->load_ilog, ++ctx->load_nlog);
+ Insert* ist = &ctx->load_ilog[ctx->load_nlog - 1];
+ ist->isphi = 0;
+ ist->num = ctx->load_inum++;
+ ist->bid = l->blk->id;
+ ist->off = l->off;
+ Ref to = newtmp(ctx, "ld", cls, ctx->load_curf);
+ ist->ins.op = static_cast<uint>(op);
+ ist->ins.cls = static_cast<uint>(cls);
+ ist->ins.to = to;
+ ist->ins.arg[0] = a0;
+ ist->ins.arg[1] = a1;
+ return to;
+}
+
+// Cast *r to cls, possibly inserting multiple Ocast / Oextuw
+void cast_ref(Ctx* ctx, Ref* r, int cls, Loc* l) {
+ if (rtype(*r) == RCon)
+ return;
+ assert(rtype(*r) == RTmp);
+ int cls0 = ctx->load_curf->tmp[r->val].cls;
+ if (cls0 == cls || (cls == Kw && cls0 == Kl))
+ return;
+ if (KWIDE(cls0) < KWIDE(cls)) {
+ if (cls0 == Ks)
+ *r = iins(ctx, Kw, Ocast, *r, Ref::none(), l);
+ *r = iins(ctx, Kl, Oextuw, *r, Ref::none(), l);
+ if (cls == Kd)
+ *r = iins(ctx, Kd, Ocast, *r, Ref::none(), l);
+ } else {
+ if (cls0 == Kd && cls != Kl)
+ *r = iins(ctx, Kl, Ocast, *r, Ref::none(), l);
+ if (cls0 != Kd || cls != Kw)
+ *r = iins(ctx, cls, Ocast, *r, Ref::none(), l);
+ }
+}
+
+void mask_ref(Ctx* ctx, int cls, Ref* r, bits msk, Loc* l) {
+ cast_ref(ctx, r, cls, l);
+ *r = iins(ctx, cls, Oand, *r, getcon(ctx, static_cast<int64_t>(msk), ctx->load_curf), l);
+}
+
+// emit_load: Insert an actual load (fallback when forwarding fails)
+Ref emit_load(Ctx* ctx, Slice sl, bits msk, Loc* l) {
+ int ld;
+ switch (sl.sz) {
+ case 1:
+ ld = Oloadub;
+ break;
+ case 2:
+ ld = Oloaduh;
+ break;
+ case 4:
+ ld = Oloaduw;
+ break;
+ case 8:
+ ld = Oload;
+ break;
+ default:
+ die("loadopt: emit_load on slice size %d", sl.sz);
+ }
+ bits all_msk = mskbits(sl.sz);
+ bool all = (msk == all_msk);
+ int cls = all ? sl.cls : (sl.sz > 4 ? Kl : Kw);
+
+ Ref r = sl.ref;
+ if (rtype(r) == RTmp) {
+ Alias* a = &ctx->load_curf->tmp[r.val].alias;
+ // offset_unknown aliases will not match via slice - invariant broken if reached
+ assert(!a->offset_unknown);
+ switch (a->type) {
+ case Alias::ALoc:
+ case Alias::AEsc:
+ case Alias::AUnk:
+ r = Ref::tmp(static_cast<uint>(a->base));
+ if (a->offset) {
+ Ref r1 = getcon(ctx, a->offset, ctx->load_curf);
+ r = iins(ctx, Kl, Oadd, r, r1, l);
+ }
+ break;
+ case Alias::ACon:
+ case Alias::ASym: {
+ Con c;
+ memset(&c, 0, sizeof c);
+ c.type = Con::CAddr;
+ c.sym = a->u.sym;
+ c.bits.i = a->offset;
+ r = newcon(ctx, &c, ctx->load_curf);
+ break;
+ }
+ default:
+ die("loadopt: emit_load on alias type %d", a->type);
+ }
+ }
+ r = iins(ctx, cls, ld, r, Ref::none(), l);
+ if (!all)
+ mask_ref(ctx, cls, &r, msk, l);
+ return r;
+}
+
+// rebase: Replace sl.ref with alias base + off
+void rebase(Ctx* ctx, Slice* sl) {
+ if (rtype(sl->ref) != RTmp)
+ return;
+ Alias* a = &ctx->load_curf->tmp[sl->ref.val].alias;
+ // offset_unknown aliases cannot be rebased
+ if (a->offset_unknown)
+ return;
+ if (static_cast<int16_t>(a->offset) != a->offset)
+ return;
+ if (a->type != Alias::ALoc && a->type != Alias::AEsc && a->type != Alias::AUnk)
+ return;
+ sl->ref = Ref::tmp(static_cast<uint>(a->base));
+ sl->off = static_cast<int>(a->offset);
+}
+
+// killsl: whether r is sl's base tmp (only valid for stack type)
+bool killsl(Ctx* ctx, Ref r, Slice sl) {
+ if (rtype(sl.ref) != RTmp)
+ return false;
+ Alias* a = &ctx->load_curf->tmp[sl.ref.val].alias;
+ switch (a->type) {
+ case Alias::ALoc:
+ case Alias::AEsc:
+ case Alias::AUnk:
+ return req(Ref::tmp(static_cast<uint>(a->base)), r);
+ case Alias::ACon:
+ case Alias::ASym:
+ return false;
+ default:
+ die("loadopt: killsl on alias type %d", a->type);
+ }
+}
+
+// qsort comparator: bid asc -> phi before ins -> off asc -> num asc
+int icmp(const void* pa, const void* pb) {
+ const Insert* a = static_cast<const Insert*>(pa);
+ const Insert* b = static_cast<const Insert*>(pb);
+ int c;
+ if ((c = static_cast<int>(a->bid) - static_cast<int>(b->bid)))
+ return c;
+ if (a->isphi && b->isphi)
+ return 0;
+ if (a->isphi)
+ return -1;
+ if (b->isphi)
+ return 1;
+ if ((c = static_cast<int>(a->off) - static_cast<int>(b->off)))
+ return c;
+ return static_cast<int>(a->num) - static_cast<int>(b->num);
+}
+
+Ref def(Ctx* ctx, Slice sl, bits msk, Blk* b, Ins* i, Loc* il);
+
+Ref try_def(Ctx* ctx, Slice sl, bits msk, Blk* b, Ins* i, Loc* il) {
+ // CFG traversal invariants
+ assert(dom(b, il->blk));
+
+ if (!i)
+ i = &b->ins[b->nins];
+ int cls = (sl.sz > 4) ? Kl : Kw;
+ bits msks = mskbits(sl.sz);
+
+ // Scan backward in block
+ while (i > b->ins) {
+ --i;
+ if (killsl(ctx, i->to, sl))
+ return Ref::none();
+ if (i->op == Ocall && escapes(sl.ref, ctx->load_curf))
+ return Ref::none();
+
+ bool is_load = isload(i->op);
+ int sz;
+ Ref r1, r;
+ if (is_load) {
+ sz = loadsz(i);
+ r1 = i->arg[0];
+ r = i->to;
+ } else if (isstore(i->op)) {
+ sz = storesz(i);
+ r1 = i->arg[1];
+ r = i->arg[0];
+ } else if (i->op == Oblit1) {
+ assert(rtype(i->arg[0]) == RInt);
+ sz = std::abs(rsval(i->arg[0]));
+ assert(i > b->ins);
+ --i;
+ assert(i->op == Oblit0);
+ r1 = i->arg[1];
+ r = Ref::none(); // blit does not use r
+ } else {
+ continue;
+ }
+
+ int off;
+ switch (alias(sl.ref, sl.off, sl.sz, r1, sz, &off, ctx->load_curf)) {
+ case MustAlias: {
+ Slice sl1 = sl;
+ if (i->op == Oblit0) {
+ // adjust blit source slice
+ sl1.ref = i->arg[0];
+ if (off >= 0) {
+ assert(off < sz);
+ sl1.off = off;
+ sz -= off;
+ off = 0;
+ } else {
+ sl1.off = 0;
+ sl1.sz = static_cast<short>(sl1.sz + off);
+ }
+ if (sz > sl1.sz)
+ sz = sl1.sz;
+ assert(sz <= 8);
+ sl1.sz = static_cast<short>(sz);
+ }
+
+ bits msk1;
+ int op;
+ if (off < 0) {
+ off = -off;
+ msk1 = (mskbits(sz) << (8 * off)) & msks;
+ op = Oshl;
+ } else {
+ msk1 = (mskbits(sz) >> (8 * off)) & msks;
+ op = Oshr;
+ }
+ if ((msk1 & msk) == 0)
+ continue;
+
+ if (i->op == Oblit0) {
+ r = def(ctx, sl1, mskbits(static_cast<int>(sl1.sz)), b, i, il);
+ if (req(r, Ref::none()))
+ return Ref::none();
+ }
+
+ if (off) {
+ int cls1 = cls;
+ if (op == Oshr && off + sl.sz > 4)
+ cls1 = Kl;
+ cast_ref(ctx, &r, cls1, il);
+ Ref r2 = getcon(ctx, 8 * off, ctx->load_curf);
+ r = iins(ctx, cls1, op, r, r2, il);
+ }
+ if ((msk1 & msk) != msk1 || off + sz < sl.sz)
+ mask_ref(ctx, cls, &r, msk1 & msk, il);
+ if ((msk & ~msk1) != 0) {
+ Ref r2 = def(ctx, sl, msk & ~msk1, b, i, il);
+ if (req(r2, Ref::none()))
+ return Ref::none();
+ r = iins(ctx, cls, Oor, r, r2, il);
+ }
+ if (msk == msks)
+ cast_ref(ctx, &r, sl.cls, il);
+ return r;
+ }
+ case MayAlias:
+ if (is_load)
+ continue;
+ return Ref::none();
+ case NoAlias:
+ continue;
+ default:
+ die("loadopt: alias() returned unexpected value");
+ }
+ }
+
+ // Block head: check if ilog already has a phi for this block and slice
+ for (Insert* ist = ctx->load_ilog; ist < &ctx->load_ilog[ctx->load_nlog]; ++ist) {
+ if (!ist->isphi || ist->bid != b->id)
+ continue;
+ if (!req(ist->slice.ref, sl.ref))
+ continue;
+ if (ist->slice.off != sl.off)
+ continue;
+ if (ist->slice.sz != sl.sz)
+ continue;
+ Ref r = ist->phi_node->to;
+ if (msk != msks)
+ mask_ref(ctx, cls, &r, msk, il);
+ else
+ cast_ref(ctx, &r, sl.cls, il);
+ return r;
+ }
+
+ // Check if real phi kills sl
+ for (Phi* p = b->phi; p; p = p->link)
+ if (killsl(ctx, p->to, sl))
+ return Ref::none();
+
+ // Predecessors
+ if (b->npred == 0)
+ return Ref::none();
+ if (b->npred == 1) {
+ Blk* bp = b->pred[0];
+ assert(bp->loop >= il->blk->loop);
+ Loc l = *il;
+ if (bp->s2)
+ l.type = Loc::LNoLoad;
+ Ref r1 = def(ctx, sl, msk, bp, nullptr, &l);
+ if (req(r1, Ref::none()))
+ return Ref::none();
+ return r1;
+ }
+
+ // Multiple predecessors: insert new phi into log, recurse on predecessors
+ Ref r = newtmp(ctx, "ld", sl.cls, ctx->load_curf);
+ Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ memset(p, 0, sizeof *p);
+ vgrow(ctx, &ctx->load_ilog, ++ctx->load_nlog);
+ Insert* ist = &ctx->load_ilog[ctx->load_nlog - 1];
+ ist->isphi = 1;
+ ist->bid = b->id;
+ ist->slice = sl;
+ ist->phi_node = p;
+ p->to = r;
+ p->cls = static_cast<short>(sl.cls);
+ p->narg = b->npred;
+ p->arg = static_cast<Ref*>(vnew(ctx, p->narg, sizeof(Ref), PFn));
+ p->blk = static_cast<Blk**>(vnew(ctx, p->narg, sizeof(Blk*), PFn));
+ for (uint np = 0; np < b->npred; ++np) {
+ Blk* bp = b->pred[np];
+ Loc l;
+ if (!bp->s2 && il->type != Loc::LNoLoad && bp->loop < il->blk->loop)
+ l.type = Loc::LLoad;
+ else
+ l.type = Loc::LNoLoad;
+ l.blk = bp;
+ l.off = bp->nins;
+ Ref r1 = def(ctx, sl, msks, bp, nullptr, &l);
+ if (req(r1, Ref::none()))
+ return Ref::none();
+ p->arg[np] = r1;
+ p->blk[np] = bp;
+ // XXX QBE note "multiplicity in predecessors" - same bp in b->pred multiple times not
+ // handled.
+ }
+ if (msk != msks)
+ mask_ref(ctx, cls, &r, msk, il);
+ return r;
+}
+
+Ref def(Ctx* ctx, Slice sl, bits msk, Blk* b, Ins* i, Loc* il) {
+ uint oldl = ctx->load_nlog;
+ int oldt = ctx->load_curf->ntmp;
+ Ref r = try_def(ctx, sl, msk, b, i, il);
+ if (!req(r, Ref::none()))
+ return r;
+ // Rollback log and fresh tmp (vgrow does not shrink, capacity kept for reuse)
+ ctx->load_nlog = oldl;
+ ctx->load_curf->ntmp = oldt;
+ if (il->type != Loc::LLoad)
+ return Ref::none();
+ return emit_load(ctx, sl, msk, il);
+}
+
+} // namespace
+
+void loadopt(Ctx* ctx, Fn* fn) {
+ ctx->load_curf = fn;
+ ctx->load_ilog = static_cast<Insert*>(vnew(ctx, 0, sizeof(Insert), PHeap));
+ ctx->load_nlog = 0;
+ ctx->load_inum = 0;
+
+ // solve each load
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) {
+ if (!isload(i->op))
+ continue;
+ int sz = loadsz(i);
+ Slice sl;
+ sl.ref = i->arg[0];
+ sl.off = 0;
+ sl.sz = static_cast<short>(sz);
+ sl.cls = static_cast<short>(i->cls);
+ Loc l;
+ l.type = Loc::LRoot;
+ l.off = static_cast<uint>(i - b->ins);
+ l.blk = b;
+ rebase(ctx, &sl);
+ i->arg[1] = def(ctx, sl, mskbits(sz), b, i, &l);
+ }
+ }
+
+ // sort and rebuild each block
+ qsort(ctx->load_ilog, ctx->load_nlog, sizeof(Insert), icmp);
+ vgrow(ctx, &ctx->load_ilog, ctx->load_nlog + 1);
+ ctx->load_ilog[ctx->load_nlog].bid = fn->nblk; // sentinel: stops ist->bid == n check
+
+ Ins* ib = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PHeap));
+ Insert* ist = ctx->load_ilog;
+ for (uint n = 0; n < fn->nblk; ++n) {
+ Blk* b = fn->rpo[n];
+
+ // Push block's phi inserts to b->phi head
+ for (; ist->bid == n && ist->isphi; ++ist) {
+ ist->phi_node->link = b->phi;
+ b->phi = ist->phi_node;
+ }
+
+ uint ni = 0;
+ uint nt = 0;
+ for (;;) {
+ Ins* i;
+ if (ist->bid == n && ist->off == ni) {
+ i = &ist->ins;
+ ++ist;
+ } else {
+ if (ni == b->nins)
+ break;
+ i = &b->ins[ni++];
+ if (isload(i->op) && !req(i->arg[1], Ref::none())) {
+ // Solved load -> rewrite op + arg
+ int ext = Oextsb + (static_cast<int>(i->op) - Oloadsb);
+ switch (i->op) {
+ case Oloadsb:
+ case Oloadub:
+ case Oloadsh:
+ case Oloaduh:
+ i->op = static_cast<uint>(ext);
+ break;
+ case Oloadsw:
+ case Oloaduw:
+ if (i->cls == Kl) {
+ i->op = static_cast<uint>(ext);
+ break;
+ }
+ // fall through: under Kw sub-word is equivalent to full word -> direct
+ // copy
+ [[fallthrough]];
+ case Oload:
+ i->op = Ocopy;
+ break;
+ default:
+ die("loadopt: unreachable in solved-load rewrite");
+ }
+ i->arg[0] = i->arg[1];
+ i->arg[1] = Ref::none();
+ }
+ }
+ vgrow(ctx, &ib, ++nt);
+ ib[nt - 1] = *i;
+ }
+ idup(ctx, b, ib, nt);
+ }
+ vfree(ib);
+ vfree(ctx->load_ilog);
+ ctx->load_ilog = nullptr;
+
+ if (debug['M']) {
+ fprintf(ctx->dbg_buf, "\n> After load elimination:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/loop_idiom.cc b/src/mid/loop_idiom.cc
new file mode 100644
index 0000000..8cbbf00
--- /dev/null
+++ b/src/mid/loop_idiom.cc
@@ -0,0 +1,401 @@
+/* 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 loop_idiom.cc
+/// @brief Recognize zero-fill / array-copy inner loops -> rewrite to runtime Ocall.
+///
+/// See docs/loop_idiom.md for architectural notes.
+
+#include "rcc/all.h"
+#include "rcc/loopinfo.h"
+
+#include <algorithm>
+#include <cstring>
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+// store op -> byte width; non-store returns 0
+int store_bytes(int op) {
+ switch (op) {
+ case Ostoreb:
+ return 1;
+ case Ostoreh:
+ return 2;
+ case Ostorew:
+ case Ostores:
+ return 4;
+ case Ostorel:
+ case Ostored:
+ return 8;
+ default:
+ return 0;
+ }
+}
+
+int load_bytes(int op, int cls) {
+ switch (op) {
+ case Oloadsb:
+ case Oloadub:
+ return 1;
+ case Oloadsh:
+ case Oloaduh:
+ return 2;
+ case Oloadsw:
+ case Oloaduw:
+ return 4;
+ case Oload:
+ return (cls == Kl || cls == Kd) ? 8 : 4;
+ default:
+ return 0;
+ }
+}
+
+// Check byte splat.
+bool is_byte_splat(Fn* fn, Ref v, uint8_t* byte_out) {
+ if (rtype(v) != RCon)
+ return false;
+ Con* c = &fn->con[v.val];
+ if (c->type != Con::CBits || c->flt)
+ return false;
+ uint64_t bits = static_cast<uint64_t>(c->bits.i);
+ uint8_t b = bits & 0xff;
+ uint64_t splat = static_cast<uint64_t>(b);
+ splat |= splat << 8;
+ splat |= splat << 16;
+ splat |= splat << 32;
+ if (bits != splat)
+ return false;
+ *byte_out = b;
+ return true;
+}
+
+// Trace Ocopy chain back to a non-Ocopy root Ref.
+Ref base_root(Fn* fn, Ref r) {
+ for (int hop = 0; hop < 16; hop++) {
+ Ins* d = def_ins(fn, r);
+ if (!d || d->op != Ocopy)
+ break;
+ r = d->arg[0];
+ }
+ return r;
+}
+
+// Find IV increment index.
+int find_iv_inc_idx(Blk* latch, Ref next) {
+ for (uint k = 0; k < latch->nins; k++)
+ if (req(latch->ins[k].to, next))
+ return static_cast<int>(k);
+ return -1;
+}
+
+enum class Idiom { None, Memset, Memcpy, Memmove };
+
+struct Match {
+ Idiom kind;
+ Ref base_dst;
+ Ref base_src; // memcpy/memmove only
+ uint8_t value; // memset only
+ Ref end_dst; // for size = end - base
+ Blk* after;
+};
+
+bool try_match_loop(Fn* fn, const Loop& L, Match* out) {
+ // Validate loop structure.
+ if (L.body.size() > 2)
+ return false;
+ // Disallow nesting.
+
+ // Primary pointer IV (dst pointer for memset/memcpy)
+ PointerIV iv_dst{};
+ Phi* phi_dst = nullptr;
+ int found = 0;
+ for (Phi* ph = L.header->phi; ph; ph = ph->link) {
+ PointerIV cand{};
+ if (pick_pointer_iv(fn, L, ph, &cand)) {
+ iv_dst = cand;
+ phi_dst = ph;
+ found++;
+ }
+ }
+ if (found == 0)
+ return false;
+ // 1 IV -> memset candidate; 2 -> memcpy candidate; >2 -> reject
+ PointerIV iv_src{};
+ Phi* phi_src = nullptr;
+ if (found >= 2) {
+ if (found != 2)
+ return false;
+ // Rescan to get the two
+ std::vector<std::pair<PointerIV, Phi*>> ivs;
+ for (Phi* ph = L.header->phi; ph; ph = ph->link) {
+ PointerIV cand{};
+ if (pick_pointer_iv(fn, L, ph, &cand))
+ ivs.push_back({cand, ph});
+ }
+ if (ivs.size() != 2)
+ return false;
+ // Will decide which is dst when scanning store in body
+ iv_dst = ivs[0].first;
+ phi_dst = ivs[0].second;
+ iv_src = ivs[1].first;
+ phi_src = ivs[1].second;
+ }
+
+ // exit cmp must be based on iv_dst (memset) or either one (memcpy)
+ Ref end_dst;
+ Blk* after = nullptr;
+ if (!check_exit_cmp(fn, L, iv_dst.phi_to, &end_dst, &after)) {
+ if (phi_src && check_exit_cmp(fn, L, iv_src.phi_to, &end_dst, &after)) {
+ // Reversed: cmp based on src, swap dst/src naming
+ std::swap(iv_dst, iv_src);
+ std::swap(phi_dst, phi_src);
+ } else {
+ return false;
+ }
+ }
+ if (iv_dst.step != iv_src.step && phi_src)
+ return false;
+
+ // Find IV self-update ins (add in latch)
+ int iv_dst_inc = find_iv_inc_idx(L.latch, iv_dst.next);
+ int iv_src_inc = phi_src ? find_iv_inc_idx(L.latch, iv_src.next) : -1;
+ if (iv_dst_inc < 0)
+ return false;
+ if (phi_src && iv_src_inc < 0)
+ return false;
+
+ // Body scan.
+ Ins* load_ins = nullptr;
+ Ins* store_ins = nullptr;
+ for (Blk* bb : L.body) {
+ for (uint k = 0; k < bb->nins; k++) {
+ Ins* i = &bb->ins[k];
+ int op = i->op;
+ // Allowed: exit cmp (in header), IV self-update (in latch)
+ if (bb == L.header && req(i->to, L.header->jmp.arg))
+ continue; // exit cmp
+ if (bb == L.latch &&
+ (static_cast<int>(k) == iv_dst_inc || static_cast<int>(k) == iv_src_inc))
+ continue;
+ // Disallowed: call / alloc / FP / div / vastart / vaarg / dbgloc / sel / blit
+ if (op == Ocall || op == Oalloc4 || op == Oalloc8 || op == Oalloc16 || op == Ovaarg ||
+ op == Ovastart || op == Odbgloc || op == Osel0 || op == Osel1 || op == Oblit0 ||
+ op == Oblit1 || op == Osalloc)
+ return false;
+ if (op == Oudiv || op == Ourem || op == Odiv || op == Orem)
+ return false;
+ if (op == Ocopy && (i->cls == Ks || i->cls == Kd))
+ return false;
+ // load / store
+ int lb = load_bytes(op, static_cast<int>(i->cls));
+ int sb = store_bytes(op);
+ if (lb > 0) {
+ if (load_ins)
+ return false; // Reject multiple loads
+ load_ins = i;
+ continue;
+ }
+ if (sb > 0) {
+ if (store_ins)
+ return false;
+ store_ins = i;
+ continue;
+ }
+ // Other allowed.
+ if (op == Oadd || op == Osub || op == Omul || op == Oand || op == Oor || op == Oxor ||
+ op == Oshl || op == Oshr || op == Osar || op == Oextsw || op == Oextuw ||
+ op == Oextsh || op == Oextuh || op == Oextsb || op == Oextub || op == Ocopy ||
+ op == Onop)
+ continue;
+ // Other cmp (besides LFTR cmp): conservatively reject
+ return false;
+ }
+ }
+
+ if (!store_ins)
+ return false;
+ int sw = store_bytes(store_ins->op);
+ if (sw != iv_dst.step)
+ return false;
+ if (!req(store_ins->arg[1], iv_dst.phi_to))
+ return false;
+
+ if (has_outside_use(fn, L))
+ return false;
+
+ if (load_ins) {
+ // memcpy path
+ if (!phi_src)
+ return false;
+ int lw = load_bytes(load_ins->op, static_cast<int>(load_ins->cls));
+ if (lw != sw)
+ return false;
+ if (!req(load_ins->arg[0], iv_src.phi_to))
+ return false;
+ if (!req(store_ins->arg[0], load_ins->to))
+ return false;
+ // Noalias check.
+ Ref root_dst = base_root(fn, iv_dst.base);
+ Ref root_src = base_root(fn, iv_src.base);
+ out->kind = req(root_dst, root_src) ? Idiom::Memmove : Idiom::Memcpy;
+ out->base_dst = iv_dst.base;
+ out->base_src = iv_src.base;
+ out->value = 0;
+ out->end_dst = end_dst;
+ out->after = after;
+ return true;
+ }
+
+ // memset path
+ if (phi_src)
+ return false; // Second IV present but no load, abnormal shape
+ uint8_t bv = 0;
+ if (!is_byte_splat(fn, store_ins->arg[0], &bv))
+ return false;
+ out->kind = Idiom::Memset;
+ out->base_dst = iv_dst.base;
+ out->base_src = Ref::none();
+ out->value = bv;
+ out->end_dst = end_dst;
+ out->after = after;
+ return true;
+}
+
+const char* runtime_name(Idiom k) {
+ switch (k) {
+ case Idiom::Memset:
+ return "__rcc_memset";
+ case Idiom::Memcpy:
+ return "__rcc_memcpy";
+ case Idiom::Memmove:
+ return "__rcc_memmove";
+ default:
+ return "";
+ }
+}
+
+Ref make_runtime_callee(Ctx* ctx, Fn* fn, const char* name) {
+ Con c;
+ std::memset(&c, 0, sizeof(c));
+ c.type = Con::CAddr;
+ c.sym.type = Sym::SGlo;
+ c.sym.id = intern(const_cast<char*>(name));
+ return newcon(ctx, &c, fn);
+}
+
+void execute_rewrite(Ctx* ctx, Fn* fn, const Loop& L, const Match& m) {
+ Blk* pre = L.preheader;
+
+ // size = end - base (Osub Kl)
+ Ref size_ref = newtmp(ctx, "midiom", Kl, fn);
+ Ins sub_ins{};
+ sub_ins.op = Osub;
+ sub_ins.cls = Kl;
+ sub_ins.to = size_ref;
+ sub_ins.arg[0] = m.end_dst;
+ sub_ins.arg[1] = m.base_dst;
+ addins(ctx, &pre->ins, &pre->nins, &sub_ins);
+
+ // Oarg dst (Kl)
+ Ins arg0{};
+ arg0.op = Oarg;
+ arg0.cls = Kl;
+ arg0.to = Ref::none();
+ arg0.arg[0] = m.base_dst;
+ arg0.arg[1] = Ref::none();
+ addins(ctx, &pre->ins, &pre->nins, &arg0);
+
+ // Oarg val/src
+ Ins arg1{};
+ arg1.op = Oarg;
+ arg1.to = Ref::none();
+ arg1.arg[1] = Ref::none();
+ if (m.kind == Idiom::Memset) {
+ arg1.cls = Kw;
+ arg1.arg[0] = getcon(ctx, static_cast<int64_t>(m.value), fn);
+ } else {
+ arg1.cls = Kl;
+ arg1.arg[0] = m.base_src;
+ }
+ addins(ctx, &pre->ins, &pre->nins, &arg1);
+
+ // Oarg size (Kl)
+ Ins arg2{};
+ arg2.op = Oarg;
+ arg2.cls = Kl;
+ arg2.to = Ref::none();
+ arg2.arg[0] = size_ref;
+ arg2.arg[1] = Ref::none();
+ addins(ctx, &pre->ins, &pre->nins, &arg2);
+
+ // Ocall (returns void -> cls=Kw, to=none)
+ Ins call_ins{};
+ call_ins.op = Ocall;
+ call_ins.cls = Kw;
+ call_ins.to = Ref::none();
+ call_ins.arg[0] = make_runtime_callee(ctx, fn, runtime_name(m.kind));
+ call_ins.arg[1] = Ref::none();
+ addins(ctx, &pre->ins, &pre->nins, &call_ins);
+
+ fn->leaf = 0;
+
+ // change preheader to jump directly to after
+ pre->jmp.type = Jjmp;
+ pre->jmp.arg = Ref::none();
+ pre->jmp.arg2 = Ref::none();
+ pre->s1 = m.after;
+ pre->s2 = nullptr;
+}
+
+} // namespace
+
+void loop_idiom(Ctx* ctx, Fn* fn) {
+ std::vector<Loop> loops;
+ find_loops(fn, loops);
+ if (loops.empty())
+ return;
+
+ // Sort innermost first.
+ std::sort(loops.begin(), loops.end(),
+ [](const Loop& a, const Loop& b) { return a.body.size() < b.body.size(); });
+
+ std::unordered_set<uint> consumed_blk;
+ for (const Loop& L : loops) {
+ bool clash = false;
+ for (Blk* bb : L.body)
+ if (consumed_blk.count(bb->id)) {
+ clash = true;
+ break;
+ }
+ if (clash)
+ continue;
+ Match m{};
+ if (!try_match_loop(fn, L, &m))
+ continue;
+ execute_rewrite(ctx, fn, L, m);
+ for (Blk* bb : L.body)
+ consumed_blk.insert(bb->id);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/loopinfo.cc b/src/mid/loopinfo.cc
new file mode 100644
index 0000000..d6041fc
--- /dev/null
+++ b/src/mid/loopinfo.cc
@@ -0,0 +1,226 @@
+/* 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 loopinfo.cc
+/// @brief find_loops + detect_bivs + ref_is_const (shared).
+
+#include "rcc/loopinfo.h"
+
+#include "rcc/all.h"
+
+namespace rcc {
+
+bool ref_is_const(Fn* fn, Ref r, int64_t& out) {
+ if (rtype(r) != RCon)
+ return false;
+ if (r.val >= static_cast<uint>(fn->ncon))
+ return false;
+ Con* c = &fn->con[r.val];
+ if (c->type != Con::CBits || c->flt)
+ return false;
+ out = c->bits.i;
+ return true;
+}
+
+Ins* def_ins(Fn* fn, Ref r) {
+ if (rtype(r) != RTmp || r.val >= static_cast<uint>(fn->ntmp))
+ return nullptr;
+ return fn->tmp[r.val].def;
+}
+
+void find_loops(Fn* fn, std::vector<Loop>& out) {
+ out.clear();
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = ~0u;
+
+ for (uint i = 0; i < fn->nblk; i++) {
+ Blk* hd = fn->rpo[i];
+ if (hd->npred != 2)
+ continue;
+
+ Blk* pre = nullptr;
+ Blk* latch = nullptr;
+ for (uint k = 0; k < hd->npred; k++) {
+ Blk* p = hd->pred[k];
+ if (p->id != ~0u && p->id >= hd->id)
+ latch = p;
+ else
+ pre = p;
+ }
+ if (!pre || !latch)
+ continue;
+
+ Loop L;
+ L.header = hd;
+ L.preheader = pre;
+ L.latch = latch;
+ L.body.push_back(hd);
+ L.body_bid.insert(hd->id);
+ hd->visit = hd->id;
+ std::vector<Blk*> stk = {latch};
+ while (!stk.empty()) {
+ Blk* n = stk.back();
+ stk.pop_back();
+ if (n->visit == hd->id)
+ continue;
+ n->visit = hd->id;
+ L.body.push_back(n);
+ L.body_bid.insert(n->id);
+ for (uint k = 0; k < n->npred; k++)
+ stk.push_back(n->pred[k]);
+ }
+ out.push_back(std::move(L));
+ }
+}
+
+void detect_bivs(Fn* fn, const Loop& L, std::vector<Biv>& out) {
+ for (Phi* p = L.header->phi; p; p = p->link) {
+ if (p->narg != 2)
+ continue;
+ Ref init_ref = Ref::none(), next_ref = Ref::none();
+ for (uint k = 0; k < p->narg; k++) {
+ if (p->blk[k] == L.preheader)
+ init_ref = p->arg[k];
+ else if (p->blk[k] == L.latch)
+ next_ref = p->arg[k];
+ }
+ int64_t init_v = 0;
+ if (!ref_is_const(fn, init_ref, init_v))
+ continue;
+ Ins* nx = def_ins(fn, next_ref);
+ if (!nx || nx->op != Oadd)
+ continue;
+ int64_t step_v = 0;
+ if (req(nx->arg[0], p->to) && ref_is_const(fn, nx->arg[1], step_v)) {
+ } else if (req(nx->arg[1], p->to) && ref_is_const(fn, nx->arg[0], step_v)) {
+ } else {
+ continue;
+ }
+ if (step_v == 0)
+ continue;
+ out.push_back({p->to, static_cast<int>(p->cls), init_v, step_v});
+ }
+}
+
+bool pick_pointer_iv(Fn* fn, const Loop& L, Phi* p, PointerIV* out) {
+ if (p->cls != Kl)
+ return false;
+ if (p->narg != 2)
+ return false;
+ Ref base = Ref::none(), next = Ref::none();
+ for (uint k = 0; k < p->narg; k++) {
+ if (p->blk[k] == L.preheader)
+ base = p->arg[k];
+ else if (p->blk[k] == L.latch)
+ next = p->arg[k];
+ else
+ return false;
+ }
+ if (rtype(base) == -1 || rtype(next) == -1)
+ return false;
+ Ins* nx = def_ins(fn, next);
+ if (!nx || nx->op != Oadd)
+ return false;
+ int64_t step = 0;
+ if (req(nx->arg[0], p->to) && ref_is_const(fn, nx->arg[1], step)) {
+ } else if (req(nx->arg[1], p->to) && ref_is_const(fn, nx->arg[0], step)) {
+ } else {
+ return false;
+ }
+ if (step <= 0)
+ return false;
+ out->phi_to = p->to;
+ out->base = base;
+ out->next = next;
+ out->step = step;
+ return true;
+}
+
+bool check_exit_cmp(Fn* fn, const Loop& L, Ref p, Ref* end_out, Blk** after_out) {
+ Blk* hd = L.header;
+ if (hd->jmp.type != Jjnz)
+ return false;
+ Ref cmp_r = hd->jmp.arg;
+ if (rtype(cmp_r) != RTmp)
+ return false;
+ Ins* cmp_def = nullptr;
+ for (uint k = 0; k < hd->nins; k++) {
+ Ins* ii = &hd->ins[k];
+ if (req(ii->to, cmp_r)) {
+ cmp_def = ii;
+ break;
+ }
+ }
+ if (!cmp_def)
+ return false;
+ if (cmp_def->op != Ocultl && cmp_def->op != Ocsltl)
+ return false;
+ if (!req(cmp_def->arg[0], p))
+ return false;
+ Ref end = cmp_def->arg[1];
+ if (rtype(end) == RTmp) {
+ Ins* d = def_ins(fn, end);
+ if (d) {
+ for (Blk* bb : L.body)
+ if (bb->ins <= d && d < bb->ins + bb->nins)
+ return false;
+ }
+ }
+ Blk* s1 = hd->s1;
+ Blk* s2 = hd->s2;
+ bool s1_in = L.body_bid.count(s1->id) != 0;
+ bool s2_in = L.body_bid.count(s2->id) != 0;
+ if (s1_in == s2_in)
+ return false;
+ *end_out = end;
+ *after_out = s1_in ? s2 : s1;
+ return true;
+}
+
+bool has_outside_use(Fn* fn, const Loop& L) {
+ std::unordered_set<uint> loop_def;
+ for (Blk* bb : L.body) {
+ for (Phi* ph = bb->phi; ph; ph = ph->link)
+ if (rtype(ph->to) == RTmp)
+ loop_def.insert(ph->to.val);
+ for (uint k = 0; k < bb->nins; k++)
+ if (rtype(bb->ins[k].to) == RTmp)
+ loop_def.insert(bb->ins[k].to.val);
+ }
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (L.body_bid.count(b->id))
+ continue;
+ for (Phi* ph = b->phi; ph; ph = ph->link)
+ for (uint k = 0; k < ph->narg; k++)
+ if (rtype(ph->arg[k]) == RTmp && loop_def.count(ph->arg[k].val))
+ return true;
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ for (int a = 0; a < 2; a++)
+ if (rtype(i->arg[a]) == RTmp && loop_def.count(i->arg[a].val))
+ return true;
+ }
+ if (rtype(b->jmp.arg) == RTmp && loop_def.count(b->jmp.arg.val))
+ return true;
+ if (rtype(b->jmp.arg2) == RTmp && loop_def.count(b->jmp.arg2.val))
+ return true;
+ }
+ return false;
+}
+
+} // namespace rcc
diff --git a/src/mid/loopswap.cc b/src/mid/loopswap.cc
new file mode 100644
index 0000000..cd8acf0
--- /dev/null
+++ b/src/mid/loopswap.cc
@@ -0,0 +1,469 @@
+/* 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 loopswap.cc
+/// @brief Integer perfect 2-level nested loop interchange.
+///
+/// See docs/loopswap.md for architectural notes.
+
+#include "rcc/all.h"
+#include "rcc/loopinfo.h"
+
+#include <cstdlib>
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+// Helper: reaches
+bool reaches(Fn* fn, Ref r, Ref target, std::unordered_set<uint>& visiting, int budget) {
+ if (req(r, target))
+ return true;
+ if (budget <= 0)
+ return false;
+ if (rtype(r) != RTmp || r.val >= static_cast<uint>(fn->ntmp))
+ return false;
+ if (!visiting.insert(r.val).second)
+ return true;
+
+ Ins* def = fn->tmp[r.val].def;
+ if (def && def->op == Ocopy) {
+ bool ok = reaches(fn, def->arg[0], target, visiting, budget - 1);
+ visiting.erase(r.val);
+ return ok;
+ }
+ for (Blk* bb = fn->start; bb; bb = bb->link) {
+ for (Phi* p = bb->phi; p; p = p->link) {
+ if (!req(p->to, r))
+ continue;
+ bool ok = true;
+ for (uint k = 0; k < p->narg; k++) {
+ if (req(p->arg[k], r))
+ continue;
+ if (!reaches(fn, p->arg[k], target, visiting, budget - 1)) {
+ ok = false;
+ break;
+ }
+ }
+ visiting.erase(r.val);
+ return ok;
+ }
+ }
+ visiting.erase(r.val);
+ return false;
+}
+
+bool carries(Fn* fn, Ref r, Ref target) {
+ std::unordered_set<uint> v;
+ return reaches(fn, r, target, v, 64);
+}
+
+// Affine decomposition
+struct Aff {
+ int64_t outer_c;
+ int64_t inner_c;
+};
+
+Aff affine(Fn* fn, Ref r, Ref outer_to, Ref inner_to, int depth) {
+ if (depth > 24)
+ return {0, 0};
+ if (rtype(r) == RCon)
+ return {0, 0};
+ if (rtype(r) != RTmp)
+ return {0, 0};
+ if (carries(fn, r, outer_to))
+ return {1, 0};
+ if (carries(fn, r, inner_to))
+ return {0, 1};
+ if (r.val >= static_cast<uint>(fn->ntmp))
+ return {0, 0};
+ Ins* def = fn->tmp[r.val].def;
+ if (!def)
+ return {0, 0};
+ switch (def->op) {
+ case Oadd: {
+ Aff a = affine(fn, def->arg[0], outer_to, inner_to, depth + 1);
+ Aff b = affine(fn, def->arg[1], outer_to, inner_to, depth + 1);
+ return {a.outer_c + b.outer_c, a.inner_c + b.inner_c};
+ }
+ case Osub: {
+ Aff a = affine(fn, def->arg[0], outer_to, inner_to, depth + 1);
+ Aff b = affine(fn, def->arg[1], outer_to, inner_to, depth + 1);
+ return {a.outer_c - b.outer_c, a.inner_c - b.inner_c};
+ }
+ case Omul: {
+ int64_t c;
+ if (ref_is_const(fn, def->arg[1], c)) {
+ Aff a = affine(fn, def->arg[0], outer_to, inner_to, depth + 1);
+ return {a.outer_c * c, a.inner_c * c};
+ }
+ if (ref_is_const(fn, def->arg[0], c)) {
+ Aff a = affine(fn, def->arg[1], outer_to, inner_to, depth + 1);
+ return {a.outer_c * c, a.inner_c * c};
+ }
+ return {0, 0};
+ }
+ case Oextsw:
+ case Ocopy:
+ return affine(fn, def->arg[0], outer_to, inner_to, depth + 1);
+ default:
+ return {0, 0};
+ }
+}
+
+// Perfect nest: L1.body \ L2.body \ {L1.header, L1.latch} must only contain empty Jjmp blocks.
+bool perfect_nest(const Loop& L1, const Loop& L2) {
+ for (Blk* bb : L1.body) {
+ if (bb == L1.header || bb == L1.latch)
+ continue;
+ if (L2.body_bid.count(bb->id))
+ continue;
+ if (bb->nins != 0)
+ return false;
+ if (bb->jmp.type != Jjmp)
+ return false;
+ }
+ return true;
+}
+
+// Does the cmp ins for header.jmp.arg reference bv.to (or its carrier equiv)?
+int find_header_cmp(Fn* fn, const Loop& L, const Biv& bv) {
+ Ref a = L.header->jmp.arg;
+ if (rtype(a) != RTmp)
+ return -1;
+ for (uint k = 0; k < L.header->nins; k++) {
+ Ins* ii = &L.header->ins[k];
+ if (!req(ii->to, a))
+ continue;
+ if (carries(fn, ii->arg[0], bv.to) || carries(fn, ii->arg[1], bv.to))
+ return static_cast<int>(k);
+ return -1;
+ }
+ return -1;
+}
+
+// Find index of BIV's latch increment ins (the Oadd) in latch->ins array.
+int find_latch_inc(const Loop& L, const Biv& bv) {
+ Phi* P = nullptr;
+ for (Phi* p = L.header->phi; p; p = p->link)
+ if (req(p->to, bv.to)) {
+ P = p;
+ break;
+ }
+ if (!P)
+ return -1;
+ Ref next = Ref::none();
+ for (uint k = 0; k < P->narg; k++)
+ if (P->blk[k] == L.latch) {
+ next = P->arg[k];
+ break;
+ }
+ if (rtype(next) != RTmp)
+ return -1;
+ for (uint k = 0; k < L.latch->nins; k++)
+ if (req(L.latch->ins[k].to, next) && L.latch->ins[k].op == Oadd)
+ return static_cast<int>(k);
+ return -1;
+}
+
+Phi* find_phi(Blk* b, Ref to) {
+ for (Phi* p = b->phi; p; p = p->link)
+ if (req(p->to, to))
+ return p;
+ return nullptr;
+}
+
+void unlink_phi(Blk* b, Phi* t) {
+ for (Phi** cur = &b->phi; *cur; cur = &(*cur)->link)
+ if (*cur == t) {
+ *cur = t->link;
+ t->link = nullptr;
+ return;
+ }
+}
+
+void prepend_phi(Blk* b, Phi* p) {
+ p->link = b->phi;
+ b->phi = p;
+}
+
+void rewrite_phi_blk(Phi* p, Blk* from, Blk* to) {
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == from)
+ p->blk[k] = to;
+}
+
+void rewrite_uses(Fn* fn, Ref from, Ref to) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Phi* p = b->phi; p; p = p->link)
+ for (uint k = 0; k < p->narg; k++)
+ if (req(p->arg[k], from))
+ p->arg[k] = to;
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (req(i->arg[0], from))
+ i->arg[0] = to;
+ if (req(i->arg[1], from))
+ i->arg[1] = to;
+ }
+ if (req(b->jmp.arg, from))
+ b->jmp.arg = to;
+ if (req(b->jmp.arg2, from))
+ b->jmp.arg2 = to;
+ }
+}
+
+struct AccessRec {
+ Ref addr;
+ int64_t inner_s;
+ bool is_store;
+};
+
+bool try_swap(Ctx* ctx, Fn* fn, const Loop& L1, const Loop& L2) {
+ if (!perfect_nest(L1, L2))
+ return false;
+
+ std::vector<Biv> bs1, bs2;
+ detect_bivs(fn, L1, bs1);
+ detect_bivs(fn, L2, bs2);
+ if (bs1.empty() || bs2.empty())
+ return false;
+
+ // Pick the BIV referenced by header.cmp (SysY loops typically compare one BIV).
+ Biv b1{}, b2{};
+ int cmp1 = -1, cmp2 = -1;
+ for (auto& bv : bs1) {
+ int idx = find_header_cmp(fn, L1, bv);
+ if (idx >= 0) {
+ b1 = bv;
+ cmp1 = idx;
+ break;
+ }
+ }
+ for (auto& bv : bs2) {
+ int idx = find_header_cmp(fn, L2, bv);
+ if (idx >= 0) {
+ b2 = bv;
+ cmp2 = idx;
+ break;
+ }
+ }
+ if (cmp1 < 0 || cmp2 < 0)
+ return false;
+ if ((b1.cls != Kw && b1.cls != Kl) || (b2.cls != Kw && b2.cls != Kl))
+ return false;
+
+ int inc1 = find_latch_inc(L1, b1);
+ int inc2 = find_latch_inc(L2, b2);
+ if (inc1 < 0 || inc2 < 0)
+ return false;
+
+ // Scan L2.body: integer only + decompose addr.
+ int64_t sum_outer = 0, sum_inner = 0;
+ std::vector<AccessRec> mems;
+ for (Blk* bb : L2.body) {
+ for (uint k = 0; k < bb->nins; k++) {
+ Ins* ii = &bb->ins[k];
+ int op = ii->op;
+ if (op == Ocall)
+ return false;
+ if (ii->cls == Ks || ii->cls == Kd)
+ return false;
+ bool is_ld = isload(op);
+ bool is_st = isstore(op);
+ if (!is_ld && !is_st)
+ continue;
+ if (op == Ostores || op == Ostored)
+ return false;
+ Ref addr = is_st ? ii->arg[1] : ii->arg[0];
+ Aff a = affine(fn, addr, b1.to, b2.to, 0);
+ sum_outer += std::abs(a.outer_c);
+ sum_inner += std::abs(a.inner_c);
+ mems.push_back({addr, a.inner_c, is_st});
+ }
+ }
+
+ // accumulator pattern: if there are stores => every store has inner_s==0 and matches a load
+ // with same addr and inner_s==0.
+ bool has_store = false;
+ for (auto& m : mems)
+ if (m.is_store) {
+ has_store = true;
+ break;
+ }
+ if (has_store) {
+ for (auto& s : mems) {
+ if (!s.is_store)
+ continue;
+ if (s.inner_s != 0)
+ return false;
+ bool found = false;
+ for (auto& l : mems) {
+ if (!l.is_store && req(l.addr, s.addr) && l.inner_s == 0) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ return false;
+ }
+ }
+
+ if (sum_inner == 0)
+ return false;
+ if (sum_outer * 4 >= sum_inner)
+ return false;
+
+ // Execute
+ Blk* h1 = L1.header;
+ Blk* h2 = L2.header;
+
+ Phi* P1 = find_phi(h1, b1.to);
+ Phi* P2 = find_phi(h2, b2.to);
+ if (!P1 || !P2)
+ return false;
+
+ // Collect self-loop carrier of b1.to on L2.header (Braun artifact; invalid after swap, needs
+ // removal).
+ std::vector<Phi*> stale;
+ for (Phi* p = h2->phi; p; p = p->link) {
+ if (p->narg != 2)
+ continue;
+ Ref pre = Ref::none();
+ bool is_self = false;
+ for (uint k = 0; k < p->narg; k++) {
+ if (p->blk[k] == L2.preheader)
+ pre = p->arg[k];
+ else if (p->blk[k] == L2.latch && req(p->arg[k], p->to))
+ is_self = true;
+ }
+ if (is_self && carries(fn, pre, b1.to))
+ stale.push_back(p);
+ }
+
+ // Snapshot: copy 4 target ins before swap to avoid distortion.
+ Ins C1 = h1->ins[cmp1];
+ Ins C2 = h2->ins[cmp2];
+ Ins I1 = L1.latch->ins[inc1];
+ Ins I2 = L2.latch->ins[inc2];
+
+ // Swap phi: unlink + rewrite edge + prepend to the other.
+ unlink_phi(h1, P1);
+ unlink_phi(h2, P2);
+ rewrite_phi_blk(P1, L1.preheader, L2.preheader);
+ rewrite_phi_blk(P1, L1.latch, L2.latch);
+ rewrite_phi_blk(P2, L2.preheader, L1.preheader);
+ rewrite_phi_blk(P2, L2.latch, L1.latch);
+ prepend_phi(h1, P2);
+ prepend_phi(h2, P1);
+
+ // Swap cmp ins + jmp descriptor.
+ h1->ins[cmp1].op = Onop;
+ h2->ins[cmp2].op = Onop;
+ addins(ctx, &h1->ins, &h1->nins, &C2);
+ addins(ctx, &h2->ins, &h2->nins, &C1);
+ std::swap(h1->jmp.arg, h2->jmp.arg);
+ std::swap(h1->jmp.cls, h2->jmp.cls);
+ std::swap(h1->jmp.arg2, h2->jmp.arg2);
+
+ // Swap increment.
+ L1.latch->ins[inc1].op = Onop;
+ L2.latch->ins[inc2].op = Onop;
+ addins(ctx, &L1.latch->ins, &L1.latch->nins, &I2);
+ addins(ctx, &L2.latch->ins, &L2.latch->nins, &I1);
+
+ // Invalidate carriers: rewrite_uses to substitute with b1.to, then unlink phi.
+ for (Phi* s : stale) {
+ rewrite_uses(fn, s->to, b1.to);
+ unlink_phi(h2, s);
+ }
+
+ return true;
+}
+
+// L2 is immediate-child of L1: strict subset of body, with no L3 strictly in between.
+bool is_immediate_child(const std::vector<Loop>& loops, size_t i_par, size_t j_chl) {
+ const Loop& par = loops[i_par];
+ const Loop& chl = loops[j_chl];
+ if (chl.body_bid.size() >= par.body_bid.size())
+ return false;
+ for (uint id : chl.body_bid)
+ if (!par.body_bid.count(id))
+ return false;
+ for (size_t m = 0; m < loops.size(); m++) {
+ if (m == i_par || m == j_chl)
+ continue;
+ const Loop& mid = loops[m];
+ if (mid.body_bid.size() <= chl.body_bid.size())
+ continue;
+ if (mid.body_bid.size() >= par.body_bid.size())
+ continue;
+ bool chl_in_mid = true;
+ for (uint id : chl.body_bid)
+ if (!mid.body_bid.count(id)) {
+ chl_in_mid = false;
+ break;
+ }
+ if (!chl_in_mid)
+ continue;
+ bool mid_in_par = true;
+ for (uint id : mid.body_bid)
+ if (!par.body_bid.count(id)) {
+ mid_in_par = false;
+ break;
+ }
+ if (mid_in_par)
+ return false;
+ }
+ return true;
+}
+
+} // namespace
+
+void loopinterchange(Ctx* ctx, Fn* fn) {
+ std::vector<Loop> loops;
+ find_loops(fn, loops);
+ if (loops.size() < 2)
+ return;
+
+ for (size_t i = 0; i < loops.size(); i++) {
+ Loop& L1 = loops[i];
+ Loop* L2 = nullptr;
+ bool amb = false;
+ for (size_t j = 0; j < loops.size(); j++) {
+ if (i == j)
+ continue;
+ if (!is_immediate_child(loops, i, j))
+ continue;
+ if (L2) {
+ amb = true;
+ break;
+ }
+ L2 = &loops[j];
+ }
+ if (!L2 || amb)
+ continue;
+ if (try_swap(ctx, fn, L1, *L2))
+ return; // One swap per pass, CFG/loops are invalidated
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/lsr.cc b/src/mid/lsr.cc
new file mode 100644
index 0000000..0197eb9
--- /dev/null
+++ b/src/mid/lsr.cc
@@ -0,0 +1,1001 @@
+/* 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 lsr.cc
+/// @brief Loop Strength Reduction (LSR).
+///
+/// See docs/lsr.md for architectural notes.
+
+#include "rcc/all.h"
+#include "rcc/loopinfo.h"
+
+#include <cstring>
+#include <functional>
+#include <optional>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+constexpr int kFixpointIters = 4;
+
+// IVs created by reduce_mul / insert_addr_iv.
+struct CreatedIV {
+ Ref to; // header phi.to
+ Ref init_ref; // phi preheader incoming edge Ref
+ int64_t step;
+ int cls;
+ int src_biv;
+ int64_t c;
+};
+
+uint64_t ref_key(Ref r) {
+ return (static_cast<uint64_t>(static_cast<uint>(rtype(r))) << 32) |
+ static_cast<uint32_t>(r.val);
+}
+
+int find_biv(const std::vector<Biv>& bivs, Ref r) {
+ for (size_t k = 0; k < bivs.size(); k++)
+ if (req(bivs[k].to, r))
+ return static_cast<int>(k);
+ return -1;
+}
+
+struct MulCand {
+ Blk* b;
+ uint idx;
+ int biv;
+ int64_t c;
+ bool is_kl;
+};
+
+// Returns number of hits.
+int reduce_mul(Ctx* ctx, Fn* fn, const Loop& L, std::vector<Biv>& bivs,
+ std::vector<CreatedIV>& created_ivs) {
+ std::vector<MulCand> cands;
+ for (Blk* b : L.body) {
+ for (uint ix = 0; ix < b->nins; ix++) {
+ Ins* m = &b->ins[ix];
+ if (m->op != Omul)
+ continue;
+
+ int64_t c = 0;
+ Ref tref = Ref::none();
+ if (ref_is_const(fn, m->arg[1], c) && rtype(m->arg[0]) == RTmp)
+ tref = m->arg[0];
+ else if (ref_is_const(fn, m->arg[0], c) && rtype(m->arg[1]) == RTmp)
+ tref = m->arg[1];
+ else
+ continue;
+ if (c == 0 || c == 1)
+ continue;
+
+ int bi = -1;
+ bool is_kl = false;
+
+ int d = find_biv(bivs, tref);
+ if (d >= 0 && static_cast<int>(m->cls) == bivs[static_cast<size_t>(d)].cls) {
+ bi = d;
+ is_kl = (m->cls == Kl);
+ }
+ if (bi < 0 && m->cls == Kl) {
+ Ins* ext = def_ins(fn, tref);
+ if (ext && ext->op == Oextsw) {
+ int e = find_biv(bivs, ext->arg[0]);
+ if (e >= 0 && bivs[static_cast<size_t>(e)].cls == Kw) {
+ bi = e;
+ is_kl = true;
+ }
+ }
+ }
+
+ if (bi < 0)
+ continue;
+ cands.push_back({b, ix, bi, c, is_kl});
+ }
+ }
+ if (cands.empty())
+ return 0;
+
+ struct Made {
+ int biv;
+ int64_t c;
+ bool is_kl;
+ Ref j;
+ int64_t init;
+ int64_t step;
+ };
+ std::vector<Made> made;
+
+ auto get_iv = [&](int bi, int64_t c, bool is_kl) -> Ref {
+ for (auto& md : made)
+ if (md.biv == bi && md.c == c && md.is_kl == is_kl)
+ return md.j;
+
+ const Biv& iv = bivs[static_cast<size_t>(bi)];
+ int cls = is_kl ? Kl : Kw;
+ int64_t j_init = iv.init * c;
+ int64_t j_step = iv.step * c;
+ Ref jt = newtmp(ctx, "lsr", cls, fn);
+ Ref jn = newtmp(ctx, "lsr", cls, fn);
+
+ Ins add{};
+ add.op = Oadd;
+ add.cls = static_cast<uint>(cls) & 3u;
+ add.to = jn;
+ add.arg[0] = jt;
+ add.arg[1] = getcon(ctx, j_step, fn);
+ addins(ctx, &L.latch->ins, &L.latch->nins, &add);
+
+ Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ std::memset(p, 0, sizeof(Phi));
+ p->to = jt;
+ p->cls = static_cast<short>(cls);
+ p->narg = 2;
+ p->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn));
+ p->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn));
+ p->arg[0] = getcon(ctx, j_init, fn);
+ p->blk[0] = L.preheader;
+ p->arg[1] = jn;
+ p->blk[1] = L.latch;
+ p->link = L.header->phi;
+ L.header->phi = p;
+
+ made.push_back({bi, c, is_kl, jt, j_init, j_step});
+ // Fallback: add derived IV to bivs.
+ bivs.push_back({jt, cls, j_init, j_step});
+ // Record for LFTR.
+ Ref j_init_ref = getcon(ctx, j_init, fn);
+ created_ivs.push_back({jt, j_init_ref, j_step, cls, bi, c});
+ return jt;
+ };
+
+ for (auto& cd : cands) {
+ Ref j = get_iv(cd.biv, cd.c, cd.is_kl);
+ Ins* m = &cd.b->ins[cd.idx]; // get_iv might realloc latch->ins, re-fetch here
+ m->op = Ocopy;
+ m->arg[0] = j;
+ m->arg[1] = Ref::none();
+ // m->cls unchanged (matches j)
+ }
+ return static_cast<int>(cands.size());
+}
+
+// Return phi input.
+Ref phi_arg_from(Phi* p, Blk* from) {
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == from)
+ return p->arg[k];
+ return Ref::none();
+}
+
+Phi* find_header_phi(Blk* hd, Ref to) {
+ for (Phi* p = hd->phi; p; p = p->link)
+ if (req(p->to, to))
+ return p;
+ return nullptr;
+}
+
+bool is_pure_arith(int op) {
+ return op == Oadd || op == Osub || op == Omul || op == Oextsw || op == Ocopy;
+}
+
+// Check load invariance.
+bool load_invariant_in_loop(Fn* fn, const Loop& L, Ins* load) {
+ int lsz = loadsz(load);
+ if (lsz <= 0)
+ return false;
+ for (Blk* bb : L.body) {
+ for (uint i = 0; i < bb->nins; i++) {
+ Ins* s = &bb->ins[i];
+ if (s->op == Ocall)
+ return false; // call may clobber any global
+ if (!isstore(s->op))
+ continue;
+ int ssz = storesz(s);
+ int delta_unused = 0;
+ // storeX value, addr → arg[0]=value, arg[1]=addr
+ int a = alias(load->arg[0], 0, lsz, s->arg[1], ssz, &delta_unused, fn);
+ if (a != NoAlias)
+ return false;
+ }
+ }
+ return true;
+}
+
+// SCEV result.
+struct Scev {
+ int64_t step;
+ int src;
+};
+
+int insert_addr_iv(Ctx* ctx, Fn* fn, const Loop& L, const std::vector<Biv>& bivs,
+ std::vector<CreatedIV>& created_ivs) {
+ // Invariance check (memoized)
+ std::unordered_map<uint64_t, int> inv_memo; // -1 uncomputed, 0 false, 1 true
+ std::function<bool(Ref)> is_inv = [&](Ref r) -> bool {
+ if (rtype(r) == RCon)
+ return true;
+ if (rtype(r) != RTmp)
+ return false;
+ uint64_t k = ref_key(r);
+ auto it = inv_memo.find(k);
+ if (it != inv_memo.end())
+ return it->second == 1;
+ inv_memo[k] =
+ -1; // Mark to prevent recursion cycles; non-self-loop phi returning here is false
+ bool result = false;
+ do {
+ uint bid = fn->tmp[r.val].bid;
+ if (L.body_bid.find(bid) == L.body_bid.end()) {
+ // Def not in loop -> invariant
+ result = true;
+ break;
+ }
+ Ins* d = fn->tmp[r.val].def;
+ if (!d) {
+ // phi def check.
+ if (bid != L.header->id) {
+ result = false;
+ break;
+ }
+ Phi* p = find_header_phi(L.header, r);
+ if (!p) {
+ result = false;
+ break;
+ }
+ bool all_ok = true;
+ for (uint i = 0; i < p->narg; i++) {
+ if (req(p->arg[i], p->to))
+ continue; // self-loop
+ if (!is_inv(p->arg[i])) {
+ all_ok = false;
+ break;
+ }
+ }
+ result = all_ok;
+ break;
+ }
+ if (isload(d->op)) {
+ result = load_invariant_in_loop(fn, L, d);
+ break;
+ }
+ if (!is_pure_arith(d->op)) {
+ result = false;
+ break;
+ }
+ bool a0_ok = req(d->arg[0], Ref::none()) || is_inv(d->arg[0]);
+ bool a1_ok = req(d->arg[1], Ref::none()) || is_inv(d->arg[1]);
+ result = a0_ok && a1_ok;
+ } while (false);
+ inv_memo[k] = result ? 1 : 0;
+ return result;
+ };
+
+ // SCEV returns {step, src_biv}.
+ std::unordered_map<uint64_t, std::optional<Scev>> scev_memo;
+ std::function<std::optional<Scev>(Ref)> scev = [&](Ref r) -> std::optional<Scev> {
+ if (rtype(r) == RCon)
+ return Scev{0, -1};
+ if (rtype(r) != RTmp)
+ return std::nullopt;
+ uint64_t k = ref_key(r);
+ auto it = scev_memo.find(k);
+ if (it != scev_memo.end())
+ return it->second;
+ scev_memo[k] = std::nullopt; // prevent recursion cycle
+ std::optional<Scev> ans = std::nullopt;
+
+ auto combine = [](Scev a, Scev b, int sign) -> std::optional<Scev> {
+ int64_t step = a.step + sign * b.step;
+ int src;
+ if (a.step == 0 && b.step == 0)
+ src = -1;
+ else if (a.step == 0)
+ src = b.src;
+ else if (b.step == 0)
+ src = a.src;
+ else if (a.src == b.src)
+ src = a.src;
+ else
+ return std::nullopt;
+ if (step == 0)
+ src = -1;
+ return Scev{step, src};
+ };
+
+ do {
+ int bi = find_biv(bivs, r);
+ if (bi >= 0) {
+ ans = Scev{bivs[static_cast<size_t>(bi)].step, bi};
+ break;
+ }
+ if (is_inv(r)) {
+ ans = Scev{0, -1};
+ break;
+ }
+ Ins* d = fn->tmp[r.val].def;
+ if (!d)
+ break; // non-BIV head phi / non-invariant phi -> fail
+ int64_t cv = 0;
+ switch (d->op) {
+ case Ocopy:
+ ans = scev(d->arg[0]);
+ break;
+ case Oextsw: {
+ ans = scev(d->arg[0]); // sext passthrough, assumes no int32 wrap
+ break;
+ }
+ case Oadd: {
+ auto s0 = scev(d->arg[0]);
+ auto s1 = scev(d->arg[1]);
+ if (s0.has_value() && s1.has_value())
+ ans = combine(*s0, *s1, +1);
+ break;
+ }
+ case Osub: {
+ auto s0 = scev(d->arg[0]);
+ auto s1 = scev(d->arg[1]);
+ if (s0.has_value() && s1.has_value())
+ ans = combine(*s0, *s1, -1);
+ break;
+ }
+ case Omul: {
+ // One side linear, one side constant
+ if (ref_is_const(fn, d->arg[1], cv)) {
+ auto s = scev(d->arg[0]);
+ if (s.has_value()) {
+ int64_t step = s->step * cv;
+ ans = Scev{step, step == 0 ? -1 : s->src};
+ }
+ } else if (ref_is_const(fn, d->arg[0], cv)) {
+ auto s = scev(d->arg[1]);
+ if (s.has_value()) {
+ int64_t step = s->step * cv;
+ ans = Scev{step, step == 0 ? -1 : s->src};
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ } while (false);
+ scev_memo[k] = ans;
+ return ans;
+ };
+
+ // Materialize init.
+ std::unordered_map<uint64_t, Ref> mat_memo;
+ std::function<Ref(Ref)> materialize = [&](Ref r) -> Ref {
+ if (rtype(r) == RCon)
+ return r;
+ uint64_t k = ref_key(r);
+ auto it = mat_memo.find(k);
+ if (it != mat_memo.end())
+ return it->second;
+
+ // BIV -> init constant
+ int bi = find_biv(bivs, r);
+ if (bi >= 0) {
+ Ref c = getcon(ctx, bivs[static_cast<size_t>(bi)].init, fn);
+ mat_memo[k] = c;
+ return c;
+ }
+
+ // Decide based on def block.
+ uint bid = fn->tmp[r.val].bid;
+ if (L.body_bid.find(bid) == L.body_bid.end()) {
+ mat_memo[k] = r;
+ return r;
+ }
+ Ins* d = fn->tmp[r.val].def;
+ if (!d) {
+ // phi in body header.
+ if (bid != L.header->id) {
+ mat_memo[k] =
+ r; // Fallback: shouldn't reach here (filtered by scev), but prevents crash
+ return r;
+ }
+ Phi* p = find_header_phi(L.header, r);
+ if (!p) {
+ mat_memo[k] = r;
+ return r;
+ }
+ Ref pe = phi_arg_from(p, L.preheader);
+ Ref sub = materialize(pe);
+ mat_memo[k] = sub;
+ return sub;
+ }
+
+ // Ins in body: Ocopy/Oextsw/Oadd/Osub/Omul - copy one to preheader
+ Ref a0 = req(d->arg[0], Ref::none()) ? Ref::none() : materialize(d->arg[0]);
+ Ref a1 = req(d->arg[1], Ref::none()) ? Ref::none() : materialize(d->arg[1]);
+
+ if (d->op == Ocopy) {
+ mat_memo[k] = a0;
+ return a0;
+ }
+ if (d->op == Oextsw && rtype(a0) == RCon) {
+ mat_memo[k] = a0; // Kl constant itself is fine
+ return a0;
+ }
+
+ int cls = static_cast<int>(d->cls);
+ Ref nt = newtmp(ctx, "lsr", cls, fn);
+ Ins nins{};
+ nins.op = d->op;
+ nins.cls = static_cast<uint>(cls) & 3u;
+ nins.to = nt;
+ nins.arg[0] = a0;
+ nins.arg[1] = a1;
+ addins(ctx, &L.preheader->ins, &L.preheader->nins, &nins);
+ mat_memo[k] = nt;
+ return nt;
+ };
+
+ // Scan candidates.
+ auto resolve_copy = [&](Ref r) -> Ref {
+ // Trace Ocopy chain.
+ for (int hop = 0; hop < 8; hop++) {
+ Ins* d = def_ins(fn, r);
+ if (!d || d->op != Ocopy)
+ break;
+ r = d->arg[0];
+ }
+ return r;
+ };
+ struct AddrCand {
+ Blk* b;
+ uint idx;
+ int64_t step;
+ int src_biv;
+ uint64_t dedup_key;
+ };
+ std::vector<AddrCand> cands;
+ for (Blk* b : L.body) {
+ for (uint ix = 0; ix < b->nins; ix++) {
+ Ins* m = &b->ins[ix];
+ if (m->op != Oadd || m->cls != Kl)
+ continue;
+ int64_t cb = 0;
+ if (ref_is_const(fn, m->arg[0], cb) || ref_is_const(fn, m->arg[1], cb))
+ continue; // CBits step -> BIV self-increment, skip
+ auto s = scev(m->to);
+ if (!s.has_value() || s->step == 0)
+ continue;
+ Ref a0r = resolve_copy(m->arg[0]);
+ Ref a1r = resolve_copy(m->arg[1]);
+ uint64_t k0 = ref_key(a0r);
+ uint64_t k1 = ref_key(a1r);
+ if (k0 > k1)
+ std::swap(k0, k1);
+ uint64_t dk = (k0 * 1315423911ull) ^ (k1 + 0x9E3779B97F4A7C15ull);
+ cands.push_back({b, ix, s->step, s->src, dk});
+ }
+ }
+ if (cands.empty())
+ return 0;
+
+ // Deduplication + Rewrite
+ struct IVRec {
+ uint64_t key;
+ Ref p;
+ };
+ std::vector<IVRec> ivs;
+
+ auto find_iv = [&](uint64_t key) -> Ref {
+ for (auto& r : ivs)
+ if (r.key == key)
+ return r.p;
+ return Ref::none();
+ };
+
+ int hits = 0;
+ for (auto& cd : cands) {
+ Ref p = find_iv(cd.dedup_key);
+ if (req(p, Ref::none())) {
+ // Materialize base (init at preheader)
+ Ins* m0 = &cd.b->ins[cd.idx];
+ Ref orig_to = m0->to;
+ Ref p_init = materialize(orig_to);
+
+ int cls = Kl;
+ Ref pt = newtmp(ctx, "lsr", cls, fn);
+ Ref pn = newtmp(ctx, "lsr", cls, fn);
+
+ // latch: pn = pt + step
+ Ins add{};
+ add.op = Oadd;
+ add.cls = static_cast<uint>(cls) & 3u;
+ add.to = pn;
+ add.arg[0] = pt;
+ add.arg[1] = getcon(ctx, cd.step, fn);
+ addins(ctx, &L.latch->ins, &L.latch->nins, &add);
+
+ // header: pt = phi(p_init @ pre, pn @ latch)
+ Phi* ph = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ std::memset(ph, 0, sizeof(Phi));
+ ph->to = pt;
+ ph->cls = static_cast<short>(cls);
+ ph->narg = 2;
+ ph->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn));
+ ph->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn));
+ ph->arg[0] = p_init;
+ ph->blk[0] = L.preheader;
+ ph->arg[1] = pn;
+ ph->blk[1] = L.latch;
+ ph->link = L.header->phi;
+ L.header->phi = ph;
+
+ ivs.push_back({cd.dedup_key, pt});
+ p = pt;
+
+ // Record CreatedIV.
+ int64_t c = 0;
+ if (cd.src_biv >= 0) {
+ int64_t bs = bivs[static_cast<size_t>(cd.src_biv)].step;
+ if (bs != 0 && cd.step % bs == 0)
+ c = cd.step / bs;
+ }
+ created_ivs.push_back({pt, p_init, cd.step, Kl, cd.src_biv, c});
+ }
+
+ // Re-fetch pointer.
+ Ins* m = &cd.b->ins[cd.idx];
+ m->op = Ocopy;
+ m->arg[0] = p;
+ m->arg[1] = Ref::none();
+ // m->cls remains Kl
+ hits++;
+ }
+ return hits;
+}
+
+// Exit cmp mapping.
+bool classify_cmp(int op, int& op_w, int& op_w_flip, int& op_l, int& op_l_flip) {
+ auto setpair = [&](int w, int w_flip, int l, int l_flip) {
+ op_w = w;
+ op_w_flip = w_flip;
+ op_l = l;
+ op_l_flip = l_flip;
+ };
+ switch (op) {
+ case Oceqw:
+ case Oceql:
+ setpair(Oceqw, Oceqw, Oceql, Oceql);
+ return true;
+ case Ocnew:
+ case Ocnel:
+ setpair(Ocnew, Ocnew, Ocnel, Ocnel);
+ return true;
+ case Ocsltw:
+ case Ocsltl:
+ setpair(Ocsltw, Ocsgtw, Ocsltl, Ocsgtl);
+ return true;
+ case Ocslew:
+ case Ocslel:
+ setpair(Ocslew, Ocsgew, Ocslel, Ocsgel);
+ return true;
+ case Ocsgtw:
+ case Ocsgtl:
+ setpair(Ocsgtw, Ocsltw, Ocsgtl, Ocsltl);
+ return true;
+ case Ocsgew:
+ case Ocsgel:
+ setpair(Ocsgew, Ocslew, Ocsgel, Ocslel);
+ return true;
+ default:
+ return false;
+ }
+}
+
+int replace_exit_test(Ctx* ctx, Fn* fn, const Loop& L, const std::vector<Biv>& bivs,
+ const std::vector<CreatedIV>& created_ivs) {
+ if (L.header->jmp.type != Jjnz)
+ return 0;
+ Ref cond = L.header->jmp.arg;
+ if (rtype(cond) != RTmp)
+ return 0;
+
+ // cmp must be in header, and nuse==1 (otherwise modifying it affects other places)
+ Ins* cmp = nullptr;
+ uint cmp_idx = 0;
+ for (uint i = 0; i < L.header->nins; i++) {
+ if (req(L.header->ins[i].to, cond)) {
+ cmp = &L.header->ins[i];
+ cmp_idx = i;
+ break;
+ }
+ }
+ if (!cmp)
+ return 0;
+ if (fn->tmp[cond.val].nuse != 1)
+ return 0;
+
+ int op_w, op_w_flip, op_l, op_l_flip;
+ if (!classify_cmp(static_cast<int>(cmp->op), op_w, op_w_flip, op_l, op_l_flip))
+ return 0;
+
+ // Find BIV side + invariant side
+ int bi = find_biv(bivs, cmp->arg[0]);
+ Ref n_ref;
+ bool biv_at_0;
+ if (bi >= 0) {
+ n_ref = cmp->arg[1];
+ biv_at_0 = true;
+ } else {
+ bi = find_biv(bivs, cmp->arg[1]);
+ if (bi < 0)
+ return 0;
+ n_ref = cmp->arg[0];
+ biv_at_0 = false;
+ }
+ // If the other side is also a BIV, LFTR skips (comparison between two IVs)
+ if (find_biv(bivs, n_ref) >= 0)
+ return 0;
+
+ // LFTR Guard.
+ {
+ Ref biv_to = bivs[static_cast<size_t>(bi)].to;
+ Phi* biv_phi = find_header_phi(L.header, biv_to);
+ Ins* self_inc = nullptr;
+ if (biv_phi) {
+ for (uint k = 0; k < biv_phi->narg; k++) {
+ if (biv_phi->blk[k] == L.latch) {
+ Ref a = biv_phi->arg[k];
+ if (rtype(a) == RTmp && a.val < static_cast<uint>(fn->ntmp))
+ self_inc = fn->tmp[a.val].def;
+ break;
+ }
+ }
+ }
+ if (biv_to.val >= static_cast<uint>(fn->ntmp))
+ return 0;
+ Tmp* bt = &fn->tmp[biv_to.val];
+ int extra = 0;
+ for (uint u = 0; u < bt->nuse; u++) {
+ const Use& uu = bt->use[u];
+ if (uu.type == Use::UIns) {
+ if (uu.u.ins == cmp)
+ continue;
+ if (uu.u.ins == self_inc)
+ continue;
+ // Orphans check.
+ Ref user_to = uu.u.ins->to;
+ if (rtype(user_to) == RTmp && user_to.val < static_cast<uint>(fn->ntmp) &&
+ fn->tmp[user_to.val].nuse == 0)
+ continue;
+ extra++;
+ } else if (uu.type == Use::UPhi) {
+ // Count phi use.
+ extra++;
+ } else if (uu.type == Use::UJmp) {
+ extra++;
+ }
+ }
+ if (extra > 0)
+ return 0;
+ }
+
+ // Materialize to preheader.
+ std::unordered_map<uint64_t, int> inv_memo;
+ std::function<bool(Ref)> is_inv = [&](Ref r) -> bool {
+ if (rtype(r) == RCon)
+ return true;
+ if (rtype(r) != RTmp)
+ return false;
+ uint64_t k = ref_key(r);
+ auto it = inv_memo.find(k);
+ if (it != inv_memo.end())
+ return it->second == 1;
+ inv_memo[k] = -1;
+ bool result = false;
+ do {
+ uint bid = fn->tmp[r.val].bid;
+ if (L.body_bid.find(bid) == L.body_bid.end()) {
+ result = true;
+ break;
+ }
+ Ins* d = fn->tmp[r.val].def;
+ if (!d) {
+ if (bid != L.header->id) {
+ result = false;
+ break;
+ }
+ Phi* p = find_header_phi(L.header, r);
+ if (!p) {
+ result = false;
+ break;
+ }
+ bool all_ok = true;
+ for (uint i = 0; i < p->narg; i++) {
+ if (req(p->arg[i], p->to))
+ continue;
+ if (!is_inv(p->arg[i])) {
+ all_ok = false;
+ break;
+ }
+ }
+ result = all_ok;
+ break;
+ }
+ if (isload(d->op)) {
+ result = load_invariant_in_loop(fn, L, d);
+ break;
+ }
+ if (!is_pure_arith(d->op)) {
+ result = false;
+ break;
+ }
+ bool a0_ok = req(d->arg[0], Ref::none()) || is_inv(d->arg[0]);
+ bool a1_ok = req(d->arg[1], Ref::none()) || is_inv(d->arg[1]);
+ result = a0_ok && a1_ok;
+ } while (false);
+ inv_memo[k] = result ? 1 : 0;
+ return result;
+ };
+
+ if (!is_inv(n_ref))
+ return 0;
+
+ // Materialize n_ref to preheader
+ std::unordered_map<uint64_t, Ref> mat_memo;
+ std::function<Ref(Ref)> materialize = [&](Ref r) -> Ref {
+ if (rtype(r) == RCon)
+ return r;
+ uint64_t k = ref_key(r);
+ auto it = mat_memo.find(k);
+ if (it != mat_memo.end())
+ return it->second;
+ uint bid = fn->tmp[r.val].bid;
+ if (L.body_bid.find(bid) == L.body_bid.end()) {
+ mat_memo[k] = r;
+ return r;
+ }
+ Ins* d = fn->tmp[r.val].def;
+ if (!d) {
+ // phi in body (invariant): take preheader edge
+ if (bid != L.header->id) {
+ mat_memo[k] = r;
+ return r;
+ }
+ Phi* p = find_header_phi(L.header, r);
+ if (!p) {
+ mat_memo[k] = r;
+ return r;
+ }
+ Ref pe = phi_arg_from(p, L.preheader);
+ Ref sub = materialize(pe);
+ mat_memo[k] = sub;
+ return sub;
+ }
+ Ref a0 = req(d->arg[0], Ref::none()) ? Ref::none() : materialize(d->arg[0]);
+ Ref a1 = req(d->arg[1], Ref::none()) ? Ref::none() : materialize(d->arg[1]);
+ if (d->op == Ocopy) {
+ mat_memo[k] = a0;
+ return a0;
+ }
+ if (d->op == Oextsw && rtype(a0) == RCon) {
+ mat_memo[k] = a0;
+ return a0;
+ }
+ int cls = static_cast<int>(d->cls);
+ Ref nt = newtmp(ctx, "lsr.lftr", cls, fn);
+ Ins ni{};
+ ni.op = d->op;
+ ni.cls = static_cast<uint>(cls) & 3u;
+ ni.to = nt;
+ ni.arg[0] = a0;
+ ni.arg[1] = a1;
+ addins(ctx, &L.preheader->ins, &L.preheader->nins, &ni);
+ mat_memo[k] = nt;
+ return nt;
+ };
+
+ Ref n_at_pre = materialize(n_ref);
+
+ // Choose replacement IV: src_biv == bi and c != 0, pick smallest |c|.
+ int best = -1;
+ int64_t best_abs = 0;
+ for (size_t i = 0; i < created_ivs.size(); i++) {
+ const CreatedIV& iv = created_ivs[i];
+ if (iv.src_biv != bi)
+ continue;
+ if (iv.c == 0)
+ continue;
+ int64_t a = iv.c < 0 ? -iv.c : iv.c;
+ if (best < 0 || a < best_abs) {
+ best = static_cast<int>(i);
+ best_abs = a;
+ }
+ }
+ if (best < 0)
+ return 0;
+
+ const CreatedIV& iv = created_ivs[static_cast<size_t>(best)];
+ int64_t i0 = bivs[static_cast<size_t>(bi)].init;
+ int64_t cc = iv.c;
+ int iv_cls = iv.cls; // Kw or Kl, determines cls of preheader arithmetic and new cmp
+
+ // n_at_pre's cls must be compatible with IV:
+ // - iv Kl: if n Kw -> extsw; if n Kl -> use directly
+ // - iv Kw: if n Kw -> use directly; if n Kl -> unsafe (truncation), skip
+ int n_cls = Kl;
+ if (rtype(n_at_pre) == RTmp)
+ n_cls = static_cast<int>(fn->tmp[n_at_pre.val].cls);
+ else
+ n_cls =
+ iv_cls; // RCon treated as target cls (CBits 64-bit constant, getcon reused directly)
+ Ref n_use;
+ if (iv_cls == Kl && n_cls == Kw) {
+ n_use = newtmp(ctx, "lsr.lftr", Kl, fn);
+ Ins ni{};
+ ni.op = Oextsw;
+ ni.cls = static_cast<uint>(Kl) & 3u;
+ ni.to = n_use;
+ ni.arg[0] = n_at_pre;
+ ni.arg[1] = Ref::none();
+ addins(ctx, &L.preheader->ins, &L.preheader->nins, &ni);
+ } else if (iv_cls == n_cls || rtype(n_at_pre) == RCon) {
+ n_use = n_at_pre;
+ } else {
+ return 0; // iv Kw, n Kl is unsafe
+ }
+
+ Ref i0_c = getcon(ctx, i0, fn);
+ Ref c_c = getcon(ctx, cc, fn);
+
+ auto emit_pre = [&](int op, int cls, Ref a0, Ref a1) -> Ref {
+ Ref nt = newtmp(ctx, "lsr.lftr", cls, fn);
+ Ins ni{};
+ ni.op = static_cast<uint>(op) & 0x3fffffffu;
+ ni.cls = static_cast<uint>(cls) & 3u;
+ ni.to = nt;
+ ni.arg[0] = a0;
+ ni.arg[1] = a1;
+ addins(ctx, &L.preheader->ins, &L.preheader->nins, &ni);
+ return nt;
+ };
+ Ref diff = emit_pre(Osub, iv_cls, n_use, i0_c);
+ Ref scaled = emit_pre(Omul, iv_cls, diff, c_c);
+ Ref pend = emit_pre(Oadd, iv_cls, iv.init_ref, scaled);
+
+ // Rewrite cmp (in-place; preheader's addins might realloc, so re-fetch is safe)
+ cmp = &L.header->ins[cmp_idx];
+ int final_op = (iv_cls == Kl) ? (cc < 0 ? op_l_flip : op_l) : (cc < 0 ? op_w_flip : op_w);
+ cmp->op = static_cast<uint>(final_op) & 0x3fffffffu;
+ cmp->cls = static_cast<uint>(iv_cls) & 3u;
+ if (biv_at_0) {
+ cmp->arg[0] = iv.to;
+ cmp->arg[1] = pend;
+ } else {
+ cmp->arg[0] = pend;
+ cmp->arg[1] = iv.to;
+ }
+ return 1;
+}
+
+bool run_loop(Ctx* ctx, Fn* fn, const Loop& L, int& reduced_total, int& lifted_total,
+ int& retested_total) {
+ std::vector<Biv> bivs;
+ detect_bivs(fn, L, bivs);
+ if (bivs.empty())
+ return false;
+
+ std::vector<CreatedIV> created_ivs;
+ int reduced = reduce_mul(ctx, fn, L, bivs, created_ivs);
+ int lifted = insert_addr_iv(ctx, fn, L, bivs, created_ivs);
+ int retested = 0;
+ if (!created_ivs.empty()) {
+ // tmp.nuse might be stale after insert_addr_iv rewrites mul->Ocopy. LFTR guard relies on
+ // nuse, so update once.
+ filluse(ctx, fn);
+ retested = replace_exit_test(ctx, fn, L, bivs, created_ivs);
+ }
+
+ reduced_total += reduced;
+ lifted_total += lifted;
+ retested_total += retested;
+ return (reduced + lifted + retested) > 0;
+}
+
+// LSR cleanup: resolve all RTmp references along Ocopy chains to their roots.
+// insert_addr_iv rewrites address additions as Ocopy %new_iv, leaving isolated Ocopys.
+// Downstream passes expect canonical IR. Walk uses and resolve RTmp defs if they are Ocopy.
+// Keep Ocopy itself; nuse becomes 0, and DCE cleans it up.
+void propagate_copies(Fn* fn) {
+ auto resolve = [&](Ref r) -> Ref {
+ for (int hop = 0; hop < 16; hop++) {
+ if (rtype(r) != RTmp)
+ break;
+ uint v = r.val;
+ if (v >= static_cast<uint>(fn->ntmp))
+ break;
+ Ins* d = fn->tmp[v].def;
+ if (!d || d->op != Ocopy)
+ break;
+ if (rtype(d->arg[0]) == -1)
+ break;
+ r = d->arg[0];
+ }
+ return r;
+ };
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Phi* p = b->phi; p; p = p->link)
+ for (uint k = 0; k < p->narg; k++)
+ p->arg[k] = resolve(p->arg[k]);
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (i->op == Ocopy)
+ continue; // Do not resolve Ocopy's own arg to preserve chain info
+ for (int a = 0; a < 2; a++)
+ i->arg[a] = resolve(i->arg[a]);
+ }
+ b->jmp.arg = resolve(b->jmp.arg);
+ b->jmp.arg2 = resolve(b->jmp.arg2);
+ }
+}
+
+} // namespace
+
+void lsr(Ctx* ctx, Fn* fn) {
+ // alias info used by is_inv to check loop-invariant loads.
+ // LSR only produces phi/add/mul/copy/sub, doesn't touch load/store, so no need to recalculate
+ // inside fixpoint iter.
+ fillalias(fn);
+
+ int iter = 0;
+ int reduced_total = 0;
+ int lifted_total = 0;
+ int retested_total = 0;
+ for (; iter < kFixpointIters; iter++) {
+ std::vector<Loop> loops;
+ find_loops(fn, loops);
+ if (loops.empty())
+ break;
+
+ bool changed = false;
+ for (auto& L : loops)
+ if (run_loop(ctx, fn, L, reduced_total, lifted_total, retested_total))
+ changed = true;
+ if (!changed)
+ break;
+ // CFG unchanged; new Ins def requires filluse to write Tmp.def for next round's
+ // scev/detect_bivs.
+ filluse(ctx, fn);
+ }
+
+ // After fixpoint, resolve Ocopy chains left by insert_addr_iv globally.
+ // Downstream passes won't see intermediate Ocopy references. Isolated Ocopy ins cleaned by DCE.
+ propagate_copies(fn);
+
+ if (debug['V']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out,
+ "\n> LSR %s: reduced=%d mul, lifted=%d addr, retested=%d exit, in %d round%s\n",
+ fn->name, reduced_total, lifted_total, retested_total, iter, iter == 1 ? "" : "s");
+ printfn(fn, out);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/mem.cc b/src/mid/mem.cc
new file mode 100644
index 0000000..12b9ef8
--- /dev/null
+++ b/src/mid/mem.cc
@@ -0,0 +1,653 @@
+/* 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 mem.cc
+/// @brief Memory optimizations: `promote` and `coalesce`.
+///
+/// See docs/mem.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <climits>
+#include <cstdlib>
+#include <cstring>
+
+namespace rcc {
+
+// promote slots.
+
+namespace {
+
+/// Describes the result of a promote compatibility scan.
+struct UniformFit {
+ int sz; // Shared access width; -1 means "undetermined" (only in all-load state)
+ int k; // Shared store cls; -1 means "no store" (pure read slot, dies later)
+};
+
+/// Scan all uses of alloc tmp to check if it can be promoted. Returns true and fills *fit on
+/// success.
+bool check_uniform(Tmp* t, Ref alloc_ref, UniformFit* fit) {
+ int s = -1, k = -1;
+ for (Use* u = t->use; u < &t->use[t->nuse]; u++) {
+ if (u->type != Use::UIns)
+ return false;
+ Ins* l = u->u.ins;
+
+ if (isload(l->op)) {
+ int ls = loadsz(l);
+ if (s != -1 && s != ls)
+ return false;
+ s = ls;
+ continue;
+ }
+
+ if (isstore(l->op)) {
+ // Must be store(value, addr=alloc): addr in arg[1], value in arg[0]
+ if (!req(alloc_ref, l->arg[1]) || req(alloc_ref, l->arg[0]))
+ return false;
+ int ss = storesz(l);
+ if (s != -1 && s != ss)
+ return false;
+ int sk = optab[l->op].argcls[0][0];
+ if (k != -1 && k != sk)
+ return false;
+ s = ss;
+ k = sk;
+ continue;
+ }
+
+ return false; // Non load/store use (addr passed out, compared, etc.)
+ }
+ fit->sz = s;
+ fit->k = k;
+ return true;
+}
+
+inline Ins make_nop() {
+ Ins n{};
+ n.op = Onop;
+ return n;
+}
+
+/// Rewrite uses to copy / cast / ext (see docs/mem.md).
+void rewrite_uses(Tmp* t, int k, Fn* fn) {
+ Use* ue = &t->use[t->nuse];
+ for (Use* u = t->use; u != ue; u++) {
+ Ins* l = u->u.ins;
+
+ if (isstore(l->op)) {
+ // store value, addr → copy value to addr-as-def
+ l->cls = static_cast<uint>(k);
+ l->op = Ocopy;
+ l->to = l->arg[1];
+ l->arg[1] = Ref::none();
+ t->nuse--;
+ t->ndef++;
+ continue;
+ }
+
+ // Check lock.
+ if (k == -1)
+ die("promote: slot %%%s is read but never stored to", fn->tmp[l->arg[0].val].name);
+
+ bool extend = false;
+ switch (l->op) {
+ case Oloadsw:
+ case Oloaduw:
+ if (k == Kl) {
+ extend = true;
+ break;
+ }
+ [[fallthrough]];
+ case Oload:
+ if (KBASE(k) != KBASE(static_cast<int>(l->cls)))
+ l->op = Ocast;
+ else
+ l->op = Ocopy;
+ break;
+ default:
+ extend = true;
+ break;
+ }
+ if (extend)
+ l->op = static_cast<uint>(Oextsb + (static_cast<int>(l->op) - Oloadsb));
+ }
+}
+
+} // namespace
+
+void promote(Ctx* ctx, Fn* fn) {
+ Blk* b = fn->start;
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ if (i->op < Oalloc || i->op > Oalloc1)
+ continue;
+ // Assumption NAlign == 3.
+ assert(rtype(i->to) == RTmp);
+ Tmp* t = &fn->tmp[i->to.val];
+ if (t->ndef != 1)
+ continue;
+
+ UniformFit fit;
+ if (!check_uniform(t, i->to, &fit))
+ continue;
+
+ // Kill alloc, rewrite all uses.
+ *i = make_nop();
+ t->ndef--;
+ rewrite_uses(t, fit.k, fn);
+ }
+
+ if (debug['M']) {
+ fprintf(ctx->dbg_buf, "\n> After slot promotion:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+// coalesce slots.
+
+namespace {
+
+/// Half-open interval [a, b). b == 0 means "empty interval" (slot not yet radd'ed).
+struct Range {
+ int a, b;
+};
+
+/// store-record: a store instruction recorded on a slot + its ip.
+struct Store {
+ int ip;
+ Ins* i;
+};
+
+struct Slot {
+ int t; // corresponding tmp number
+ int sz; // alloc size (bytes)
+ bits m; // accumulated write mask for this slot (from fillalias)
+ bits l; // read mask accumulated during reverse block traversal
+ Range r; // live range (covers all active ips)
+ Slot* s; // fused representative (s == this means self is rep); nullptr if not fused
+ Store* st; // array of store records on this slot (PHeap-vnew)
+ int nst;
+};
+
+inline bool rin(Range r, int n) {
+ return r.a <= n && n < r.b;
+}
+
+inline bool rovlap(Range r0, Range r1) {
+ // Overlap check.
+ return r0.b && r1.b && r0.a < r1.b && r1.a < r0.b;
+}
+
+void radd(Range* r, int n) {
+ if (!r->b) {
+ r->a = n;
+ r->b = n + 1;
+ } else if (n < r->a) {
+ r->a = n;
+ } else if (n >= r->b) {
+ r->b = n + 1;
+ }
+}
+
+/// Translate ref r to its slot (if any) and offset. Returns false if r does not point to a coalesce
+/// candidate.
+bool slot_of(Slot** ps, int64_t* off, Ref r, Fn* fn, Slot* sl) {
+ Alias a;
+ getalias(&a, r, fn);
+ if (a.type != Alias::ALoc)
+ return false;
+ Tmp* t = &fn->tmp[a.base];
+ if (t->visit < 0)
+ return false; // tmp not in sl[]
+ *off = a.offset;
+ *ps = &sl[t->visit];
+ return true;
+}
+
+/// Encountered a read of x high-order bytes from r during reverse RPO.
+void load(Ref r, bits x, int ip, Fn* fn, Slot* sl) {
+ int64_t off;
+ Slot* s;
+ if (!slot_of(&s, &off, r, fn, sl))
+ return;
+ s->l |= x << off;
+ s->l &= s->m;
+ if (s->l)
+ radd(&s->r, ip);
+}
+
+/// Encountered a write of x bytes to r during reverse RPO.
+/// Encountered a write.
+void store(Ref r, bits x, int ip, Ins* i, Ctx* ctx, Fn* fn, Slot* sl) {
+ int64_t off;
+ Slot* s;
+ if (!slot_of(&s, &off, r, fn, sl))
+ return;
+ if (s->l) {
+ // Currently has live bytes: store is valid, advance live range.
+ radd(&s->r, ip);
+ s->l &= ~(x << off);
+ } else {
+ // No live bytes: store is a potential dead store, record in slot.st[].
+ vgrow(ctx, &s->st, static_cast<ulong>(++s->nst));
+ s->st[s->nst - 1].ip = ip;
+ s->st[s->nst - 1].i = i;
+ }
+}
+
+/// qsort comparator: descending by sz; ascending by r.a if sz equal (earlier first).
+int scmp(const void* pa, const void* pb) {
+ const Slot* a = static_cast<const Slot*>(pa);
+ const Slot* b = static_cast<const Slot*>(pb);
+ if (a->sz != b->sz)
+ return b->sz - a->sz;
+ return a->r.a - b->r.a;
+}
+
+/// loopiter callback (see docs/mem.md).
+void maxrpo(Blk* hd, Blk* b) {
+ if (hd->loop < static_cast<int>(b->id))
+ hd->loop = static_cast<int>(b->id);
+}
+
+struct SlotList {
+ Slot* sl;
+ int nsl;
+};
+
+SlotList collect_slots(Ctx* ctx, Fn* fn) {
+ SlotList sls;
+ sls.nsl = 0;
+ sls.sl = static_cast<Slot*>(vnew(ctx, 0, sizeof(Slot), PHeap));
+ for (int n = Tmp0; n < fn->ntmp; n++) {
+ Tmp* t = &fn->tmp[n];
+ t->visit = -1;
+ // Qualification check.
+ if (t->alias.type != Alias::ALoc)
+ continue;
+ if (t->alias.slot != &t->alias)
+ continue;
+ if (t->bid != fn->start->id)
+ continue;
+ if (t->alias.u.loc.sz == -1)
+ continue;
+
+ t->visit = sls.nsl;
+ vgrow(ctx, &sls.sl, static_cast<ulong>(++sls.nsl));
+ Slot* s = &sls.sl[sls.nsl - 1];
+ s->t = n;
+ s->sz = t->alias.u.loc.sz;
+ s->m = t->alias.u.loc.m;
+ s->l = 0;
+ s->r = Range{0, 0};
+ s->s = nullptr;
+ s->st = static_cast<Store*>(vnew(ctx, 0, sizeof(Store), PHeap));
+ s->nst = 0;
+ }
+ return sls;
+}
+
+struct LiveOut {
+ Range* br; // br[n] is the entry/exit ip range of the n-th RPO block; PHeap, freed at coalesce
+ // end.
+ Ins** bl; // blit list (PHeap vnew)
+ int nbl;
+ int ip; // final ip (minimum); used for debugging output restoration.
+};
+
+LiveOut compute_liveness(Ctx* ctx, Fn* fn, Slot* sl, int nsl) {
+ LiveOut lo;
+ lo.nbl = 0;
+ lo.bl = static_cast<Ins**>(vnew(ctx, 0, sizeof(Ins*), PHeap));
+ lo.br = static_cast<Range*>(emalloc(static_cast<size_t>(fn->nblk) * sizeof(Range)));
+
+ // Initialize b->loop for maxrpo
+ for (Blk* b = fn->start; b; b = b->link)
+ b->loop = -1;
+ loopiter(fn, maxrpo);
+
+ int ip = INT_MAX - 1;
+ for (int n = static_cast<int>(fn->nblk) - 1; n >= 0; n--) {
+ Blk* b = fn->rpo[n];
+
+ // succ array (s1, s2 deduplicated, terminated by nullptr)
+ Blk* succ[3];
+ succ[0] = b->s1;
+ succ[1] = b->s2;
+ succ[2] = nullptr;
+
+ lo.br[n].b = ip--;
+
+ // For each slot: inherit live bytes from successor entries.
+ for (Slot* s = sl; s < &sl[nsl]; s++) {
+ s->l = 0;
+ for (Blk** ps = succ; *ps; ps++) {
+ int m = static_cast<int>((*ps)->id);
+ if (m > n && rin(s->r, lo.br[m].a)) {
+ s->l = s->m;
+ radd(&s->r, ip);
+ }
+ }
+ }
+
+ // Process Jretc jmp arg: treat as full slot read.
+ if (b->jmp.type == Jretc)
+ load(b->jmp.arg, static_cast<bits>(-1), --ip, fn, sl);
+
+ // Reverse traversal of instructions
+ for (Ins* i = &b->ins[b->nins]; i != b->ins;) {
+ --i;
+ Ref* arg = i->arg;
+
+ if (i->op == Oargc) {
+ // by-value struct arg: mark entire slot as read
+ load(arg[1], static_cast<bits>(-1), --ip, fn, sl);
+ }
+ if (isload(i->op)) {
+ bits x = BIT(loadsz(i)) - 1;
+ load(arg[0], x, --ip, fn, sl);
+ }
+ if (isstore(i->op)) {
+ bits x = BIT(storesz(i)) - 1;
+ store(arg[1], x, ip--, i, ctx, fn, sl);
+ }
+ if (i->op == Oblit0) {
+ assert((i + 1)->op == Oblit1);
+ assert(rtype((i + 1)->arg[0]) == RInt);
+ int sz = std::abs(rsval((i + 1)->arg[0]));
+ bits x = sz >= static_cast<int>(NBit) ? static_cast<bits>(-1) : BIT(sz) - 1;
+ store(arg[1], x, ip--, i, ctx, fn, sl);
+ load(arg[0], x, ip, fn, sl);
+ vgrow(ctx, &lo.bl, static_cast<ulong>(++lo.nbl));
+ lo.bl[lo.nbl - 1] = i;
+ }
+ }
+
+ // Block entry update.
+ for (Slot* s = sl; s < &sl[nsl]; s++)
+ if (s->l) {
+ radd(&s->r, ip);
+ if (b->loop != -1) {
+ assert(b->loop >= n);
+ radd(&s->r, lo.br[b->loop].b - 1);
+ }
+ }
+ lo.br[n].a = ip;
+ }
+
+ lo.ip = ip;
+ return lo;
+}
+
+void kill_dead_stores(Slot* sl, int nsl) {
+ for (Slot* s = sl; s < &sl[nsl]; s++) {
+ for (int n = 0; n < s->nst; n++) {
+ if (rin(s->r, s->st[n].ip))
+ continue;
+ // ip not in r -> dead store
+ Ins* i = s->st[n].i;
+ if (i->op == Oblit0)
+ *(i + 1) = make_nop();
+ *i = make_nop();
+ }
+ }
+}
+
+/// Kills empty interval slots recursively (see docs/mem.md).
+struct KillStats {
+ int new_nsl;
+ uint total;
+ uint freed;
+};
+
+KillStats kill_empty_slots(Ctx* ctx, Slot* sl, int nsl, Fn* fn) {
+ int* stk = static_cast<int*>(vnew(ctx, 0, sizeof(int), PHeap));
+ int n = 0;
+ uint total = 0, freed = 0;
+
+ // Compact sl[].
+ Slot* s0 = sl;
+ for (Slot* s = sl; s < &sl[nsl]; s++) {
+ total += static_cast<uint>(s->sz);
+ if (!s->r.b) {
+ vfree(s->st);
+ vgrow(ctx, &stk, static_cast<ulong>(++n));
+ stk[n - 1] = s->t;
+ freed += static_cast<uint>(s->sz);
+ } else {
+ *s0++ = *s;
+ }
+ }
+ int new_nsl = static_cast<int>(s0 - sl);
+
+ if (debug['M']) {
+ fputs("\n> Slot coalescing:\n", ctx->dbg_buf);
+ if (n) {
+ fputs("\tkill [", ctx->dbg_buf);
+ for (int m = 0; m < n; m++)
+ fprintf(ctx->dbg_buf, " %%%s", fn->tmp[stk[m]].name);
+ fputs(" ]\n", ctx->dbg_buf);
+ }
+ }
+
+ // Propagate kill.
+ while (n--) {
+ Tmp* t = &fn->tmp[stk[n]];
+ assert(t->ndef == 1 && t->def);
+ Ins* i = t->def;
+ if (isload(i->op)) {
+ // This tmp is a pointer loaded; change to copy UNDEF so subsequent fold cleans it
+ // naturally.
+ i->op = Ocopy;
+ i->arg[0] = Ref::undef();
+ continue;
+ }
+ *i = make_nop();
+ for (Use* u = t->use; u < &t->use[t->nuse]; u++) {
+ if (u->type == Use::UJmp) {
+ Blk* b = fn->rpo[u->bid];
+ assert(isret(b->jmp.type));
+ b->jmp.type = Jret0;
+ b->jmp.arg = Ref::none();
+ continue;
+ }
+ assert(u->type == Use::UIns);
+ i = u->u.ins;
+ if (!req(i->to, Ref::none())) {
+ assert(rtype(i->to) == RTmp);
+ vgrow(ctx, &stk, static_cast<ulong>(++n));
+ stk[n - 1] = static_cast<int>(i->to.val);
+ } else if (isarg(i->op)) {
+ // Only Oargc reaches here: change its arg[1] to CON_Z to crash the callee
+ // intentionally.
+ assert(i->op == Oargc);
+ i->arg[1] = Ref::con_z();
+ } else {
+ if (i->op == Oblit0)
+ *(i + 1) = make_nop();
+ *i = make_nop();
+ }
+ }
+ }
+ vfree(stk);
+
+ return KillStats{new_nsl, total, freed};
+}
+
+uint fuse_slots(Slot* sl, int nsl) {
+ qsort(sl, static_cast<size_t>(nsl), sizeof(Slot), scmp);
+ uint fused = 0;
+
+ for (int n = 0; n < nsl; n++) {
+ Slot* s0 = &sl[n];
+ if (s0->s)
+ continue;
+ s0->s = s0;
+ Range r = s0->r;
+
+ for (Slot* s = s0 + 1; s < &sl[nsl]; s++) {
+ if (s->s || !s->r.b)
+ continue;
+ if (rovlap(r, s->r)) {
+ // Scan for collision.
+ bool conflict = false;
+ for (int m = n; &sl[m] < s; m++) {
+ if (sl[m].s == s0 && rovlap(sl[m].r, s->r)) {
+ conflict = true;
+ break;
+ }
+ }
+ if (conflict)
+ continue;
+ }
+ radd(&r, s->r.a);
+ radd(&r, s->r.b - 1);
+ s->s = s0;
+ fused += static_cast<uint>(s->sz);
+ }
+ }
+ return fused;
+}
+
+void substitute_fused_slots(Slot* sl, int nsl, Fn* fn) {
+ for (Slot* s = sl; s < &sl[nsl]; s++) {
+ Tmp* t = &fn->tmp[s->t];
+ // Reset visit.
+ t->visit = static_cast<int>(s - sl);
+ assert(t->ndef == 1 && t->def);
+ if (s->s == s)
+ continue;
+
+ // Erase alloc instruction of this slot
+ *t->def = make_nop();
+
+ Tmp* ts = &fn->tmp[s->s->t];
+ assert(t->bid == ts->bid);
+
+ // Guarantee dominance.
+ if (t->def < ts->def) {
+ *t->def = *ts->def;
+ *ts->def = make_nop();
+ ts->def = t->def;
+ }
+
+ // Rewrite all uses to point to the representative tmp
+ for (Use* u = t->use; u < &t->use[t->nuse]; u++) {
+ if (u->type == Use::UJmp) {
+ Blk* b = fn->rpo[u->bid];
+ b->jmp.arg = Ref::tmp(static_cast<uint>(s->s->t));
+ continue;
+ }
+ assert(u->type == Use::UIns);
+ Ref* arg = u->u.ins->arg;
+ for (int n = 0; n < 2; n++)
+ if (req(arg[n], Ref::tmp(static_cast<uint>(s->t))))
+ arg[n] = Ref::tmp(static_cast<uint>(s->s->t));
+ }
+ }
+}
+
+void fix_blits(Ins** bl, int nbl, Slot* sl, Fn* fn) {
+ for (int n = 0; n < nbl; n++) {
+ Ins* i = bl[n];
+ if (i->op != Oblit0)
+ continue; // Already cleaned up in some round
+ Slot *sa, *sb;
+ int64_t off0, off1;
+ if (!slot_of(&sa, &off0, i->arg[0], fn, sl))
+ continue;
+ if (!slot_of(&sb, &off1, i->arg[1], fn, sl))
+ continue;
+ if (sa->s != sb->s)
+ continue;
+
+ // Both ends fused into same slot
+ if (off0 < off1) {
+ // Reverse blit: negative size to tell backend to reverse memmove.
+ int sz = rsval((i + 1)->arg[0]);
+ assert(sz >= 0);
+ (i + 1)->arg[0] = Ref::integer(-sz);
+ } else if (off0 == off1) {
+ // Self-blit: delete entirely.
+ *i = make_nop();
+ *(i + 1) = make_nop();
+ }
+ }
+}
+
+void print_fuse_groups(Ctx* ctx, Slot* sl, int nsl, int ip, Fn* fn, uint freed, uint fused,
+ uint total) {
+ FILE* f = ctx->dbg_buf;
+ for (Slot* s0 = sl; s0 < &sl[nsl]; s0++) {
+ if (s0->s != s0)
+ continue;
+ fprintf(f, "\tfuse (% 3db) [", s0->sz);
+ for (Slot* s = s0; s < &sl[nsl]; s++) {
+ if (s->s != s0)
+ continue;
+ fprintf(f, " %%%s", fn->tmp[s->t].name);
+ if (s->r.b)
+ fprintf(f, "[%d,%d)", s->r.a - ip, s->r.b - ip);
+ else
+ fputs("{}", f);
+ }
+ fputs(" ]\n", f);
+ }
+ fprintf(f, "\tsums %u/%u/%u (killed/fused/total)\n\n", freed, fused, total);
+ printfn(fn, f);
+}
+
+} // namespace
+
+void coalesce(Ctx* ctx, Fn* fn) {
+ // Collect candidate slots
+ SlotList sls = collect_slots(ctx, fn);
+ Slot* sl = sls.sl;
+ int nsl = sls.nsl;
+
+ // Single reverse RPO to compute live ranges
+ LiveOut lo = compute_liveness(ctx, fn, sl, nsl);
+ free(lo.br);
+
+ // Kill out-of-range stores
+ kill_dead_stores(sl, nsl);
+
+ // Kill empty interval slots and propagate use-chain cleanup
+ KillStats ks = kill_empty_slots(ctx, sl, nsl, fn);
+ nsl = ks.new_nsl;
+
+ // Fuse slots
+ uint fused = fuse_slots(sl, nsl);
+
+ // Redirect def/use of fused slots to representatives
+ substitute_fused_slots(sl, nsl, fn);
+
+ // Fix instructions that became same-slot self-blits
+ fix_blits(lo.bl, lo.nbl, sl, fn);
+ vfree(lo.bl);
+
+ if (debug['M'])
+ print_fuse_groups(ctx, sl, nsl, lo.ip, fn, ks.freed, fused, ks.total);
+
+ for (Slot* s = sl; s < &sl[nsl]; s++)
+ vfree(s->st);
+ vfree(sl);
+}
+
+} // namespace rcc
diff --git a/src/mid/mulopt.cc b/src/mid/mulopt.cc
new file mode 100644
index 0000000..0a369db
--- /dev/null
+++ b/src/mid/mulopt.cc
@@ -0,0 +1,208 @@
+/* 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 mulopt.cc
+/// @brief Constant integer multiplication strength reduction.
+///
+/// See docs/mulopt.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cstdint>
+
+namespace rcc {
+
+namespace {
+
+enum Kind {
+ KZero,
+ KCopy,
+ KShl,
+ KShlAddShl,
+ KShlSubShl,
+};
+
+struct Rewrite {
+ Kind kind;
+ int sh1; // Primary shift for KShl / KShlAddShl / KShlSubShl
+ int sh2; // Secondary shift for KShlAddShl / KShlSubShl (0 means use x itself)
+};
+
+// Classify patterns.
+bool classify(uint64_t ac, int max_shift, Rewrite& out) {
+ if (ac == 0) {
+ out.kind = KZero;
+ return true;
+ }
+ if (ac == 1) {
+ out.kind = KCopy;
+ return true;
+ }
+ if ((ac & (ac - 1)) == 0) {
+ int k = __builtin_ctzll(ac);
+ if (k >= max_shift)
+ return false;
+ out.kind = KShl;
+ out.sh1 = k;
+ return true;
+ }
+ int a = 63 - __builtin_clzll(ac); // floor log2(ac), 0 < ac, ac is not pow2
+ if (a >= max_shift)
+ return false;
+ uint64_t hi = static_cast<uint64_t>(1) << a;
+
+ // pow2 + pow2.
+ uint64_t low = ac - hi;
+ if ((low & (low - 1)) == 0) {
+ int b = __builtin_ctzll(low);
+ out.kind = KShlAddShl;
+ out.sh1 = a;
+ out.sh2 = b;
+ return true;
+ }
+
+ // pow2 - pow2.
+ if (a + 1 < max_shift) {
+ uint64_t hi2 = hi << 1;
+ uint64_t diff = hi2 - ac;
+ // Check diff.
+ if ((diff & (diff - 1)) == 0) {
+ int b = __builtin_ctzll(diff);
+ out.kind = KShlSubShl;
+ out.sh1 = a + 1;
+ out.sh2 = b;
+ return true;
+ }
+ }
+ return false;
+}
+
+// emit() prepends.
+bool lower_mul(Ctx* ctx, Fn* fn, Ins* i, Blk* b, bool* rebuilt) {
+ if (i->op != Omul)
+ return false;
+ if (i->cls != Kw && i->cls != Kl)
+ return false;
+ if (rtype(i->arg[1]) != RCon)
+ return false;
+ Con* c = &fn->con[i->arg[1].val];
+ if (c->type != Con::CBits)
+ return false;
+
+ int cls = i->cls;
+ int64_t cv;
+ int max_shift;
+ if (cls == Kw) {
+ cv = static_cast<int32_t>(
+ c->bits.i); // Narrow to i32 to avoid interference from Kl residual bits
+ max_shift = 32;
+ } else {
+ cv = c->bits.i;
+ max_shift = 64;
+ }
+
+ bool neg = (cv < 0);
+ uint64_t ac = static_cast<uint64_t>(cv);
+ if (neg)
+ ac = -ac; // wrap-around produces 2^31 / 2^63 for INT_MIN / INT64_MIN respectively
+
+ Rewrite r;
+ if (!classify(ac, max_shift, r))
+ return false;
+
+ Ref dst = i->to;
+ Ref x = i->arg[0];
+
+ // Move unprocessed tail.
+ if (!*rebuilt) {
+ ctx->curi = ctx->insb + NIns;
+ ulong tail = static_cast<ulong>((b->ins + b->nins) - (i + 1));
+ ctx->curi -= tail;
+ icpy(ctx->curi, i + 1, tail);
+ *rebuilt = true;
+ }
+
+ switch (r.kind) {
+ case KZero:
+ // 0 * x = 0.
+ emit(ctx, Ocopy, cls, dst, getcon(ctx, 0, fn), Ref::none());
+ break;
+ case KCopy:
+ if (neg)
+ emit(ctx, Oneg, cls, dst, x, Ref::none());
+ else
+ emit(ctx, Ocopy, cls, dst, x, Ref::none());
+ break;
+ case KShl: {
+ if (neg) {
+ Ref t = newtmp(ctx, "mulopt", cls, fn);
+ emit(ctx, Oneg, cls, dst, t, Ref::none());
+ emit(ctx, Oshl, cls, t, x, getcon(ctx, r.sh1, fn));
+ } else {
+ emit(ctx, Oshl, cls, dst, x, getcon(ctx, r.sh1, fn));
+ }
+ break;
+ }
+ case KShlAddShl: {
+ // sum.
+ Ref sum = neg ? newtmp(ctx, "mulopt", cls, fn) : dst;
+ Ref t1 = newtmp(ctx, "mulopt", cls, fn);
+ Ref t2 = (r.sh2 == 0) ? x : newtmp(ctx, "mulopt", cls, fn);
+ if (neg)
+ emit(ctx, Oneg, cls, dst, sum, Ref::none());
+ emit(ctx, Oadd, cls, sum, t1, t2);
+ if (r.sh2 != 0)
+ emit(ctx, Oshl, cls, t2, x, getcon(ctx, r.sh2, fn));
+ emit(ctx, Oshl, cls, t1, x, getcon(ctx, r.sh1, fn));
+ break;
+ }
+ case KShlSubShl: {
+ Ref diff = neg ? newtmp(ctx, "mulopt", cls, fn) : dst;
+ Ref t1 = newtmp(ctx, "mulopt", cls, fn);
+ Ref t2 = (r.sh2 == 0) ? x : newtmp(ctx, "mulopt", cls, fn);
+ if (neg)
+ emit(ctx, Oneg, cls, dst, diff, Ref::none());
+ emit(ctx, Osub, cls, diff, t1, t2);
+ if (r.sh2 != 0)
+ emit(ctx, Oshl, cls, t2, x, getcon(ctx, r.sh2, fn));
+ emit(ctx, Oshl, cls, t1, x, getcon(ctx, r.sh1, fn));
+ break;
+ }
+ }
+ return true;
+}
+
+} // namespace
+
+void mulopt(Ctx* ctx, Fn* fn) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ bool rebuilt = false;
+ for (Ins* i = b->ins + b->nins; i != b->ins;) {
+ --i;
+ if (lower_mul(ctx, fn, i, b, &rebuilt))
+ continue;
+ if (rebuilt)
+ emiti(ctx, *i);
+ }
+ if (rebuilt)
+ idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi));
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/reassoc.cc b/src/mid/reassoc.cc
new file mode 100644
index 0000000..91ff747
--- /dev/null
+++ b/src/mid/reassoc.cc
@@ -0,0 +1,436 @@
+/* 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 reassoc.cc
+/// @brief Reassociation pass (LLVM rank-based style).
+///
+/// See docs/reassoc.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <cstdio>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+enum Family { FAdd, FMul, FAnd, FOr, FXor, FNone };
+
+Family family_of(uint op) {
+ switch (op) {
+ case Oadd:
+ case Osub:
+ return FAdd;
+ case Omul:
+ return FMul;
+ case Oand:
+ return FAnd;
+ case Oor:
+ return FOr;
+ case Oxor:
+ return FXor;
+ default:
+ return FNone;
+ }
+}
+
+// Binary op for the family.
+int family_binop(Family f) {
+ switch (f) {
+ case FAdd:
+ return Oadd;
+ case FMul:
+ return Omul;
+ case FAnd:
+ return Oand;
+ case FOr:
+ return Oor;
+ case FXor:
+ return Oxor;
+ default:
+ return Onop;
+ }
+}
+
+struct Leaf {
+ Ref ref;
+ int sign; // Only meaningful for FAdd, otherwise always +1
+};
+
+struct Reassoc {
+ Ctx* ctx;
+ Fn* fn;
+ std::unordered_set<Ins*> absorbed;
+ std::unordered_map<Ins*, std::vector<Ins>>
+ prelude; // root ins -> intermediate instructions in chain
+ int n_trees = 0, n_folded = 0, n_addsub = 0, n_chainmax = 0;
+
+ bool is_family_node(Ins* d, Family fam, int cls) {
+ return d && family_of(d->op) == fam && static_cast<int>(d->cls) == cls;
+ }
+
+ int64_t mask_val(int cls, int64_t v) {
+ return (cls == Kw) ? static_cast<int64_t>(static_cast<int32_t>(v)) : v;
+ }
+
+ // Flatten nodes.
+ void collect(Ins* root, Family fam, int cls, std::vector<Leaf>& out) {
+ std::vector<Leaf> stk;
+ if (fam == FAdd) {
+ stk.push_back({root->arg[0], +1});
+ stk.push_back({root->arg[1], (root->op == Osub) ? -1 : +1});
+ } else {
+ stk.push_back({root->arg[0], +1});
+ stk.push_back({root->arg[1], +1});
+ }
+ while (!stk.empty()) {
+ Leaf cur = stk.back();
+ stk.pop_back();
+ if (rtype(cur.ref) == RTmp && cur.ref.val != 0) {
+ Tmp* t = &fn->tmp[cur.ref.val];
+ Ins* d = t->def;
+ if (d && t->nuse == 1 && is_family_node(d, fam, cls) && !absorbed.count(d)) {
+ absorbed.insert(d);
+ if (fam == FAdd) {
+ stk.push_back({d->arg[0], cur.sign});
+ stk.push_back({d->arg[1], (d->op == Osub) ? -cur.sign : cur.sign});
+ } else {
+ stk.push_back({d->arg[0], cur.sign});
+ stk.push_back({d->arg[1], cur.sign});
+ }
+ continue;
+ }
+ }
+ out.push_back(cur);
+ }
+ }
+
+ // rank nodes.
+ static uint64_t rank_of(Ref r) {
+ if (rtype(r) == RCon)
+ return (static_cast<uint64_t>(UINT32_MAX) << 32) | r.val;
+ return (static_cast<uint64_t>(r.val) << 32) | r.val;
+ }
+
+ void sort_terms(std::vector<Leaf>& v) {
+ std::sort(v.begin(), v.end(),
+ [](const Leaf& a, const Leaf& b) { return rank_of(a.ref) < rank_of(b.ref); });
+ }
+
+ // Build left-leaning chain.
+ Ref build_full(const std::vector<Ref>& refs, int bop, int cls, std::vector<Ins>& pre) {
+ Ref acc = refs[0];
+ for (size_t k = 1; k < refs.size(); k++) {
+ Ref t = newtmp(ctx, "rea", cls, fn);
+ Ins ni;
+ ni.op = static_cast<uint>(bop);
+ ni.cls = static_cast<uint>(cls);
+ ni.to = t;
+ ni.arg[0] = acc;
+ ni.arg[1] = refs[k];
+ pre.push_back(ni);
+ acc = t;
+ }
+ return acc;
+ }
+
+ void make_copy(Ins* root, Ref r) {
+ root->op = Ocopy;
+ root->arg[0] = r;
+ root->arg[1] = Ref::none();
+ }
+
+ void make_bin(Ins* root, int op, Ref a0, Ref a1) {
+ root->op = static_cast<uint>(op);
+ root->arg[0] = a0;
+ root->arg[1] = a1;
+ }
+
+ // Add/Sub Reconstruction
+ void rebuild_addsub(Ins* root, int cls, std::vector<Leaf>& leaves) {
+ // Constant folding.
+ int64_t cacc = 0;
+ std::vector<Leaf> terms;
+ int nconst = 0;
+ for (const Leaf& lf : leaves) {
+ int64_t v;
+ if (isconbits(fn, lf.ref, &v)) {
+ cacc += lf.sign * v;
+ nconst++;
+ } else {
+ terms.push_back(lf);
+ }
+ }
+ cacc = mask_val(cls, cacc);
+ if (nconst >= 2)
+ n_folded++;
+
+ sort_terms(terms);
+ // Net sum of signs.
+ std::vector<Leaf> canon;
+ for (size_t k = 0; k < terms.size();) {
+ size_t j = k;
+ int64_t net = 0;
+ while (j < terms.size() && req(terms[j].ref, terms[k].ref)) {
+ net += terms[j].sign;
+ j++;
+ }
+ int s = (net > 0) ? +1 : -1;
+ for (int64_t c = 0; c < (net < 0 ? -net : net); c++)
+ canon.push_back({terms[k].ref, s});
+ k = j;
+ }
+
+ // Merge constants.
+ std::vector<Ref> pos, neg;
+ for (const Leaf& lf : canon) {
+ if (lf.sign > 0)
+ pos.push_back(lf.ref);
+ else
+ neg.push_back(lf.ref);
+ }
+ if (cacc != 0)
+ pos.push_back(getcon(ctx, cacc, fn));
+
+ n_addsub++;
+
+ if (pos.empty() && neg.empty()) {
+ make_copy(root, getcon(ctx, cacc, fn)); // cacc==0 here -> copy 0
+ return;
+ }
+ std::vector<Ins>& pre = prelude[root];
+ if (neg.empty()) {
+ if (pos.size() == 1) {
+ make_copy(root, pos[0]);
+ } else {
+ Ref acc = build_full(std::vector<Ref>(pos.begin(), pos.end() - 1), Oadd, cls, pre);
+ make_bin(root, Oadd, acc, pos.back());
+ }
+ } else if (pos.empty()) {
+ Ref negr = build_full(neg, Oadd, cls, pre);
+ make_bin(root, Oneg, negr, Ref::none());
+ } else {
+ Ref posr = build_full(pos, Oadd, cls, pre);
+ Ref negr = build_full(neg, Oadd, cls, pre);
+ make_bin(root, Osub, posr, negr);
+ }
+ size_t len = pos.size() + neg.size();
+ if (static_cast<int>(len) > n_chainmax)
+ n_chainmax = static_cast<int>(len);
+ }
+
+ // Mul/And/Or/Xor Reconstruction
+ void rebuild_logic(Ins* root, Family fam, int cls, std::vector<Leaf>& leaves) {
+ int bop = family_binop(fam);
+ int64_t mask = (cls == Kw) ? 0xffffffffLL : ~0LL;
+ int64_t allones = mask;
+
+ // Identity starting point
+ int64_t acc;
+ switch (fam) {
+ case FMul:
+ acc = 1;
+ break;
+ case FAnd:
+ acc = allones;
+ break;
+ default:
+ acc = 0; // Or / Xor
+ break;
+ }
+ int nconst = 0;
+ std::vector<Leaf> terms;
+ for (const Leaf& lf : leaves) {
+ int64_t v;
+ if (isconbits(fn, lf.ref, &v)) {
+ nconst++;
+ v &= mask;
+ switch (fam) {
+ case FMul:
+ acc *= v;
+ break;
+ case FAnd:
+ acc &= v;
+ break;
+ case FOr:
+ acc |= v;
+ break;
+ default:
+ acc ^= v;
+ break;
+ }
+ } else {
+ terms.push_back(lf);
+ }
+ }
+ acc &= mask;
+ if (nconst >= 2)
+ n_folded++;
+
+ // Short circuit.
+ if ((fam == FMul && acc == 0 && nconst > 0) || (fam == FAnd && acc == 0 && nconst > 0)) {
+ make_copy(root, getcon(ctx, 0, fn));
+ return;
+ }
+ if (fam == FOr && acc == allones && nconst > 0) {
+ make_copy(root, getcon(ctx, mask_val(cls, allones), fn));
+ return;
+ }
+
+ sort_terms(terms);
+ // Cancellation.
+ std::vector<Ref> refs;
+ if (fam == FAnd || fam == FOr) {
+ for (size_t k = 0; k < terms.size(); k++)
+ if (k == 0 || !req(terms[k].ref, terms[k - 1].ref))
+ refs.push_back(terms[k].ref);
+ } else if (fam == FXor) {
+ for (size_t k = 0; k < terms.size();) {
+ size_t j = k;
+ while (j < terms.size() && req(terms[j].ref, terms[k].ref))
+ j++;
+ if ((j - k) & 1)
+ refs.push_back(terms[k].ref);
+ k = j;
+ }
+ } else {
+ for (const Leaf& lf : terms)
+ refs.push_back(lf.ref);
+ }
+
+ // Whether to merge constant: only add if not identity.
+ bool const_is_id = (fam == FMul && acc == 1) || (fam == FAnd && acc == allones) ||
+ ((fam == FOr || fam == FXor) && acc == 0);
+ if (!const_is_id || refs.empty())
+ refs.push_back(getcon(ctx, mask_val(cls, acc), fn));
+
+ if (refs.size() == 1) {
+ make_copy(root, refs[0]);
+ } else {
+ std::vector<Ins>& pre = prelude[root];
+ Ref acc_ref = build_full(std::vector<Ref>(refs.begin(), refs.end() - 1), bop, cls, pre);
+ make_bin(root, bop, acc_ref, refs.back());
+ }
+ if (static_cast<int>(refs.size()) > n_chainmax)
+ n_chainmax = static_cast<int>(refs.size());
+ }
+
+ void process_root(Ins* root) {
+ Family fam = family_of(root->op);
+ int cls = static_cast<int>(root->cls);
+ size_t before = absorbed.size();
+ std::vector<Leaf> leaves;
+ collect(root, fam, cls, leaves);
+ size_t delta = absorbed.size() - before;
+
+ // Act on trees.
+ int nconst = 0;
+ for (const Leaf& lf : leaves) {
+ int64_t v;
+ if (isconbits(fn, lf.ref, &v))
+ nconst++;
+ }
+ if (delta == 0 && nconst < 2)
+ return;
+
+ n_trees++;
+ if (fam == FAdd)
+ rebuild_addsub(root, cls, leaves);
+ else
+ rebuild_logic(root, fam, cls, leaves);
+ }
+
+ void run() {
+ // Collect root candidates.
+ std::vector<Ins*> roots;
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (rtype(i->to) != RTmp)
+ continue;
+ Family fam = family_of(i->op);
+ if (fam == FNone)
+ continue;
+ if (KBASE(i->cls) != 0)
+ continue; // integer only
+ // Check root.
+ Tmp* t = &fn->tmp[i->to.val];
+ if (t->nuse == 1 && t->use && t->use[0].type == Use::UIns) {
+ Ins* parent = t->use[0].u.ins;
+ if (is_family_node(parent, fam, static_cast<int>(i->cls)))
+ continue;
+ }
+ roots.push_back(i);
+ }
+ }
+
+ // Process each root.
+ for (Ins* r : roots) {
+ if (absorbed.count(r))
+ continue;
+ process_root(r);
+ }
+
+ // Mark absorbed intermediate nodes as Onop.
+ for (Ins* a : absorbed)
+ a->op = Onop;
+
+ // Reconstruct.
+ for (Blk* b = fn->start; b; b = b->link) {
+ Ins* nins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn));
+ uint cnt = 0;
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (i->op == Onop)
+ continue;
+ auto it = prelude.find(i);
+ if (it != prelude.end())
+ for (Ins& p : it->second)
+ addins(ctx, &nins, &cnt, &p);
+ addins(ctx, &nins, &cnt, i);
+ }
+ b->ins = nins;
+ b->nins = cnt;
+ }
+
+ if (debug['E']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> reassoc %s: trees=%d folded=%d addsub=%d chain_max=%d\n", fn->name,
+ n_trees, n_folded, n_addsub, n_chainmax);
+ }
+ }
+};
+
+} // namespace
+
+void reassoc(Ctx* ctx, Fn* fn) {
+ // Pre/post conditions.
+ Reassoc r;
+ r.ctx = ctx;
+ r.fn = fn;
+ r.run();
+}
+
+} // namespace rcc
diff --git a/src/mid/reduction_unroll.cc b/src/mid/reduction_unroll.cc
new file mode 100644
index 0000000..08edb3d
--- /dev/null
+++ b/src/mid/reduction_unroll.cc
@@ -0,0 +1,528 @@
+/* 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 reduction_unroll.cc
+/// @brief Identifies inner loop integer reductions and unrolls them into K-way parallel
+/// accumulators.
+///
+/// See docs/reduction_unroll.md for architectural notes.
+
+#include "rcc/all.h"
+#include "rcc/loopinfo.h"
+
+#include <algorithm>
+#include <cstring>
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+constexpr int kUnrollK = 4;
+
+int load_bytes(int op, int cls) {
+ switch (op) {
+ case Oloadsb:
+ case Oloadub:
+ return 1;
+ case Oloadsh:
+ case Oloaduh:
+ return 2;
+ case Oloadsw:
+ case Oloaduw:
+ return 4;
+ case Oload:
+ return (cls == Kl || cls == Kd) ? 8 : 4;
+ default:
+ return 0;
+ }
+}
+
+bool is_int_reduce_op(int op) {
+ return op == Oadd || op == Omul || op == Oand || op == Oor || op == Oxor;
+}
+
+int64_t identity_for(int op) {
+ switch (op) {
+ case Oadd:
+ case Oor:
+ case Oxor:
+ return 0;
+ case Omul:
+ return 1;
+ case Oand:
+ return -1;
+ default:
+ return 0;
+ }
+}
+
+bool is_int_cls(int cls) {
+ return cls == Kw || cls == Kl;
+}
+
+struct Match {
+ PointerIV iv_p; // pointer IV
+ Phi* phi_p; // pointer phi
+ Phi* phi_s; // accumulator phi
+ Ref s_init; // accumulator init (preheader side)
+ Ref s_next; // accumulator next (latch side, = s_op.to)
+ Ins* load_ins; // load inside body
+ Ins* op_ins; // reduce op inside body (s.next = OP s, v)
+ Ins* p_inc_ins; // pointer increment inside body
+ Ref end; // end ref for exit cmp
+ Blk* after; // exit block
+ int op; // op_ins->op
+ int s_cls; // accumulator and op cls
+ int load_op; // load_ins->op
+ int load_cls; // load_ins->cls
+};
+
+// Check latch body.
+bool scan_body(Fn* /*fn*/, const Loop& L, const PointerIV& iv_p, Phi* phi_s, Match* m) {
+ m->load_ins = nullptr;
+ m->op_ins = nullptr;
+ m->p_inc_ins = nullptr;
+
+ Ref p = iv_p.phi_to;
+ Ref s = phi_s->to;
+ Ref s_next = Ref::none(), p_next = iv_p.next;
+ for (uint k = 0; k < phi_s->narg; k++)
+ if (phi_s->blk[k] == L.latch)
+ s_next = phi_s->arg[k];
+ if (rtype(s_next) != RTmp)
+ return false;
+
+ for (Blk* bb : L.body) {
+ for (uint k = 0; k < bb->nins; k++) {
+ Ins* i = &bb->ins[k];
+ int op = i->op;
+ // header allows exit cmp (guaranteed unique by check_exit_cmp)
+ if (bb == L.header && req(i->to, L.header->jmp.arg))
+ continue;
+ if (op == Onop)
+ continue;
+ if (op == Ocall || op == Oalloc4 || op == Oalloc8 || op == Oalloc16 || op == Ovaarg ||
+ op == Ovastart || op == Odbgloc || op == Osel0 || op == Osel1 || op == Oblit0 ||
+ op == Oblit1 || op == Osalloc || op == Oudiv || op == Ourem || op == Odiv ||
+ op == Orem)
+ return false;
+ // pointer increment?
+ if (req(i->to, p_next)) {
+ if (m->p_inc_ins)
+ return false;
+ m->p_inc_ins = i;
+ continue;
+ }
+ // accumulator increment?
+ if (req(i->to, s_next)) {
+ if (m->op_ins)
+ return false;
+ if (!is_int_reduce_op(op))
+ return false;
+ if (!is_int_cls(static_cast<int>(i->cls)))
+ return false;
+ // Must be OP(s, v) or OP(v, s)
+ if (!req(i->arg[0], s) && !req(i->arg[1], s))
+ return false;
+ m->op_ins = i;
+ continue;
+ }
+ // Is it a load?
+ int lb = load_bytes(op, static_cast<int>(i->cls));
+ if (lb > 0) {
+ if (m->load_ins)
+ return false;
+ if (!req(i->arg[0], p))
+ return false;
+ m->load_ins = i;
+ continue;
+ }
+ // Reject any other ins (reduction form requires a clean body)
+ return false;
+ }
+ }
+ if (!m->load_ins || !m->op_ins || !m->p_inc_ins)
+ return false;
+ // Loaded value must equal the other operand of the OP
+ Ref v_ref = m->load_ins->to;
+ if (!(req(m->op_ins->arg[0], v_ref) || req(m->op_ins->arg[1], v_ref)))
+ return false;
+ // class & width
+ int lb = load_bytes(m->load_ins->op, static_cast<int>(m->load_ins->cls));
+ if (lb != iv_p.step)
+ return false;
+ if (m->op_ins->cls != m->load_ins->cls)
+ return false;
+ m->s_next = s_next;
+ return true;
+}
+
+bool try_match_loop(Fn* fn, const Loop& L, Match* m) {
+ if (L.body.size() > 2)
+ return false;
+ if (!L.preheader)
+ return false;
+ if (L.preheader->jmp.type != Jjmp)
+ return false;
+ if (L.preheader->s1 != L.header || L.preheader->s2 != nullptr)
+ return false;
+
+ // Collect header phis.
+ PointerIV iv_p{};
+ Phi* phi_p = nullptr;
+ Phi* phi_s = nullptr;
+ int n_ptr = 0, n_acc = 0;
+ for (Phi* ph = L.header->phi; ph; ph = ph->link) {
+ PointerIV cand{};
+ if (pick_pointer_iv(fn, L, ph, &cand)) {
+ iv_p = cand;
+ phi_p = ph;
+ n_ptr++;
+ continue;
+ }
+ if (ph->narg == 2 && is_int_cls(static_cast<int>(ph->cls))) {
+ phi_s = ph;
+ n_acc++;
+ continue;
+ }
+ return false; // Reject any third type of phi
+ }
+ if (n_ptr != 1 || n_acc != 1)
+ return false;
+
+ // accumulator phi incoming edges
+ Ref s_init = Ref::none();
+ for (uint k = 0; k < phi_s->narg; k++) {
+ if (phi_s->blk[k] == L.preheader)
+ s_init = phi_s->arg[k];
+ else if (phi_s->blk[k] != L.latch)
+ return false;
+ }
+ if (rtype(s_init) == -1)
+ return false;
+
+ Ref end_ref;
+ Blk* after = nullptr;
+ if (!check_exit_cmp(fn, L, iv_p.phi_to, &end_ref, &after))
+ return false;
+
+ if (!scan_body(fn, L, iv_p, phi_s, m))
+ return false;
+
+ // accumulator cls == op cls, must match load cls (same bitwidth)
+ if (phi_s->cls != m->op_ins->cls)
+ return false;
+
+ if (has_outside_use(fn, L)) {
+ // Check outside loop uses strictly.
+ std::unordered_set<uint> loop_def;
+ for (Blk* bb : L.body) {
+ for (Phi* ph = bb->phi; ph; ph = ph->link)
+ if (rtype(ph->to) == RTmp)
+ loop_def.insert(ph->to.val);
+ for (uint k = 0; k < bb->nins; k++)
+ if (rtype(bb->ins[k].to) == RTmp)
+ loop_def.insert(bb->ins[k].to.val);
+ }
+ loop_def.erase(phi_p->to.val);
+ loop_def.erase(phi_s->to.val);
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (L.body_bid.count(b->id))
+ continue;
+ for (Phi* ph = b->phi; ph; ph = ph->link)
+ for (uint k = 0; k < ph->narg; k++)
+ if (rtype(ph->arg[k]) == RTmp && loop_def.count(ph->arg[k].val))
+ return false;
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ for (int a = 0; a < 2; a++)
+ if (rtype(i->arg[a]) == RTmp && loop_def.count(i->arg[a].val))
+ return false;
+ }
+ if (rtype(b->jmp.arg) == RTmp && loop_def.count(b->jmp.arg.val))
+ return false;
+ if (rtype(b->jmp.arg2) == RTmp && loop_def.count(b->jmp.arg2.val))
+ return false;
+ }
+ }
+
+ m->iv_p = iv_p;
+ m->phi_p = phi_p;
+ m->phi_s = phi_s;
+ m->s_init = s_init;
+ m->end = end_ref;
+ m->after = after;
+ m->op = m->op_ins->op;
+ m->s_cls = m->op_ins->cls;
+ m->load_op = m->load_ins->op;
+ m->load_cls = static_cast<int>(m->load_ins->cls);
+ return true;
+}
+
+Phi* make_phi(Ctx* ctx, int cls, Ref to, Ref arg_pre, Blk* pre, Ref arg_back, Blk* back) {
+ Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ p->cls = static_cast<short>(cls);
+ p->visit = 0;
+ p->to = to;
+ p->narg = 2;
+ p->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn));
+ p->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn));
+ p->arg[0] = arg_pre;
+ p->blk[0] = pre;
+ p->arg[1] = arg_back;
+ p->blk[1] = back;
+ p->link = nullptr;
+ return p;
+}
+
+static int rd_seq = 0;
+
+void execute(Ctx* ctx, Fn* fn, const Loop& L, const Match& m) {
+ int K = kUnrollK;
+ int S = static_cast<int>(m.iv_p.step);
+ int KS = K * S;
+ int my_seq = rd_seq++;
+
+ Blk* pre = L.preheader;
+ Blk* tail_hdr = L.header;
+ int s_cls = m.s_cls;
+ int load_op = m.load_op;
+ int load_cls = m.load_cls;
+ int op = m.op;
+
+ // Append size / size_main / end_main to the end of preheader
+ Ref size_ref = newtmp(ctx, "rd.size", Kl, fn);
+ Ref size_main_ref = newtmp(ctx, "rd.szm", Kl, fn);
+ Ref end_main_ref = newtmp(ctx, "rd.endm", Kl, fn);
+
+ Ins sub_i{};
+ sub_i.op = Osub;
+ sub_i.cls = Kl;
+ sub_i.to = size_ref;
+ sub_i.arg[0] = m.end;
+ sub_i.arg[1] = m.iv_p.base;
+ addins(ctx, &pre->ins, &pre->nins, &sub_i);
+
+ Ins and_i{};
+ and_i.op = Oand;
+ and_i.cls = Kl;
+ and_i.to = size_main_ref;
+ and_i.arg[0] = size_ref;
+ and_i.arg[1] = getcon(ctx, -static_cast<int64_t>(KS), fn);
+ addins(ctx, &pre->ins, &pre->nins, &and_i);
+
+ Ins add_i{};
+ add_i.op = Oadd;
+ add_i.cls = Kl;
+ add_i.to = end_main_ref;
+ add_i.arg[0] = m.iv_p.base;
+ add_i.arg[1] = size_main_ref;
+ addins(ctx, &pre->ins, &pre->nins, &add_i);
+
+ // Create main_header / main_body / main_join
+ Blk* mh = newblk(ctx);
+ mh->name = strf(ctx, PFn, "rd%d.mh", my_seq);
+ mh->id = fn->nblk++;
+
+ Blk* mb = newblk(ctx);
+ mb->name = strf(ctx, PFn, "rd%d.mb", my_seq);
+ mb->id = fn->nblk++;
+
+ Blk* mj = newblk(ctx);
+ mj->name = strf(ctx, PFn, "rd%d.mj", my_seq);
+ mj->id = fn->nblk++;
+
+ // Pre-generate names (main_header phi.to, etc.)
+ Ref pm_ref = newtmp(ctx, "rd.pm", Kl, fn);
+ Ref pm_next_ref = newtmp(ctx, "rd.pm.n", Kl, fn);
+ std::vector<Ref> s_refs(static_cast<size_t>(K)), s_next_refs(static_cast<size_t>(K));
+ for (size_t i = 0; i < static_cast<size_t>(K); i++) {
+ s_refs[i] = newtmp(ctx, "rd.s", s_cls, fn);
+ s_next_refs[i] = newtmp(ctx, "rd.s.n", s_cls, fn);
+ }
+
+ // Identity ref
+ Ref ident_ref = getcon(ctx, identity_for(op), fn);
+
+ // main_header phi chain: pm + s0..K-1
+ Phi* phi_pm = make_phi(ctx, Kl, pm_ref, m.iv_p.base, pre, pm_next_ref, mb);
+ mh->phi = phi_pm;
+ Phi* tail_phi = phi_pm;
+ for (size_t i = 0; i < static_cast<size_t>(K); i++) {
+ Ref init = (i == 0) ? m.s_init : ident_ref;
+ Phi* ph = make_phi(ctx, s_cls, s_refs[i], init, pre, s_next_refs[i], mb);
+ tail_phi->link = ph;
+ tail_phi = ph;
+ }
+
+ // main_header cmp + jnz
+ Ref cmpm_ref = newtmp(ctx, "rd.cmpm", Kw, fn);
+ Ins cmpm_i{};
+ cmpm_i.op = Ocultl;
+ cmpm_i.cls = Kw;
+ cmpm_i.to = cmpm_ref;
+ cmpm_i.arg[0] = pm_ref;
+ cmpm_i.arg[1] = end_main_ref;
+ addins(ctx, &mh->ins, &mh->nins, &cmpm_i);
+ mh->jmp.type = Jjnz;
+ mh->jmp.arg = cmpm_ref;
+ mh->jmp.arg2 = Ref::none();
+ mh->s1 = mb;
+ mh->s2 = mj;
+
+ // main_body: K loads + K ops + 1 pointer step
+ std::vector<Ref> v_refs(static_cast<size_t>(K));
+ for (size_t i = 0; i < static_cast<size_t>(K); i++) {
+ Ref addr;
+ if (i == 0) {
+ addr = pm_ref;
+ } else {
+ addr = newtmp(ctx, "rd.adr", Kl, fn);
+ Ins ai{};
+ ai.op = Oadd;
+ ai.cls = Kl;
+ ai.to = addr;
+ ai.arg[0] = pm_ref;
+ ai.arg[1] = getcon(ctx, static_cast<int64_t>(i) * S, fn);
+ addins(ctx, &mb->ins, &mb->nins, &ai);
+ }
+ v_refs[i] = newtmp(ctx, "rd.v", load_cls, fn);
+ Ins li{};
+ li.op = static_cast<uint>(load_op);
+ li.cls = static_cast<uint>(load_cls) & 3u;
+ li.to = v_refs[i];
+ li.arg[0] = addr;
+ li.arg[1] = Ref::none();
+ addins(ctx, &mb->ins, &mb->nins, &li);
+ }
+ for (size_t i = 0; i < static_cast<size_t>(K); i++) {
+ Ins oi{};
+ oi.op = static_cast<uint>(op);
+ oi.cls = static_cast<uint>(s_cls) & 3u;
+ oi.to = s_next_refs[i];
+ oi.arg[0] = s_refs[i];
+ oi.arg[1] = v_refs[i];
+ addins(ctx, &mb->ins, &mb->nins, &oi);
+ }
+ Ins pn_i{};
+ pn_i.op = Oadd;
+ pn_i.cls = Kl;
+ pn_i.to = pm_next_ref;
+ pn_i.arg[0] = pm_ref;
+ pn_i.arg[1] = getcon(ctx, static_cast<int64_t>(KS), fn);
+ addins(ctx, &mb->ins, &mb->nins, &pn_i);
+ mb->jmp.type = Jjmp;
+ mb->jmp.arg = Ref::none();
+ mb->jmp.arg2 = Ref::none();
+ mb->s1 = mh;
+ mb->s2 = nullptr;
+
+ // main_join: log2(K) tree reduction
+ Ref s01_ref = newtmp(ctx, "rd.s01", s_cls, fn);
+ Ref s23_ref = newtmp(ctx, "rd.s23", s_cls, fn);
+ Ref s_main_ref = newtmp(ctx, "rd.sm", s_cls, fn);
+
+ Ins j1{};
+ j1.op = static_cast<uint>(op);
+ j1.cls = static_cast<uint>(s_cls) & 3u;
+ j1.to = s01_ref;
+ j1.arg[0] = s_refs[0];
+ j1.arg[1] = s_refs[1];
+ addins(ctx, &mj->ins, &mj->nins, &j1);
+
+ Ins j2{};
+ j2.op = static_cast<uint>(op);
+ j2.cls = static_cast<uint>(s_cls) & 3u;
+ j2.to = s23_ref;
+ j2.arg[0] = s_refs[2];
+ j2.arg[1] = s_refs[3];
+ addins(ctx, &mj->ins, &mj->nins, &j2);
+
+ Ins j3{};
+ j3.op = static_cast<uint>(op);
+ j3.cls = static_cast<uint>(s_cls) & 3u;
+ j3.to = s_main_ref;
+ j3.arg[0] = s01_ref;
+ j3.arg[1] = s23_ref;
+ addins(ctx, &mj->ins, &mj->nins, &j3);
+
+ mj->jmp.type = Jjmp;
+ mj->jmp.arg = Ref::none();
+ mj->jmp.arg2 = Ref::none();
+ mj->s1 = tail_hdr;
+ mj->s2 = nullptr;
+
+ // Change preheader.s1 -> main_header
+ pre->s1 = mh;
+
+ // Link new blocks.
+ mj->link = pre->link;
+ mb->link = mj;
+ mh->link = mb;
+ pre->link = mh;
+
+ // Update tail_header phis.
+ for (Phi* ph = tail_hdr->phi; ph; ph = ph->link) {
+ for (uint k = 0; k < ph->narg; k++) {
+ if (ph->blk[k] != pre)
+ continue;
+ ph->blk[k] = mj;
+ if (req(ph->to, m.phi_p->to))
+ ph->arg[k] = pm_ref;
+ else if (req(ph->to, m.phi_s->to))
+ ph->arg[k] = s_main_ref;
+ break;
+ }
+ }
+}
+
+} // namespace
+
+void reduction_unroll(Ctx* ctx, Fn* fn) {
+ std::vector<Loop> loops;
+ find_loops(fn, loops);
+ if (loops.empty())
+ return;
+
+ // Inner loops first (smaller body first)
+ std::sort(loops.begin(), loops.end(),
+ [](const Loop& a, const Loop& b) { return a.body.size() < b.body.size(); });
+
+ std::unordered_set<uint> consumed;
+ for (const Loop& L : loops) {
+ bool clash = false;
+ for (Blk* bb : L.body)
+ if (consumed.count(bb->id)) {
+ clash = true;
+ break;
+ }
+ if (clash)
+ continue;
+ Match m{};
+ if (!try_match_loop(fn, L, &m))
+ continue;
+ execute(ctx, fn, L, m);
+ for (Blk* bb : L.body)
+ consumed.insert(bb->id);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/scalar_repl.cc b/src/mid/scalar_repl.cc
new file mode 100644
index 0000000..04c0a82
--- /dev/null
+++ b/src/mid/scalar_repl.cc
@@ -0,0 +1,528 @@
+/* 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 scalar_repl.cc
+/// @brief Converts in-loop memory accumulators into a register + phi node.
+///
+/// See docs/scalar_repl.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cstring>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+Ins make_nop_ins() {
+ Ins i = {};
+ i.op = Onop;
+ return i;
+}
+
+struct LoopInfo {
+ Blk* header;
+ std::vector<Blk*> body;
+ Blk* preheader;
+ Blk* latch;
+ Blk* exit_blk;
+ Blk* exit_pred; // exit_blk's only pred inside the body
+};
+
+bool blk_in(Blk* x, const std::vector<Blk*>& body) {
+ for (Blk* b : body)
+ if (b == x)
+ return true;
+ return false;
+}
+
+bool find_loops(Fn* fn, std::vector<LoopInfo>& out) {
+ out.clear();
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = ~0u;
+
+ for (uint i = 0; i < fn->nblk; i++) {
+ Blk* hd = fn->rpo[i];
+ bool has_back = false;
+ for (uint k = 0; k < hd->npred; k++)
+ if (hd->pred[k]->id >= i) {
+ has_back = true;
+ break;
+ }
+ if (!has_back)
+ continue;
+
+ LoopInfo L{hd, {hd}, nullptr, nullptr, nullptr, nullptr};
+ std::vector<Blk*> stack;
+ for (uint k = 0; k < hd->npred; k++)
+ if (hd->pred[k]->id >= i)
+ stack.push_back(hd->pred[k]);
+ while (!stack.empty()) {
+ Blk* n = stack.back();
+ stack.pop_back();
+ if (n->id < hd->id || n->visit == hd->id)
+ continue;
+ n->visit = hd->id;
+ L.body.push_back(n);
+ for (uint k = 0; k < n->npred; k++)
+ stack.push_back(n->pred[k]);
+ }
+
+ Blk* phdr = nullptr;
+ Blk* latch = nullptr;
+ bool unique = true;
+ for (uint k = 0; k < hd->npred; k++) {
+ Blk* p = hd->pred[k];
+ if (blk_in(p, L.body)) {
+ if (latch) {
+ unique = false;
+ break;
+ }
+ latch = p;
+ } else {
+ if (phdr) {
+ unique = false;
+ break;
+ }
+ phdr = p;
+ }
+ }
+ if (!unique || !phdr || !latch)
+ continue;
+ L.preheader = phdr;
+ L.latch = latch;
+
+ Blk* exit_blk = nullptr;
+ bool exit_unique = true;
+ for (Blk* b : L.body) {
+ Blk* succs[2] = {b->s1, b->s2};
+ for (Blk* s : succs) {
+ if (!s || blk_in(s, L.body))
+ continue;
+ if (exit_blk && exit_blk != s) {
+ exit_unique = false;
+ break;
+ }
+ exit_blk = s;
+ }
+ if (!exit_unique)
+ break;
+ }
+ if (!exit_unique || !exit_blk)
+ continue;
+ int from_loop = 0;
+ Blk* exit_pred = nullptr;
+ for (uint k = 0; k < exit_blk->npred; k++)
+ if (blk_in(exit_blk->pred[k], L.body)) {
+ from_loop++;
+ exit_pred = exit_blk->pred[k];
+ }
+ if (from_loop != 1)
+ continue;
+ L.exit_blk = exit_blk;
+ L.exit_pred = exit_pred;
+
+ out.push_back(std::move(L));
+ }
+ return !out.empty();
+}
+
+// Recursively check address invariance.
+bool is_pure_arith(int op) {
+ switch (op) {
+ case Oadd:
+ case Osub:
+ case Omul:
+ case Oand:
+ case Oor:
+ case Oxor:
+ case Oshl:
+ case Oshr:
+ case Osar:
+ case Oextsw:
+ case Oextuw:
+ case Oextsh:
+ case Oextuh:
+ case Oextsb:
+ case Oextub:
+ case Ocopy:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Find phi node for t in a blk (t->def doesn't point to phi, must scan).
+Phi* find_phi(Fn* fn, Ref r) {
+ if (rtype(r) != RTmp)
+ return nullptr;
+ Tmp* t = &fn->tmp[r.val];
+ if (t->bid == ~0u)
+ return nullptr;
+ Blk* b = fn->rpo[t->bid];
+ for (Phi* p = b->phi; p; p = p->link)
+ if (req(p->to, r))
+ return p;
+ return nullptr;
+}
+
+// Each incoming phi arg must be invariant. Cannot do optimistic cycle back-edge.
+bool phi_invariant(Fn* fn, Phi* p, const LoopInfo& L) {
+ for (uint k = 0; k < p->narg; k++) {
+ Ref a = p->arg[k];
+ if (req(a, p->to))
+ continue;
+ if (rtype(a) == RCon)
+ continue;
+ if (rtype(a) != RTmp)
+ return false;
+ Tmp* t = &fn->tmp[a.val];
+ if (t->bid == ~0u)
+ continue;
+ if (blk_in(fn->rpo[t->bid], L.body))
+ return false;
+ }
+ return true;
+}
+
+bool addr_invariant_rec(Fn* fn, Ref addr, const LoopInfo& L, int depth) {
+ if (rtype(addr) == RCon)
+ return true;
+ if (rtype(addr) != RTmp)
+ return false;
+ Tmp* t = &fn->tmp[addr.val];
+ if (t->bid == ~0u)
+ return true;
+ if (!blk_in(fn->rpo[t->bid], L.body))
+ return true;
+ if (depth <= 0)
+ return false;
+ if (Phi* p = find_phi(fn, addr))
+ return phi_invariant(fn, p, L);
+ Ins* d = t->def;
+ if (!d || !is_pure_arith(static_cast<int>(d->op)))
+ return false;
+ if (!addr_invariant_rec(fn, d->arg[0], L, depth - 1))
+ return false;
+ if (!req(d->arg[1], Ref::none()) && !addr_invariant_rec(fn, d->arg[1], L, depth - 1))
+ return false;
+ return true;
+}
+
+bool addr_invariant(Fn* fn, Ref addr, const LoopInfo& L) {
+ return addr_invariant_rec(fn, addr, L, 8);
+}
+
+// Structural equivalence: two Ref chains compute the same value.
+bool addr_same_value(Fn* fn, Ref a, Ref b, int depth = 8) {
+ if (req(a, b))
+ return true;
+ if (rtype(a) == RCon && rtype(b) == RCon) {
+ Con* ca = &fn->con[a.val];
+ Con* cb = &fn->con[b.val];
+ if (ca->type != cb->type)
+ return false;
+ if (ca->type == Con::CAddr && !symeq(ca->sym, cb->sym))
+ return false;
+ return ca->bits.i == cb->bits.i;
+ }
+ if (rtype(a) != RTmp || rtype(b) != RTmp)
+ return false;
+ if (depth <= 0)
+ return false;
+ Tmp* ta = &fn->tmp[a.val];
+ Tmp* tb = &fn->tmp[b.val];
+ Ins* da = ta->def;
+ Ins* db = tb->def;
+ if (!da || !db)
+ return false;
+ if (da->op != db->op)
+ return false;
+ if (!is_pure_arith(static_cast<int>(da->op)))
+ return false;
+ if (!addr_same_value(fn, da->arg[0], db->arg[0], depth - 1))
+ return false;
+ if (req(da->arg[1], Ref::none()) && req(db->arg[1], Ref::none()))
+ return true;
+ return addr_same_value(fn, da->arg[1], db->arg[1], depth - 1);
+}
+
+// Clone invariant address chain to preheader.
+Ref clone_invariant_to_preheader(Ctx* ctx, Fn* fn, Ref r, const LoopInfo& L,
+ std::vector<std::pair<uint, Ref>>& cache, int depth = 8) {
+ if (rtype(r) == RCon)
+ return r;
+ if (rtype(r) != RTmp)
+ return Ref::none();
+ Tmp* t = &fn->tmp[r.val];
+ if (t->bid == ~0u || !blk_in(fn->rpo[t->bid], L.body))
+ return r; // Already defined outside loop, reuse directly
+ if (depth <= 0)
+ return Ref::none();
+ for (auto& kv : cache)
+ if (kv.first == r.val)
+ return kv.second;
+
+ Phi* p = find_phi(fn, r);
+ if (p) {
+ // Look for preheader-side arg in phi.blk[]
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == L.preheader) {
+ Ref v = clone_invariant_to_preheader(ctx, fn, p->arg[k], L, cache, depth - 1);
+ if (rtype(v) == -1)
+ return Ref::none();
+ cache.push_back(std::make_pair(static_cast<uint>(r.val), v));
+ return v;
+ }
+ return Ref::none();
+ }
+ Ins* d = t->def;
+ if (!d || !is_pure_arith(static_cast<int>(d->op)))
+ return Ref::none();
+ Ref a0 = clone_invariant_to_preheader(ctx, fn, d->arg[0], L, cache, depth - 1);
+ if (rtype(a0) == -1)
+ return Ref::none();
+ Ref a1 = Ref::none();
+ if (!req(d->arg[1], Ref::none())) {
+ a1 = clone_invariant_to_preheader(ctx, fn, d->arg[1], L, cache, depth - 1);
+ if (rtype(a1) == -1)
+ return Ref::none();
+ }
+ Ref nt = newtmp(ctx, "sr.addr", static_cast<int>(d->cls), fn);
+ Ins ni{};
+ ni.op = d->op;
+ ni.cls = d->cls;
+ ni.to = nt;
+ ni.arg[0] = a0;
+ ni.arg[1] = a1;
+ addins(ctx, &L.preheader->ins, &L.preheader->nins, &ni);
+ cache.push_back(std::make_pair(static_cast<uint>(r.val), nt));
+ return nt;
+}
+
+// Globally replace all ref==from with to (in phi.arg / ins.arg / jmp.arg / jmp.arg2).
+void rewrite_ref(Fn* fn, Ref from, Ref to) {
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Phi* p = b->phi; p; p = p->link)
+ for (uint k = 0; k < p->narg; k++)
+ if (req(p->arg[k], from))
+ p->arg[k] = to;
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ if (req(i->arg[0], from))
+ i->arg[0] = to;
+ if (req(i->arg[1], from))
+ i->arg[1] = to;
+ }
+ if (req(b->jmp.arg, from))
+ b->jmp.arg = to;
+ if (req(b->jmp.arg2, from))
+ b->jmp.arg2 = to;
+ }
+}
+
+void prepend_ins(Ctx* ctx, Blk* b, const Ins& ins) {
+ vgrow(ctx, &b->ins, b->nins + 1);
+ std::memmove(b->ins + 1, b->ins, b->nins * sizeof(Ins));
+ b->ins[0] = ins;
+ b->nins += 1;
+}
+
+// Try one promotion on L. Returns true on hit; outer loop recomputes use/dom/alias.
+bool try_promote(Ctx* ctx, Fn* fn, const LoopInfo& L) {
+ for (Blk* b : L.body) {
+ for (Ins* iload = b->ins; iload < &b->ins[b->nins]; iload++) {
+ if (!isload(iload->op))
+ continue;
+ Ref addr = iload->arg[0];
+ if (!addr_invariant(fn, addr, L))
+ continue;
+ int sz = loadsz(iload);
+
+ std::vector<Ins*> matched_loads;
+ Blk* store_blk = nullptr;
+ Ins* matched_store = nullptr;
+ int store_count = 0;
+ bool ok = true;
+
+ // Three-tier classification: structurally identical -> matched, NoAlias -> skip,
+ // MayAlias -> bail.
+ auto classify = [&](Ref jaddr, int jsz) -> int {
+ // ret: 0=matched, 1=skip(NoAlias), 2=bail
+ if (jsz != sz)
+ return 2; // Different size cannot be promoted (conservative)
+ if (addr_same_value(fn, addr, jaddr))
+ return 0;
+ int delta;
+ int r = alias(addr, 0, sz, jaddr, jsz, &delta, fn);
+ if (r == NoAlias)
+ return 1;
+ if (r == MustAlias && delta == 0)
+ return 0;
+ return 2;
+ };
+
+ for (Blk* bb : L.body) {
+ for (Ins* j = bb->ins; j < &bb->ins[bb->nins]; j++) {
+ if (j->op == Ocall || j->op == Oblit0 || j->op == Oblit1) {
+ ok = false;
+ break;
+ }
+ if (isload(j->op)) {
+ int jsz = loadsz(j);
+ // Different size but NoAlias can still be skipped
+ if (jsz != sz) {
+ int delta;
+ int r = alias(addr, 0, sz, j->arg[0], jsz, &delta, fn);
+ if (r != NoAlias) {
+ ok = false;
+ break;
+ }
+ continue;
+ }
+ int c = classify(j->arg[0], jsz);
+ if (c == 0)
+ matched_loads.push_back(j);
+ else if (c == 2) {
+ ok = false;
+ break;
+ }
+ } else if (isstore(j->op)) {
+ int jsz = storesz(j);
+ if (jsz != sz) {
+ int delta;
+ int r = alias(addr, 0, sz, j->arg[1], jsz, &delta, fn);
+ if (r != NoAlias) {
+ ok = false;
+ break;
+ }
+ continue;
+ }
+ int c = classify(j->arg[1], jsz);
+ if (c == 0) {
+ store_count++;
+ if (store_count > 1) {
+ ok = false;
+ break;
+ }
+ matched_store = j;
+ store_blk = bb;
+ } else if (c == 2) {
+ ok = false;
+ break;
+ }
+ }
+ }
+ if (!ok)
+ break;
+ }
+ if (!ok || store_count != 1)
+ continue;
+ // Store must be must-execute.
+ if (!dom(store_blk, L.latch))
+ continue;
+ if (!dom(store_blk, L.exit_pred))
+ continue;
+
+ int cls = static_cast<int>(iload->cls);
+
+ // Clone address chain to preheader, then issue load.
+ std::vector<std::pair<uint, Ref>> addr_cache;
+ Ref pre_addr = clone_invariant_to_preheader(ctx, fn, addr, L, addr_cache);
+ if (rtype(pre_addr) == -1)
+ continue; // Clone failed (rare corner case)
+ Ref init_ref = newtmp(ctx, "sr.init", cls, fn);
+ Ins pre_ld{};
+ pre_ld.op = iload->op;
+ pre_ld.cls = static_cast<uint>(cls);
+ pre_ld.to = init_ref;
+ pre_ld.arg[0] = pre_addr;
+ pre_ld.arg[1] = Ref::none();
+ addins(ctx, &L.preheader->ins, &L.preheader->nins, &pre_ld);
+
+ // Insert phi at header top.
+ Ref back_val = matched_store->arg[0];
+ Ref phi_ref = newtmp(ctx, "sr.phi", cls, fn);
+ Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ std::memset(p, 0, sizeof *p);
+ p->to = phi_ref;
+ p->cls = static_cast<short>(cls);
+ p->narg = 2;
+ p->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn));
+ p->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn));
+ p->arg[0] = init_ref;
+ p->blk[0] = L.preheader;
+ p->arg[1] = back_val;
+ p->blk[1] = L.latch;
+ p->link = L.header->phi;
+ L.header->phi = p;
+
+ // Change all matched_loads def tmps to phi_ref; original loads become Onop.
+ for (Ins* ml : matched_loads) {
+ rewrite_ref(fn, ml->to, phi_ref);
+ *ml = make_nop_ins();
+ }
+
+ // Sink store at top of exit_blk.
+ Ins sink{};
+ sink.op = matched_store->op;
+ sink.cls = matched_store->cls;
+ sink.to = Ref::none();
+ sink.arg[0] = phi_ref;
+ sink.arg[1] = pre_addr;
+ prepend_ins(ctx, L.exit_blk, sink);
+
+ // Original store becomes Onop.
+ *matched_store = make_nop_ins();
+
+ return true;
+ }
+ }
+ return false;
+}
+
+} // namespace
+
+void scalar_repl(Ctx* ctx, Fn* fn) {
+ int total = 0;
+ for (int iter = 0; iter < 16; iter++) {
+ std::vector<LoopInfo> loops;
+ if (!find_loops(fn, loops))
+ break;
+ bool changed = false;
+ for (const LoopInfo& L : loops) {
+ if (try_promote(ctx, fn, L)) {
+ changed = true;
+ total++;
+ break;
+ }
+ }
+ if (!changed)
+ break;
+ filluse(ctx, fn);
+ filldom(fn);
+ fillalias(fn);
+ }
+ if (debug['L']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "\n> scalar-repl promoted %d location(s) in %s\n", total, fn->name);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/sccp.cc b/src/mid/sccp.cc
new file mode 100644
index 0000000..537773a
--- /dev/null
+++ b/src/mid/sccp.cc
@@ -0,0 +1,449 @@
+/* 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 sccp.cc
+/// @brief Sparse Conditional Constant Propagation (Wegman-Zadeck).
+///
+/// See docs/sccp.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cassert>
+#include <cstdio>
+#include <set>
+#include <utility>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+enum LatState : uchar { Top = 0, Const = 1, Bot = 2 };
+
+struct Lat {
+ LatState state;
+ Ref con; // Only valid when state==Const, type==RCon
+};
+
+constexpr Lat kTop = {Top, {RTmp, 0}};
+constexpr Lat kBot = {Bot, {RTmp, 0}};
+
+bool con_eq(Fn* fn, Ref a, Ref b) {
+ if (req(a, b))
+ return true;
+ if (rtype(a) != RCon || rtype(b) != RCon)
+ return false;
+ if (a.val >= static_cast<uint>(fn->ncon) || b.val >= static_cast<uint>(fn->ncon))
+ return false;
+ Con* ca = &fn->con[a.val];
+ Con* cb = &fn->con[b.val];
+ if (ca->type != cb->type)
+ return false;
+ if (ca->type == Con::CAddr && !symeq(ca->sym, cb->sym))
+ return false;
+ return ca->bits.i == cb->bits.i && ca->flt == cb->flt;
+}
+
+bool lat_eq(Fn* fn, Lat a, Lat b) {
+ if (a.state != b.state)
+ return false;
+ if (a.state == Const)
+ return con_eq(fn, a.con, b.con);
+ return true;
+}
+
+// Check if unfoldable type.
+bool op_is_unfoldable(uint op) {
+ if (isstore(op))
+ return true; // store does not define tmp, but safe
+ if (isload(op))
+ return true;
+ if (isalloc(op))
+ return true;
+ if (ispar(op))
+ return true;
+ if (isarg(op))
+ return true;
+ if (op == Ocall)
+ return true;
+ if (op == Oblit0 || op == Oblit1)
+ return true;
+ if (op == Ovastart || op == Ovaarg)
+ return true;
+ if (op == Odbgloc)
+ return true;
+ if (op == Osel0 || op == Osel1)
+ return true;
+ if (op == Onop)
+ return true;
+ return false;
+}
+
+struct Sccp {
+ Ctx* ctx;
+ Fn* fn;
+ std::vector<Lat> lat; // ntmp
+ std::vector<char> blk_reachable; // nblk
+ // edge_exec pairs.
+ std::set<std::pair<uint, uint>> edge_exec;
+ std::vector<Blk*> cfg_wl;
+ std::vector<uint> ssa_wl; // tmp val queue
+
+ Lat arg_lat(Ref r) {
+ if (rtype(r) == RCon)
+ return {Const, r};
+ if (rtype(r) == RTmp) {
+ if (r.val >= lat.size())
+ return kBot;
+ return lat[r.val];
+ }
+ // RSlot / RType / RCall / RMem etc: do not participate in constant propagation
+ return kBot;
+ }
+
+ void set_lat(uint t, Lat nv) {
+ if (t >= lat.size() || t == 0)
+ return;
+ if (lat_eq(fn, lat[t], nv))
+ return;
+ // lattice monotonically descends: Top -> Const -> Bot
+ lat[t] = nv;
+ ssa_wl.push_back(t);
+ }
+
+ // Evaluate instruction using lattice.
+ Lat eval_ins(Ins* i) {
+ if (rtype(i->to) != RTmp)
+ return kBot;
+ if (op_is_unfoldable(i->op))
+ return kBot;
+
+ Lat a0 = arg_lat(i->arg[0]);
+ Lat a1;
+ if (req(i->arg[1], Ref::none()))
+ a1 = {Const, Ref::con_z()}; // phantom arg[1] for unary op
+ else
+ a1 = arg_lat(i->arg[1]);
+
+ // Ocopy special case: lattice equals arg[0]
+ if (i->op == Ocopy)
+ return a0;
+
+ if (a0.state == Top || a1.state == Top)
+ return kTop;
+ if (a0.state == Bot || a1.state == Bot)
+ return kBot;
+
+ // Both sides are Const: try to fold
+ Ins tmp = *i;
+ tmp.arg[0] = a0.con;
+ tmp.arg[1] = a1.con;
+ Ref r = foldref(ctx, fn, &tmp);
+ if (req(r, Ref::none()))
+ return kBot;
+ return {Const, r};
+ }
+
+ Lat eval_phi(Phi* p, Blk* in_blk) {
+ Lat acc = kTop;
+ for (uint k = 0; k < p->narg; k++) {
+ Blk* pred = p->blk[k];
+ if (!pred)
+ continue;
+ if (!edge_exec.count({pred->id, in_blk->id}))
+ continue; // Incoming edge unreachable - skip
+ Lat a = arg_lat(p->arg[k]);
+ if (a.state == Bot)
+ return kBot;
+ if (a.state == Top)
+ continue;
+ // Const
+ if (acc.state == Top) {
+ acc = a;
+ } else if (!con_eq(fn, acc.con, a.con)) {
+ return kBot;
+ }
+ }
+ return acc;
+ }
+
+ // Activate CFG edge.
+ void activate_edge(Blk* pred, Blk* succ) {
+ if (!succ)
+ return;
+ auto key = std::make_pair(pred->id, succ->id);
+ if (!edge_exec.insert(key).second)
+ return; // Already activated
+ // Enqueue succ if needed.
+ cfg_wl.push_back(succ);
+ if (!blk_reachable[succ->id])
+ blk_reachable[succ->id] = 1;
+ }
+
+ void eval_jmp(Blk* b) {
+ int t = b->jmp.type;
+ if (isret(t) || t == Jhlt)
+ return;
+ if (t == Jjmp) {
+ activate_edge(b, b->s1);
+ return;
+ }
+ if (INRANGE(t, Jjf, Jjf1)) {
+ // Conditional jumps.
+ activate_edge(b, b->s1);
+ activate_edge(b, b->s2);
+ return;
+ }
+ if (t == Jjnz) {
+ Lat c = arg_lat(b->jmp.arg);
+ if (c.state == Top)
+ return;
+ if (c.state == Bot) {
+ activate_edge(b, b->s1);
+ activate_edge(b, b->s2);
+ return;
+ }
+ // Const: check if non-zero
+ if (rtype(c.con) != RCon || c.con.val >= static_cast<uint>(fn->ncon)) {
+ activate_edge(b, b->s1);
+ activate_edge(b, b->s2);
+ return;
+ }
+ Con* cc = &fn->con[c.con.val];
+ if (cc->type != Con::CBits) {
+ // CAddr is treated as truthy (non-null address)
+ activate_edge(b, b->s1);
+ return;
+ }
+ if (cc->bits.i != 0)
+ activate_edge(b, b->s1);
+ else
+ activate_edge(b, b->s2);
+ return;
+ }
+ // Other unknown jmp types: activate both sides
+ activate_edge(b, b->s1);
+ activate_edge(b, b->s2);
+ }
+
+ void visit_block(Blk* b) {
+ // Re-eval phis.
+ for (Phi* p = b->phi; p; p = p->link) {
+ if (rtype(p->to) != RTmp)
+ continue;
+ Lat nv = eval_phi(p, b);
+ set_lat(p->to.val, nv);
+ }
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (rtype(i->to) != RTmp)
+ continue;
+ Lat nv = eval_ins(i);
+ set_lat(i->to.val, nv);
+ }
+ eval_jmp(b);
+ }
+
+ // Process tmp uses.
+ void visit_uses(uint t) {
+ if (t >= static_cast<uint>(fn->ntmp))
+ return;
+ Tmp* tt = &fn->tmp[t];
+ for (uint u = 0; u < tt->nuse; u++) {
+ Use& uu = tt->use[u];
+ if (uu.bid >= blk_reachable.size())
+ continue;
+ if (!blk_reachable[uu.bid])
+ continue; // In unreachable block, ignore for now
+ Blk* ub = fn->rpo[uu.bid];
+ if (uu.type == Use::UPhi) {
+ Phi* p = uu.u.phi;
+ if (rtype(p->to) == RTmp) {
+ Lat nv = eval_phi(p, ub);
+ set_lat(p->to.val, nv);
+ }
+ } else if (uu.type == Use::UIns) {
+ Ins* i = uu.u.ins;
+ if (rtype(i->to) == RTmp) {
+ Lat nv = eval_ins(i);
+ set_lat(i->to.val, nv);
+ }
+ } else if (uu.type == Use::UJmp) {
+ eval_jmp(ub);
+ }
+ }
+ }
+
+ void solve() {
+ // Init state.
+ lat.assign(static_cast<size_t>(fn->ntmp), kTop);
+ blk_reachable.assign(static_cast<size_t>(fn->nblk), 0);
+ edge_exec.clear();
+ cfg_wl.clear();
+ ssa_wl.clear();
+
+ blk_reachable[fn->start->id] = 1;
+ cfg_wl.push_back(fn->start);
+ // Enqueue start.
+
+ while (!cfg_wl.empty() || !ssa_wl.empty()) {
+ // Process worklists.
+ while (!cfg_wl.empty()) {
+ Blk* b = cfg_wl.back();
+ cfg_wl.pop_back();
+ visit_block(b);
+ }
+ while (!ssa_wl.empty()) {
+ uint t = ssa_wl.back();
+ ssa_wl.pop_back();
+ visit_uses(t);
+ }
+ }
+ }
+
+ // Apply: Commit lattice to IR.
+ Ref maybe_const(Ref r) {
+ if (rtype(r) != RTmp)
+ return r;
+ if (r.val >= lat.size())
+ return r;
+ Lat& l = lat[r.val];
+ if (l.state == Const)
+ return l.con;
+ return r;
+ }
+
+ int apply() {
+ int n_ins = 0, n_phi = 0, n_jmp = 0, n_arg = 0;
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (b->id >= blk_reachable.size() || !blk_reachable[b->id])
+ continue;
+
+ // Phi: Remove incoming edges from unreachable preds; replace arg operands
+ for (Phi* p = b->phi; p; p = p->link) {
+ uint w = 0;
+ for (uint k = 0; k < p->narg; k++) {
+ Blk* pred = p->blk[k];
+ if (!pred || !edge_exec.count({pred->id, b->id})) {
+ // This incoming edge was not activated during SCCP -> unreachable -> delete
+ continue;
+ }
+ Ref na = maybe_const(p->arg[k]);
+ if (!req(na, p->arg[k]))
+ n_arg++;
+ if (w != k) {
+ p->arg[w] = na;
+ p->blk[w] = pred;
+ } else {
+ p->arg[w] = na;
+ }
+ w++;
+ }
+ p->narg = w;
+
+ // Keep constant phi.to for DCE.
+ if (rtype(p->to) == RTmp && p->to.val < lat.size() && lat[p->to.val].state == Const)
+ n_phi++;
+ }
+
+ // Ins: Change Const def to Ocopy; replace arg operands
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ bool is_const_def = (rtype(i->to) == RTmp && i->to.val < lat.size() &&
+ lat[i->to.val].state == Const);
+ if (is_const_def && i->op != Ocopy && !op_is_unfoldable(i->op)) {
+ Ref cr = lat[i->to.val].con;
+ i->op = Ocopy;
+ i->arg[0] = cr;
+ i->arg[1] = Ref::none();
+ n_ins++;
+ continue;
+ }
+ // Otherwise just replace args
+ Ref na0 = maybe_const(i->arg[0]);
+ Ref na1 = maybe_const(i->arg[1]);
+ if (!req(na0, i->arg[0])) {
+ i->arg[0] = na0;
+ n_arg++;
+ }
+ if (!req(na1, i->arg[1])) {
+ i->arg[1] = na1;
+ n_arg++;
+ }
+ }
+
+ // jmp: Jjnz cond Const -> Jjmp; replace args
+ if (b->jmp.type == Jjnz) {
+ Lat c = arg_lat(b->jmp.arg);
+ if (c.state == Const && rtype(c.con) == RCon &&
+ c.con.val < static_cast<uint>(fn->ncon)) {
+ Con* cc = &fn->con[c.con.val];
+ if (cc->type == Con::CBits) {
+ // Choose succ
+ Blk* taken = (cc->bits.i != 0) ? b->s1 : b->s2;
+ b->jmp.type = Jjmp;
+ b->jmp.arg = Ref::none();
+ b->s1 = taken;
+ b->s2 = nullptr;
+ n_jmp++;
+ } else if (cc->type == Con::CAddr) {
+ // Address is always truthy
+ b->jmp.type = Jjmp;
+ b->jmp.arg = Ref::none();
+ b->s1 = b->s1;
+ b->s2 = nullptr;
+ n_jmp++;
+ }
+ } else {
+ Ref na = maybe_const(b->jmp.arg);
+ if (!req(na, b->jmp.arg)) {
+ b->jmp.arg = na;
+ n_arg++;
+ }
+ }
+ } else if (!isret(b->jmp.type) && b->jmp.type != Jhlt && b->jmp.type != Jjmp) {
+ Ref na = maybe_const(b->jmp.arg);
+ if (!req(na, b->jmp.arg)) {
+ b->jmp.arg = na;
+ n_arg++;
+ }
+ }
+ }
+
+ if (debug['S']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> SCCP %s: const_ins=%d const_phi=%d jjnz_collapse=%d arg_subst=%d\n",
+ fn->name, n_ins, n_phi, n_jmp, n_arg);
+ }
+ return n_ins + n_phi + n_jmp + n_arg;
+ }
+};
+
+} // namespace
+
+void sccp(Ctx* ctx, Fn* fn) {
+ // Prerequisites: filluse and rpo ready.
+ Sccp s;
+ s.ctx = ctx;
+ s.fn = fn;
+ s.solve();
+ s.apply();
+}
+
+} // namespace rcc
diff --git a/src/mid/ssa.cc b/src/mid/ssa.cc
new file mode 100644
index 0000000..75e2773
--- /dev/null
+++ b/src/mid/ssa.cc
@@ -0,0 +1,512 @@
+/* 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 ssa.cc
+/// @brief Use chain population / SSA construction (Cytron-style) / SSA invariant validation.
+///
+/// See docs/ssa.md for architectural notes.
+
+#include "rcc/all.h"
+
+namespace rcc {
+
+// Use chain append
+
+namespace {
+
+inline void use_append(Ctx* ctx, Tmp* tmp, decltype(Use::type) ty, Blk* b, Phi* phi, Ins* ins) {
+ if (!tmp->use)
+ return;
+ uint n = tmp->nuse++;
+ vgrow(ctx, &tmp->use, tmp->nuse);
+ Use* u = &tmp->use[n];
+ u->type = ty;
+ u->bid = b->id;
+ if (ty == Use::UPhi)
+ u->u.phi = phi;
+ else if (ty == Use::UIns)
+ u->u.ins = ins;
+ // UJmp has no extra payload
+}
+
+} // namespace
+
+void adduse_ins(Ctx* ctx, Tmp* tmp, Blk* b, Ins* ins) {
+ use_append(ctx, tmp, Use::UIns, b, nullptr, ins);
+}
+void adduse_phi(Ctx* ctx, Tmp* tmp, Blk* b, Phi* phi) {
+ use_append(ctx, tmp, Use::UPhi, b, phi, nullptr);
+}
+void adduse_jmp(Ctx* ctx, Tmp* tmp, Blk* b) {
+ use_append(ctx, tmp, Use::UJmp, b, nullptr, nullptr);
+}
+
+// filluse - Recompute def / use / cls / width / phi-equiv-class for each tmp
+
+namespace {
+
+/// Derive width from def instruction op (sub-word width info, for spill storage selection).
+int derive_width(Ins* i) {
+ int op = i->op;
+ if (isparbh(op))
+ return Tmp::Wsb + (op - Oparsb);
+ if (isload(op) && op != Oload)
+ return Tmp::Wsb + (op - Oloadsb);
+ if (isext(op))
+ return Tmp::Wsb + (op - Oextsb);
+ int kx, cx;
+ if (iscmp(op, &kx, &cx))
+ return Tmp::Wub;
+ return Tmp::WFull;
+}
+
+} // namespace
+
+void filluse(Ctx* ctx, Fn* fn) {
+ Tmp* tmp = fn->tmp;
+
+ // Reset metadata for each user tmp; first call creates empty vnew head for use[].
+ for (int t = Tmp0; t < fn->ntmp; t++) {
+ tmp[t].def = nullptr;
+ tmp[t].bid = ~0u;
+ tmp[t].ndef = 0;
+ tmp[t].nuse = 0;
+ tmp[t].cls = 0;
+ tmp[t].phi = 0;
+ tmp[t].width = Tmp::WFull;
+ if (!tmp[t].use)
+ tmp[t].use = static_cast<Use*>(vnew(ctx, 0, sizeof(Use), PFn));
+ }
+
+ // Iterate blocks, register phi def/use, ins def/use, jmp arg use.
+ for (Blk* b = fn->start; b; b = b->link) {
+ // phi
+ for (Phi* p = b->phi; p; p = p->link) {
+ assert(rtype(p->to) == RTmp);
+ int td = p->to.val;
+ tmp[td].bid = b->id;
+ tmp[td].ndef++;
+ tmp[td].cls = p->cls;
+
+ int td_cls = phicls(td, fn->tmp);
+ for (uint a = 0; a < p->narg; a++) {
+ if (rtype(p->arg[a]) != RTmp)
+ continue;
+ int ta = p->arg[a].val;
+ adduse_phi(ctx, &tmp[ta], b, p);
+ int ta_cls = phicls(ta, fn->tmp);
+ if (ta_cls != td_cls)
+ tmp[ta_cls].phi = td_cls;
+ }
+ }
+
+ // Instruction def + double arg use
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ if (!req(i->to, Ref::none())) {
+ assert(rtype(i->to) == RTmp);
+ int td = i->to.val;
+ int w = derive_width(i);
+ // Under Kw cls, Wsw/Wuw is equivalent to WFull (high bits don't exist anyway)
+ if ((w == Tmp::Wsw || w == Tmp::Wuw) && i->cls == Kw)
+ w = Tmp::WFull;
+ tmp[td].width = static_cast<decltype(tmp[td].width)>(w);
+ tmp[td].def = i;
+ tmp[td].bid = b->id;
+ tmp[td].ndef++;
+ tmp[td].cls = static_cast<short>(i->cls);
+ }
+ for (int m = 0; m < 2; m++) {
+ if (rtype(i->arg[m]) != RTmp)
+ continue;
+ int ta = i->arg[m].val;
+ adduse_ins(ctx, &tmp[ta], b, i);
+ }
+ }
+
+ // Terminating jump arg
+ if (rtype(b->jmp.arg) == RTmp)
+ adduse_jmp(ctx, &tmp[b->jmp.arg.val], b);
+ }
+}
+
+// Name Pool - per-Ctx, emalloc nodes reused across fn, drained at ~Ctx
+
+struct Name {
+ Ref r;
+ Blk* b;
+ Name* up;
+};
+
+namespace {
+
+Name* nnew(Ctx* ctx, Ref r, Blk* b, Name* up) {
+ Name* n;
+ if (ctx->ssa_namel) {
+ n = ctx->ssa_namel;
+ ctx->ssa_namel = n->up;
+ } else {
+ // Reused across fn, so not in SubPool (SubPool frees with fn, list would dangle)
+ n = static_cast<Name*>(emalloc(sizeof(Name)));
+ }
+ n->r = r;
+ n->b = b;
+ n->up = up;
+ return n;
+}
+
+void nfree(Ctx* ctx, Name* n) {
+ n->up = ctx->ssa_namel;
+ ctx->ssa_namel = n;
+}
+
+} // namespace
+
+void ssa_drain_namel(Ctx* ctx) {
+ Name* n = ctx->ssa_namel;
+ while (n) {
+ Name* nxt = n->up;
+ free(n);
+ n = nxt;
+ }
+ ctx->ssa_namel = nullptr;
+}
+
+// Rename helpers: rendef registers new def, getstk queries currently visible name, renblk DFS along
+// dom tree
+
+namespace {
+
+Ref fresh_rename(Ctx* ctx, int t, Fn* fn) {
+ return newtmp(ctx, fn->tmp[t].name, fn->tmp[t].cls, fn);
+}
+
+inline bool needs_rename(Fn* fn, int t) {
+ return fn->tmp[t].visit != 0;
+}
+
+/// Change a def to a fresh name.
+void rendef(Ctx* ctx, Ref* r, Blk* b, Name** stk, Fn* fn) {
+ if (req(*r, Ref::none()))
+ return;
+ int t = r->val;
+ if (!needs_rename(fn, t))
+ return;
+ Ref r1 = fresh_rename(ctx, t, fn);
+ fn->tmp[r1.val].visit = t; // Record "this fresh name originates from original tmp t"
+ stk[t] = nnew(ctx, r1, b, stk[t]);
+ *r = r1;
+}
+
+/// Query currently visible name of tmp t at b.
+Ref getstk(Ctx* ctx, int t, Blk* b, Name** stk) {
+ Name* n = stk[t];
+ while (n && !dom(n->b, b)) {
+ Name* dead = n;
+ n = n->up;
+ nfree(ctx, dead);
+ }
+ stk[t] = n;
+ return n ? n->r : Ref::undef();
+}
+
+void renblk(Ctx* ctx, Blk* b, Name** stk, Fn* fn) {
+ // Rename phi-def
+ for (Phi* p = b->phi; p; p = p->link)
+ rendef(ctx, &p->to, b, stk, fn);
+
+ // Instructions: rename args first (read old -> write new), then rendef def
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ for (int m = 0; m < 2; m++) {
+ if (rtype(i->arg[m]) != RTmp)
+ continue;
+ int t = i->arg[m].val;
+ if (needs_rename(fn, t))
+ i->arg[m] = getstk(ctx, t, b, stk);
+ }
+ rendef(ctx, &i->to, b, stk, fn);
+ }
+
+ // jmp arg
+ if (rtype(b->jmp.arg) == RTmp) {
+ int t = b->jmp.arg.val;
+ if (needs_rename(fn, t))
+ b->jmp.arg = getstk(ctx, t, b, stk);
+ }
+
+ // Append arg from this block to phi of each successor s
+ Blk* succs[2] = {b->s1, (b->s2 == b->s1) ? nullptr : b->s2};
+ for (Blk* s : succs) {
+ if (!s)
+ continue;
+ for (Phi* p = s->phi; p; p = p->link) {
+ int orig_t = p->to.val;
+ int src_t = fn->tmp[orig_t].visit; // phi.to was inserted by phiins, visit=source tmp
+ if (!src_t)
+ continue;
+ uint k = p->narg++;
+ vgrow(ctx, &p->arg, p->narg);
+ vgrow(ctx, &p->blk, p->narg);
+ p->arg[k] = getstk(ctx, src_t, b, stk);
+ p->blk[k] = b;
+ }
+ }
+
+ // Recurse dom-tree children
+ for (Blk* s = b->dom; s; s = s->dlink)
+ renblk(ctx, s, stk, fn);
+}
+
+// phiins - Cytron-style phi insertion
+
+/// Scan all blocks, handle intra-block multi-defs of tmp t.
+short collect_real_defs(Ctx* ctx, Fn* fn, int t, BSet* on_worklist, Blk** be, Blk*** bp_io) {
+ short k = Kx;
+ Ref t_ref = Ref::tmp(static_cast<uint>(t));
+
+ for (Blk* b = fn->start; b; b = b->link) {
+ b->visit = 0;
+ Ref running = Ref::none();
+
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ // Rewrite uses appearing after running to running name
+ if (!req(running, Ref::none())) {
+ if (req(i->arg[0], t_ref))
+ i->arg[0] = running;
+ if (req(i->arg[1], t_ref))
+ i->arg[1] = running;
+ }
+ if (!req(i->to, t_ref))
+ continue;
+
+ if (!bshas(b->out, static_cast<uint>(t))) {
+ // Def that doesn't escape block at the end: fresh naming in-place
+ running = fresh_rename(ctx, t, fn);
+ i->to = running;
+ } else {
+ // Truly escaping def: push b to worklist
+ if (!bshas(on_worklist, b->id)) {
+ bsset(on_worklist, b->id);
+ *--(*bp_io) = b;
+ }
+ if (clsmerge(&k, static_cast<short>(i->cls)))
+ die("ssa: incompatible classes for %%%s", fn->tmp[t].name);
+ }
+ }
+ (void)be;
+
+ // Block tail jmp arg also follows running
+ if (!req(running, Ref::none()) && req(b->jmp.arg, t_ref))
+ b->jmp.arg = running;
+ }
+ return k;
+}
+
+void phiins(Ctx* ctx, Fn* fn) {
+ BSet on_worklist[1], original_defs[1];
+ bsinit(ctx, on_worklist, fn->nblk);
+ bsinit(ctx, original_defs, fn->nblk);
+
+ // worklist is a stack, growing backwards from blist end. bp == be means empty.
+ Blk** blist = static_cast<Blk**>(emalloc(fn->nblk * sizeof(Blk*)));
+ Blk** be = blist + fn->nblk;
+
+ for (int t = Tmp0; t < fn->ntmp; t++) {
+ fn->tmp[t].visit = 0;
+ if (fn->tmp[t].phi != 0)
+ continue; // Not head of phi-equiv-class, skip (meaning tmp is already handled by its
+ // representative)
+
+ // Fast path: single def and (def at entry, or all uses in same block as def) -> no phi and
+ // rename needed
+ if (fn->tmp[t].ndef == 1) {
+ bool same = true;
+ uint defb = fn->tmp[t].bid;
+ Use* uses = fn->tmp[t].use;
+ for (uint n = 0; n < fn->tmp[t].nuse; n++)
+ if (uses[n].bid != defb) {
+ same = false;
+ break;
+ }
+ if (same || defb == fn->start->id)
+ continue;
+ }
+
+ bszero(on_worklist);
+ Blk** bp = be;
+ short k = collect_real_defs(ctx, fn, t, on_worklist, be, &bp);
+
+ // original_defs is used later to check "did phi block originally have def of t"
+ bscopy(original_defs, on_worklist);
+
+ // Phase 2: IDF main loop
+ while (bp != be) {
+ fn->tmp[t].visit = t; // Mark: t actually entered renaming
+ Blk* b = *bp++;
+ bsclr(on_worklist, b->id);
+
+ for (uint n = 0; n < b->nfron; n++) {
+ Blk* a = b->fron[n];
+ if (a->visit++ != 0)
+ continue; // Already inserted phi for a in this round
+ if (!bshas(a->in, static_cast<uint>(t)))
+ continue; // t not live at a entry, no phi needed
+
+ Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ memset(p, 0, sizeof(Phi));
+ p->cls = k;
+ p->to = Ref::tmp(static_cast<uint>(t));
+ p->link = a->phi;
+ p->arg = static_cast<Ref*>(vnew(ctx, 0, sizeof(Ref), PFn));
+ p->blk = static_cast<Blk**>(vnew(ctx, 0, sizeof(Blk*), PFn));
+ a->phi = p;
+
+ // a is now also a def block of t (phi form), push a to worklist to continue IDF
+ if (bshas(original_defs, a->id))
+ continue;
+ if (bshas(on_worklist, a->id))
+ continue;
+ bsset(on_worklist, a->id);
+ *--bp = a;
+ }
+ }
+ }
+
+ free(blist);
+}
+
+} // namespace
+
+// ssa() - Main entry: filldom -> fillfron -> filllive -> phiins -> renblk
+
+void ssa(Ctx* ctx, Fn* fn) {
+ // stk[] is allocated by "ntmp before rename".
+ int ntmp0 = fn->ntmp;
+ Name** stk = static_cast<Name**>(emalloc(static_cast<size_t>(ntmp0) * sizeof(Name*)));
+
+ // Internal filllive call in ssa() is only for phiins, avoiding leak to -d L dump
+ int saved_L = debug['L'];
+ debug['L'] = 0;
+
+ filldom(fn);
+
+ if (debug['N']) {
+ FILE* f = ctx->dbg_buf;
+ fprintf(f, "\n> Dominators:\n");
+ for (Blk* bb = fn->start; bb; bb = bb->link) {
+ if (!bb->dom)
+ continue;
+ fprintf(f, "%10s:", bb->name);
+ for (Blk* c = bb->dom; c; c = c->dlink)
+ fprintf(f, " %s", c->name);
+ fputc('\n', f);
+ }
+ }
+
+ fillfron(ctx, fn);
+ filllive(ctx, fn);
+ phiins(ctx, fn);
+ renblk(ctx, fn->start, stk, fn);
+
+ // Reclaim all leftover Names on stk[] to free list (only walking ntmp0 range)
+ for (int t = ntmp0; --t >= 0;) {
+ Name* n = stk[t];
+ while (n) {
+ Name* nx = n->up;
+ nfree(ctx, n);
+ n = nx;
+ }
+ }
+ free(stk);
+
+ debug['L'] = static_cast<char>(saved_L);
+
+ if (debug['N']) {
+ fprintf(ctx->dbg_buf, "\n> After SSA construction:\n");
+ printfn(fn, ctx->dbg_buf);
+ }
+}
+
+// ssacheck - SSA invariant validation
+
+namespace {
+
+bool phi_arg_dominated_by(Phi* p, Blk* def_blk, Ref defref) {
+ for (uint n = 0; n < p->narg; n++) {
+ if (!req(p->arg[n], defref))
+ continue;
+ if (!dom(def_blk, p->blk[n]))
+ return false;
+ }
+ return true;
+}
+
+[[noreturn]] void ssa_err(Tmp* t, Blk* ub) {
+ if (t->visit)
+ die("ssa: %%%s violates SSA invariant", t->name);
+ die("ssa: %%%s used undefined in @%s", t->name, ub->name);
+}
+
+/// Verify all uses of t are dominated by (def_blk, def_ins). def_ins = nullptr means phi-def.
+void verify_uses(Fn* fn, Blk* def_blk, Ins* def_ins, Tmp* t, Ref defref) {
+ for (Use* u = t->use; u < &t->use[t->nuse]; u++) {
+ Blk* ub = fn->rpo[u->bid];
+
+ if (u->type == Use::UPhi) {
+ if (!phi_arg_dominated_by(u->u.phi, def_blk, defref))
+ ssa_err(t, ub);
+ continue;
+ }
+
+ if (ub == def_blk) {
+ // Intra-block: ins-use must be after ins-def (pointer order; valid comparison in same
+ // b->ins array)
+ if (def_ins && u->type == Use::UIns && u->u.ins <= def_ins)
+ ssa_err(t, ub);
+ // UJmp is at block end, definitely after def; no extra check needed
+ } else {
+ if (!sdom(def_blk, ub))
+ ssa_err(t, ub);
+ }
+ }
+}
+
+} // namespace
+
+void ssacheck(Fn* fn) {
+ // Global check: each tmp <=1 def; use must have def
+ for (Tmp* t = &fn->tmp[Tmp0]; t - fn->tmp < fn->ntmp; t++) {
+ if (t->ndef > 1)
+ die("ssa: temporary %%%s defined more than once", t->name);
+ if (t->nuse > 0 && t->ndef == 0)
+ ssa_err(t, fn->rpo[t->use[0].bid]);
+ }
+
+ // Each def's use must be dominated by it
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Phi* p = b->phi; p; p = p->link)
+ verify_uses(fn, b, nullptr, &fn->tmp[p->to.val], p->to);
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) {
+ if (rtype(i->to) != RTmp)
+ continue;
+ verify_uses(fn, b, i, &fn->tmp[i->to.val], i->to);
+ }
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/storemrg.cc b/src/mid/storemrg.cc
new file mode 100644
index 0000000..65b4bd4
--- /dev/null
+++ b/src/mid/storemrg.cc
@@ -0,0 +1,231 @@
+/* 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 storemrg.cc
+/// @brief Fuses adjacent 4B stores into 8B storel.
+///
+/// See docs/storemrg.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+// Read 32-bit integer constant value from Ref r (put into *val_out).
+bool get_int_con32(Fn* fn, Ref r, uint32_t* val_out) {
+ if (rtype(r) != RCon)
+ return false;
+ Con* c = &fn->con[r.val];
+ if (c->type != Con::CBits || c->flt)
+ return false;
+ *val_out = static_cast<uint32_t>(c->bits.i);
+ return true;
+}
+
+bool ref_eq(Ref a, Ref b) {
+ return a.type == b.type && a.val == b.val;
+}
+
+// Decompose addr into (base, offset).
+void decompose_addr(Fn* fn, Ref addr, Ref* base_out, int64_t* off_out) {
+ *base_out = addr;
+ *off_out = 0;
+ if (rtype(addr) != RTmp)
+ return;
+ Tmp* t = &fn->tmp[addr.val];
+ if (!t->def)
+ return;
+ Ins* def = t->def;
+ if (def->op != Oadd)
+ return;
+ Ref ra = def->arg[0], rb = def->arg[1];
+ auto try_split = [&](Ref tmp_r, Ref con_r) -> bool {
+ if (rtype(tmp_r) != RTmp || rtype(con_r) != RCon)
+ return false;
+ Con* c = &fn->con[con_r.val];
+ if (c->type != Con::CBits || c->flt)
+ return false;
+ *base_out = tmp_r;
+ *off_out = c->bits.i;
+ return true;
+ };
+ if (try_split(ra, rb))
+ return;
+ try_split(rb, ra);
+}
+
+bool con_is_aligned8(Fn* fn, Ref r) {
+ if (rtype(r) != RCon)
+ return false;
+ Con* c = &fn->con[r.val];
+ return c->type == Con::CBits && !c->flt && (c->bits.i & 7) == 0;
+}
+
+bool tmp_in_align8(const std::vector<bool>& a8, Ref r) {
+ if (rtype(r) != RTmp)
+ return false;
+ if (r.val >= a8.size())
+ return false;
+ return a8[r.val];
+}
+
+// Optimistic fixpoint: compute aligned8 set.
+std::vector<bool> compute_aligned8(Fn* fn) {
+ std::vector<bool> a8(static_cast<size_t>(fn->ntmp), true);
+ bool changed = true;
+ while (changed) {
+ changed = false;
+ for (Blk* b = fn->start; b; b = b->link) {
+ for (Phi* p = b->phi; p; p = p->link) {
+ if (rtype(p->to) != RTmp)
+ continue;
+ uint tid = p->to.val;
+ if (tid >= a8.size() || !a8[tid])
+ continue;
+ bool ok = p->narg > 0;
+ for (uint k = 0; k < p->narg; k++) {
+ Ref r = p->arg[k];
+ if (tmp_in_align8(a8, r))
+ continue;
+ if (con_is_aligned8(fn, r))
+ continue;
+ ok = false;
+ break;
+ }
+ if (!ok) {
+ a8[tid] = false;
+ changed = true;
+ }
+ }
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ if (rtype(i->to) != RTmp)
+ continue;
+ uint tid = i->to.val;
+ if (tid >= a8.size() || !a8[tid])
+ continue;
+ bool ok = false;
+ int op = i->op;
+ if (op == Oalloc8 || op == Oalloc16) {
+ ok = true;
+ } else if (op == Ocopy) {
+ if (tmp_in_align8(a8, i->arg[0]))
+ ok = true;
+ } else if (op == Oadd) {
+ if ((tmp_in_align8(a8, i->arg[0]) && con_is_aligned8(fn, i->arg[1])) ||
+ (tmp_in_align8(a8, i->arg[1]) && con_is_aligned8(fn, i->arg[0])))
+ ok = true;
+ }
+ // All other ops (Omul / Oload / Opar / Ocall / ...) are unconditionally demoted.
+ if (!ok) {
+ a8[tid] = false;
+ changed = true;
+ }
+ }
+ }
+ }
+ return a8;
+}
+
+} // namespace
+
+void storemrg(Ctx* ctx, Fn* fn) {
+ std::vector<bool> a8 = compute_aligned8(fn);
+
+ int merged = 0;
+ for (Blk* b = fn->start; b; b = b->link) {
+ struct Pending {
+ uint idx;
+ Ref base;
+ int64_t off;
+ uint32_t val; // 32-bit store value
+ };
+ std::vector<Pending> pending;
+ auto flush = [&]() { pending.clear(); };
+
+ for (uint k = 0; k < b->nins; k++) {
+ Ins* i = &b->ins[k];
+ int op = i->op;
+
+ // Memory barrier: any load / call / other store flushes pending.
+ if (op == Ocall || isload(op)) {
+ flush();
+ continue;
+ }
+
+ uint32_t val32 = 0;
+ if (op == Ostorew && get_int_con32(fn, i->arg[0], &val32)) {
+ Ref base;
+ int64_t off;
+ decompose_addr(fn, i->arg[1], &base, &off);
+ if (tmp_in_align8(a8, base)) {
+ bool matched = false;
+ for (auto it = pending.begin(); it != pending.end(); ++it) {
+ bool prev_is_lower = (it->off + 4 == off) && (it->off & 7) == 0;
+ bool curr_is_lower = (off + 4 == it->off) && (off & 7) == 0;
+ // Must be same base + same 32-bit value + lower offset 8B-aligned pair.
+ if (ref_eq(it->base, base) && it->val == val32 &&
+ (prev_is_lower || curr_is_lower)) {
+ uint lo_idx = prev_is_lower ? it->idx : k;
+ uint hi_idx = prev_is_lower ? k : it->idx;
+ b->ins[lo_idx].op = Ostorel;
+ if (val32 != 0) {
+ // Assemble 64-bit duplicated constant.
+ uint64_t combined = static_cast<uint64_t>(val32) |
+ (static_cast<uint64_t>(val32) << 32);
+ b->ins[lo_idx].arg[0] =
+ getcon(ctx, static_cast<int64_t>(combined), fn);
+ }
+ // When val32==0, RCon{0} has identical bit pattern for Kw/Kl.
+ b->ins[hi_idx] = Ins{};
+ b->ins[hi_idx].op = Onop;
+ pending.erase(it);
+ merged++;
+ matched = true;
+ break;
+ }
+ }
+ if (!matched)
+ pending.push_back({k, base, off, val32});
+ continue;
+ }
+ // base not in align8 set -> cannot prove no-alias with pending, flush.
+ flush();
+ continue;
+ }
+
+ if (isstore(op)) {
+ flush();
+ continue;
+ }
+
+ // Pure instructions have no memory side-effects, pending untouched.
+ }
+ }
+
+ if (debug['M']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "\n> storemrg %s: merged %d storew pairs → storel\n", fn->name, merged);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/tre.cc b/src/mid/tre.cc
new file mode 100644
index 0000000..6345647
--- /dev/null
+++ b/src/mid/tre.cc
@@ -0,0 +1,214 @@
+/* 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 tre.cc
+/// @brief Tail Recursion Elimination (TRE)
+
+#include "rcc/all.h"
+
+#include <cstring>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+struct Site {
+ Blk* b;
+ Ins* call; // Points to Ocall in b->ins
+ Ins* arg_first; // [arg_first..call) is Oarg sequence
+ std::vector<Ref> args;
+};
+
+bool is_self_call(Fn* fn, Ins* call, uint32_t self_id) {
+ if (call->op != Ocall)
+ return false;
+ Ref tgt = call->arg[0];
+ if (rtype(tgt) != RCon)
+ return false;
+ Con* c = &fn->con[tgt.val];
+ if (c->type != Con::CAddr)
+ return false;
+ if (c->sym.type != Sym::SGlo)
+ return false;
+ return c->sym.id == self_id;
+}
+
+// Locate Ocall and arg-group start in b.
+bool collect_site(Fn* fn, Blk* b, int npar, uint32_t self_id, Site& out) {
+ if (b->nins == 0)
+ return false;
+ Ins* last = &b->ins[b->nins - 1];
+ if (!is_self_call(fn, last, self_id))
+ return false;
+
+ // ret form matches call.to
+ if (b->jmp.type == Jret0) {
+ if (!req(last->to, Ref::none()))
+ return false;
+ } else if (b->jmp.type == Jretw || b->jmp.type == Jretl || b->jmp.type == Jrets ||
+ b->jmp.type == Jretd) {
+ if (!req(b->jmp.arg, last->to))
+ return false;
+ } else {
+ return false;
+ }
+
+ // Verify all instructions before call are Oarg and count matches.
+ Ins *i0 = nullptr, *i1 = nullptr;
+ igroup(b, last, &i0, &i1);
+ if (i1 != last + 1)
+ return false;
+ int argc = static_cast<int>(last - i0);
+ if (argc != npar)
+ return false;
+ out.b = b;
+ out.call = last;
+ out.arg_first = i0;
+ out.args.clear();
+ out.args.reserve(static_cast<size_t>(npar));
+ for (Ins* j = i0; j < last; j++) {
+ if (!isarg(j->op))
+ return false;
+ out.args.push_back(j->arg[0]);
+ }
+ return true;
+}
+
+} // namespace
+
+void tre(Ctx* ctx, Fn* fn) {
+ if (fn->vararg)
+ return;
+ if (fn->name && std::strcmp(fn->name, "main") == 0)
+ return;
+
+ // exit if any block contains isalloc
+ for (Blk* b = fn->start; b; b = b->link)
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (isalloc(i->op))
+ return;
+
+ // fn->start cannot already have phi
+ if (fn->start->phi)
+ return;
+
+ // Collect Opar from start block
+ std::vector<Ins*> par_inss;
+ Ins* ie0 = fn->start->ins + fn->start->nins;
+ for (Ins* i = fn->start->ins; i < ie0 && i->op == Opar; i++)
+ par_inss.push_back(i);
+ int npar = static_cast<int>(par_inss.size());
+
+ // Reject theoretically allowed Opar variants (struct by-value) not used by SysY
+ for (Ins* i = fn->start->ins; i < ie0; i++) {
+ if (i->op == Oparc || i->op == Opare)
+ return;
+ }
+
+ uint32_t self_id = intern(fn->name);
+
+ // Scan qualified sites
+ std::vector<Site> sites;
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (!isret(b->jmp.type))
+ continue;
+ Site s;
+ if (collect_site(fn, b, npar, self_id, s))
+ sites.push_back(std::move(s));
+ }
+ if (sites.empty())
+ return;
+
+ // Transformation
+ Blk* loop_header = fn->start;
+
+ // Prepare %p_init_i for each Opar, record original %p_i and cls
+ auto upar = static_cast<size_t>(npar);
+ std::vector<Ref> par_init_refs(upar);
+ std::vector<Ref> par_orig_refs(upar);
+ std::vector<int> par_clses(upar);
+ for (int i = 0; i < npar; i++) {
+ size_t ui = static_cast<size_t>(i);
+ par_orig_refs[ui] = par_inss[ui]->to;
+ par_clses[ui] = static_cast<int>(par_inss[ui]->cls);
+ par_init_refs[ui] = newtmp(ctx, "p_init", par_clses[ui], fn);
+ }
+
+ // Create @tre_entry
+ Blk* tre_entry = newblk(ctx);
+ tre_entry->name = strf(ctx, PFn, "tre_entry");
+ for (int i = 0; i < npar; i++) {
+ size_t ui = static_cast<size_t>(i);
+ Ins ni = *par_inss[ui];
+ ni.to = par_init_refs[ui];
+ addins(ctx, &tre_entry->ins, &tre_entry->nins, &ni);
+ }
+ tre_entry->jmp.type = Jjmp;
+ tre_entry->jmp.arg = Ref::none();
+ tre_entry->s1 = loop_header;
+ tre_entry->s2 = nullptr;
+ tre_entry->link = loop_header;
+ fn->start = tre_entry;
+
+ // Remove head npar Opars from loop_header
+ int remaining = static_cast<int>(loop_header->nins) - npar;
+ if (remaining > 0)
+ std::memmove(loop_header->ins, loop_header->ins + npar,
+ static_cast<size_t>(remaining) * sizeof(Ins));
+ loop_header->nins = static_cast<uint>(remaining);
+
+ // Insert npar phis at loop_header head
+ int nphi_args = 1 + static_cast<int>(sites.size());
+ for (int i = npar - 1; i >= 0; i--) {
+ size_t ui = static_cast<size_t>(i);
+ auto* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ std::memset(p, 0, sizeof(Phi));
+ p->to = par_orig_refs[ui];
+ p->cls = static_cast<short>(par_clses[ui]);
+ p->narg = static_cast<uint>(nphi_args);
+ p->arg = static_cast<Ref*>(vnew(ctx, static_cast<ulong>(nphi_args), sizeof(Ref), PFn));
+ p->blk = static_cast<Blk**>(vnew(ctx, static_cast<ulong>(nphi_args), sizeof(Blk*), PFn));
+ p->arg[0] = par_init_refs[ui];
+ p->blk[0] = tre_entry;
+ for (size_t k = 0; k < sites.size(); k++) {
+ p->arg[1 + k] = sites[k].args[ui];
+ p->blk[1 + k] = sites[k].b;
+ }
+ p->link = loop_header->phi;
+ loop_header->phi = p;
+ }
+
+ // Truncate block at each site and redirect jmp to loop_header
+ for (auto& s : sites) {
+ int new_nins = static_cast<int>(s.arg_first - s.b->ins);
+ s.b->nins = static_cast<uint>(new_nins);
+ s.b->jmp.type = Jjmp;
+ s.b->jmp.arg = Ref::none();
+ s.b->s1 = loop_header;
+ s.b->s2 = nullptr;
+ }
+
+ if (debug['T']) {
+ fprintf(stderr, "\n>>> after tre (%s) — %zu site(s):\n", fn->name, sites.size());
+ printfn(fn, stderr);
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/unroll.cc b/src/mid/unroll.cc
new file mode 100644
index 0000000..4e5462c
--- /dev/null
+++ b/src/mid/unroll.cc
@@ -0,0 +1,847 @@
+/* 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 unroll.cc
+/// @brief Loop Unrolling.
+///
+/// See docs/unroll.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cassert>
+#include <cstdio>
+#include <cstring>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+constexpr int kFullUnrollTripMax = 16;
+constexpr int kFullUnrollBudget = 256;
+constexpr int kUnrollFixpointIters = 4;
+constexpr int kPartialFactor = 4;
+constexpr int kPartialMaxBodyIns = 64; // F * body_ins <= 256 total instruction budget
+
+// Loop Identification
+
+struct LoopShape {
+ Blk* header;
+ Blk* preheader;
+ Blk* latch;
+ Blk* exit_bb;
+ uint preheader_idx; // index in header->pred where preheader sits
+ uint latch_idx; // index in header->pred where latch sits
+ std::vector<Blk*> body; // includes header, latch
+};
+
+// Helper: find_simple_loops (see docs/unroll.md)
+std::vector<LoopShape> find_simple_loops(Fn* fn) {
+ std::vector<LoopShape> out;
+
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = ~0u;
+
+ for (uint i = 0; i < fn->nblk; i++) {
+ Blk* hd = fn->rpo[i];
+
+ // single back-edge
+ int back_cnt = 0;
+ uint latch_pred_idx = 0;
+ uint pre_pred_idx = 0;
+ int pre_cnt = 0;
+ for (uint k = 0; k < hd->npred; k++) {
+ if (hd->pred[k]->id >= i && hd->pred[k]->id != ~0u) {
+ back_cnt++;
+ latch_pred_idx = k;
+ } else {
+ pre_cnt++;
+ pre_pred_idx = k;
+ }
+ }
+ if (back_cnt != 1 || pre_cnt != 1)
+ continue; // Require single latch + single preheader
+
+ Blk* latch = hd->pred[latch_pred_idx];
+ Blk* preheader = hd->pred[pre_pred_idx];
+
+ // Reverse DFS to collect body
+ std::vector<Blk*> body = {hd};
+ std::vector<Blk*> stack = {latch};
+ hd->visit = hd->id;
+ while (!stack.empty()) {
+ Blk* n = stack.back();
+ stack.pop_back();
+ if (n->visit == hd->id)
+ continue;
+ n->visit = hd->id;
+ body.push_back(n);
+ for (uint k = 0; k < n->npred; k++)
+ stack.push_back(n->pred[k]);
+ }
+
+ // single exit (see docs/unroll.md)
+ if (hd->jmp.type != Jjnz)
+ continue;
+ Blk* exit_bb = hd->s2;
+ if (!exit_bb)
+ continue;
+ auto in_body = [&](Blk* x) -> bool {
+ for (Blk* bb : body)
+ if (bb == x)
+ return true;
+ return false;
+ };
+ if (in_body(exit_bb))
+ continue;
+ if (!in_body(hd->s1))
+ continue;
+ bool ok = true;
+ for (Blk* bb : body) {
+ if (bb == hd)
+ continue; // header already checked
+ auto check_succ = [&](Blk* s) -> bool {
+ if (!s)
+ return true;
+ return in_body(s);
+ };
+ if (!check_succ(bb->s1) || !check_succ(bb->s2)) {
+ ok = false;
+ break;
+ }
+ // Forbid Ocall / nested ret jmp in body
+ if (isret(bb->jmp.type)) {
+ ok = false;
+ break;
+ }
+ for (Ins* ii = bb->ins; ii < &bb->ins[bb->nins]; ii++) {
+ if (ii->op == Ocall) {
+ ok = false;
+ break;
+ }
+ }
+ if (!ok)
+ break;
+ }
+ if (!ok)
+ continue;
+ // header itself cannot have Ocall either (although Jjnz is not ret)
+ for (Ins* ii = hd->ins; ii < &hd->ins[hd->nins]; ii++) {
+ if (ii->op == Ocall) {
+ ok = false;
+ break;
+ }
+ }
+ if (!ok)
+ continue;
+
+ LoopShape L;
+ L.header = hd;
+ L.preheader = preheader;
+ L.latch = latch;
+ L.exit_bb = exit_bb;
+ L.preheader_idx = pre_pred_idx;
+ L.latch_idx = latch_pred_idx;
+ L.body = std::move(body);
+ out.push_back(std::move(L));
+ }
+
+ return out;
+}
+
+// Induction Variable Detection + Trip Count
+
+struct Induction {
+ int64_t init;
+ int64_t step; // > 0
+ int64_t bound; // Valid only when bound_const is true
+ Ref bound_ref; // cmp's arg[1]: RCon or loop-invariant RTmp
+ bool bound_const; // bound_ref is compile-time constant
+ int cmp_op; // Ocsltw/Ocsltl/Ocslew/Ocslel
+ int cmp_cls; // cls of cmp ins (for copying guard in partial unroll)
+ Ref iv_ref; // induction variable header phi.to (used to find corresponding new phi in
+ // main.head)
+ int64_t trip; // Calculated when bound_const is true; -1 for runtime bounds
+};
+
+bool ref_is_const(Fn* fn, Ref r, int64_t& out_val) {
+ if (rtype(r) != RCon)
+ return false;
+ if (r.val >= fn->ncon)
+ return false;
+ Con* c = &fn->con[r.val];
+ if (c->type != Con::CBits)
+ return false;
+ if (c->flt)
+ return false;
+ out_val = c->bits.i;
+ return true;
+}
+
+bool is_one_of(int op, std::initializer_list<int> opts) {
+ for (int o : opts)
+ if (op == o)
+ return true;
+ return false;
+}
+
+// Find def of cmp. Returns Ins* or nullptr.
+Ins* find_def_ins(Fn* fn, Ref r) {
+ if (rtype(r) != RTmp)
+ return nullptr;
+ if (r.val >= fn->ntmp)
+ return nullptr;
+ return fn->tmp[r.val].def;
+}
+
+// Check if ref is defined outside the loop (including constants). Uses Tmp.bid (set by filluse).
+bool def_outside_body(Fn* fn, const LoopShape& L, Ref r) {
+ if (rtype(r) != RTmp)
+ return true; // Constants / non-tmps are loop-invariant
+ uint bid = fn->tmp[r.val].bid;
+ for (Blk* bb : L.body)
+ if (bb->id == bid)
+ return false;
+ return true;
+}
+
+// Resolve self-loop phis to their loop-invariant preheader value.
+Ref resolve_loop_invariant(Fn* fn, const LoopShape& L, Ref r) {
+ for (int guard = 0; guard < 8; guard++) {
+ if (def_outside_body(fn, L, r))
+ return r;
+ // Defined inside loop: can only resolve further if it's a self-loop phi in header
+ if (rtype(r) != RTmp || fn->tmp[r.val].bid != L.header->id)
+ return Ref::none();
+ Phi* p = nullptr;
+ for (Phi* q = L.header->phi; q; q = q->link)
+ if (req(q->to, r)) {
+ p = q;
+ break;
+ }
+ if (!p)
+ return Ref::none(); // Inside header but not a phi (it's a regular ins) -> true
+ // variable
+ Ref pre_arg = Ref::none();
+ bool selfish = true;
+ for (uint k = 0; k < p->narg; k++) {
+ if (p->blk[k] == L.preheader)
+ pre_arg = p->arg[k];
+ else if (!req(p->arg[k], p->to))
+ selfish = false; // Non-self-loop incoming edge -> actually varying
+ }
+ if (!selfish || req(pre_arg, Ref::none()))
+ return Ref::none();
+ r = pre_arg; // Resolved one level, continue (preheader value might also be a self-loop
+ // phi)
+ }
+ return Ref::none();
+}
+
+bool detect_induction(Fn* fn, const LoopShape& L, Induction& out) {
+ // Header must end with Jjnz, arg = cmp tmp
+ if (rtype(L.header->jmp.arg) != RTmp)
+ return false;
+ Ins* cmp = find_def_ins(fn, L.header->jmp.arg);
+ if (!cmp)
+ return false;
+ if (!is_one_of(cmp->op, {Ocsltw, Ocsltl, Ocslew, Ocslel}))
+ return false;
+
+ // Parse bound (see docs/unroll.md)
+ if (rtype(cmp->arg[0]) != RTmp)
+ return false;
+ Ref bound_ref = resolve_loop_invariant(fn, L, cmp->arg[1]);
+ if (req(bound_ref, Ref::none()))
+ return false;
+ int64_t bound_v = 0;
+ bool bound_const = ref_is_const(fn, bound_ref, bound_v);
+
+ // Find matching header phi
+ Phi* ind_phi = nullptr;
+ for (Phi* p = L.header->phi; p; p = p->link) {
+ if (req(p->to, cmp->arg[0])) {
+ ind_phi = p;
+ break;
+ }
+ }
+ if (!ind_phi)
+ return false;
+
+ // Get init (preheader side) and i_next (latch side)
+ if (ind_phi->narg != L.header->npred)
+ return false; // Defensive check
+ Ref init_ref = Ref::none(), i_next_ref = Ref::none();
+ for (uint k = 0; k < ind_phi->narg; k++) {
+ if (ind_phi->blk[k] == L.preheader)
+ init_ref = ind_phi->arg[k];
+ else if (ind_phi->blk[k] == L.latch)
+ i_next_ref = ind_phi->arg[k];
+ }
+ int64_t init_v;
+ if (!ref_is_const(fn, init_ref, init_v))
+ return false;
+ if (rtype(i_next_ref) != RTmp)
+ return false;
+
+ // i_next def must be Oadd(i, step_const) or Oadd(step_const, i)
+ Ins* next_def = find_def_ins(fn, i_next_ref);
+ if (!next_def)
+ return false;
+ if (next_def->op != Oadd)
+ return false;
+ int64_t step_v = 0;
+ if (req(next_def->arg[0], ind_phi->to) && ref_is_const(fn, next_def->arg[1], step_v)) {
+ // i_next = i + step
+ } else if (req(next_def->arg[1], ind_phi->to) && ref_is_const(fn, next_def->arg[0], step_v)) {
+ // i_next = step + i (commutative)
+ } else {
+ return false;
+ }
+ if (step_v <= 0)
+ return false; // Only positive step supported for now
+
+ out.init = init_v;
+ out.step = step_v;
+ out.bound = bound_v;
+ out.bound_ref = bound_ref;
+ out.bound_const = bound_const;
+ out.cmp_op = cmp->op;
+ out.cmp_cls = cmp->cls;
+ out.iv_ref = ind_phi->to;
+
+ if (!bound_const) {
+ out.trip = -1; // Runtime bound: only usable for partial unroll
+ return true;
+ }
+
+ // trip count depends on cmp_op
+ int64_t span = bound_v - init_v;
+ if (cmp->op == Ocslew || cmp->op == Ocslel)
+ span += 1;
+ if (span <= 0) {
+ out.trip = 0;
+ return true;
+ }
+ out.trip = (span + step_v - 1) / step_v;
+ return true;
+}
+
+int count_body_ins(const LoopShape& L) {
+ int n = 0;
+ for (Blk* b : L.body) {
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (i->op != Onop)
+ n++;
+ for (Phi* p = b->phi; p; p = p->link)
+ (void)p, n++;
+ }
+ return n;
+}
+
+// Clone / Unroll
+
+struct CopyState {
+ std::unordered_map<uint, Blk*> blk_map; // body blk.id -> clone
+ std::unordered_map<uint, Ref> tmp_map; // body-defined tmp.val -> clone Ref
+ Blk* first_blk; // = blk_map[L.header.id]
+ Blk* last_blk; // = blk_map[L.latch.id]
+};
+
+Ref remap_tmp(Ref r, const std::unordered_map<uint, Ref>& m) {
+ if (rtype(r) != RTmp)
+ return r;
+ auto it = m.find(r.val);
+ if (it == m.end())
+ return r;
+ return it->second;
+}
+
+// Deep copy a body block into dst. Discard header phis, map header Jjnz to Jjmp.
+void clone_body_blk(Ctx* ctx, const CopyState& st, Blk* src, Blk* dst, bool is_header,
+ bool is_latch) {
+ // ── phi ──
+ dst->phi = nullptr;
+ if (!is_header) {
+ Phi** tail = &dst->phi;
+ for (Phi* sp = src->phi; sp; sp = sp->link) {
+ Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ np->to = remap_tmp(sp->to, st.tmp_map);
+ np->cls = sp->cls;
+ np->visit = 0;
+ np->narg = sp->narg;
+ np->arg = static_cast<Ref*>(vnew(ctx, sp->narg, sizeof(Ref), PFn));
+ np->blk = static_cast<Blk**>(vnew(ctx, sp->narg, sizeof(Blk*), PFn));
+ for (uint k = 0; k < sp->narg; k++) {
+ np->arg[k] = remap_tmp(sp->arg[k], st.tmp_map);
+ auto it = st.blk_map.find(sp->blk[k]->id);
+ np->blk[k] = (it != st.blk_map.end()) ? it->second : sp->blk[k];
+ }
+ np->link = nullptr;
+ *tail = np;
+ tail = &np->link;
+ }
+ }
+
+ // ── ins ──
+ dst->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn));
+ dst->nins = 0;
+ for (Ins* si = src->ins; si < &src->ins[src->nins]; si++) {
+ Ins ni;
+ ni.op = si->op;
+ ni.cls = si->cls;
+ ni.to = remap_tmp(si->to, st.tmp_map);
+ ni.arg[0] = remap_tmp(si->arg[0], st.tmp_map);
+ ni.arg[1] = remap_tmp(si->arg[1], st.tmp_map);
+ addins(ctx, &dst->ins, &dst->nins, &ni);
+ }
+
+ // ── jmp ──
+ if (is_header) {
+ // Jjnz cmp, body_first, exit -> Jjmp blk_map[body_first.id]
+ dst->jmp.type = Jjmp;
+ dst->jmp.arg = Ref::none();
+ auto it = st.blk_map.find(src->s1->id);
+ assert(it != st.blk_map.end());
+ dst->s1 = it->second;
+ dst->s2 = nullptr;
+ } else if (is_latch) {
+ // Kept for now, caller rewrites when linking across copies
+ dst->jmp.type = Jjmp;
+ dst->jmp.arg = Ref::none();
+ dst->s1 = nullptr;
+ dst->s2 = nullptr;
+ } else {
+ dst->jmp.type = src->jmp.type;
+ dst->jmp.arg = remap_tmp(src->jmp.arg, st.tmp_map);
+ dst->s1 = nullptr;
+ dst->s2 = nullptr;
+ if (src->s1) {
+ auto it = st.blk_map.find(src->s1->id);
+ dst->s1 = (it != st.blk_map.end()) ? it->second : src->s1;
+ }
+ if (src->s2) {
+ auto it = st.blk_map.find(src->s2->id);
+ dst->s2 = (it != st.blk_map.end()) ? it->second : src->s2;
+ }
+ }
+}
+
+// Clone body N times sequentially to replace original loop. Matching by Blk* pointer is the only
+// robust way.
+Ref phi_arg_for_pred(Phi* p, Blk* pred) {
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == pred)
+ return p->arg[k];
+ die("phi_arg_for_pred: pred not found in phi.blk[]");
+}
+
+// Prerequisites: detect_induction successful, N = induction.trip.
+void full_unroll(Ctx* ctx, Fn* fn, const LoopShape& L, int64_t N) {
+ // Collect body-defined tmps
+ std::unordered_set<uint> body_defined;
+ for (Blk* b : L.body) {
+ for (Phi* p = b->phi; p; p = p->link)
+ if (rtype(p->to) == RTmp)
+ body_defined.insert(p->to.val);
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (rtype(i->to) == RTmp && i->to.val != 0)
+ body_defined.insert(i->to.val);
+ }
+
+ // Collect header phi list (in linked list order, to re-define them later)
+ std::vector<Phi*> header_phis;
+ for (Phi* p = L.header->phi; p; p = p->link)
+ header_phis.push_back(p);
+
+ // Clone for each iteration
+ std::vector<CopyState> copies(static_cast<size_t>(N));
+
+ // Map previous latch tmps
+ std::unordered_map<uint, Ref> prev_latch_tmp_map;
+
+ static int unroll_seq = 0;
+ int my_seq = unroll_seq++;
+
+ for (int64_t k = 0; k < N; k++) {
+ CopyState& st = copies[static_cast<size_t>(k)];
+
+ // Allocate new blk for each body block
+ for (Blk* b : L.body) {
+ Blk* nb = newblk(ctx);
+ char buf[128];
+ std::snprintf(buf, sizeof(buf), "u%d.%lld.%s", my_seq, static_cast<long long>(k),
+ b->name ? b->name : "blk");
+ nb->name = strf(ctx, PFn, "%s", buf);
+ nb->id = fn->nblk++;
+ st.blk_map[b->id] = nb;
+ }
+ st.first_blk = st.blk_map[L.header->id];
+ st.last_blk = st.blk_map[L.latch->id];
+
+ // Assign fresh ids to body-defined tmps; then populate header phi.tos with entry values
+ for (uint tv : body_defined) {
+ const char* prfx = fn->tmp[tv].name ? fn->tmp[tv].name : "u";
+ st.tmp_map[tv] = newtmp(ctx, prfx, fn->tmp[tv].cls, fn);
+ }
+ for (Phi* p : header_phis) {
+ Ref entry_val;
+ if (k == 0) {
+ entry_val = phi_arg_for_pred(p, L.preheader);
+ } else {
+ Ref prev_latch_ref = phi_arg_for_pred(p, L.latch);
+ entry_val = remap_tmp(prev_latch_ref, prev_latch_tmp_map);
+ }
+ if (rtype(p->to) == RTmp)
+ st.tmp_map[p->to.val] = entry_val;
+ }
+
+ // Clone each body block
+ for (Blk* b : L.body) {
+ Blk* dst = st.blk_map[b->id];
+ bool is_h = (b == L.header);
+ bool is_l = (b == L.latch);
+ // Single-block loop specialization
+ clone_body_blk(ctx, st, b, dst, is_h, is_l && !is_h);
+ }
+
+ // Save current tmp_map for next round
+ prev_latch_tmp_map = st.tmp_map;
+ }
+
+ // Link copies: copy_k's latch_clone.jmp = Jjmp copy_{k+1}.first
+ for (int64_t k = 0; k < N - 1; k++) {
+ Blk* lc = copies[static_cast<size_t>(k)].last_blk;
+ Blk* nh = copies[static_cast<size_t>(k + 1)].first_blk;
+ // Single-block loop self-loop fix: rewrite last_blk's Jjmp to point to next copy
+ lc->jmp.type = Jjmp;
+ lc->jmp.arg = Ref::none();
+ lc->s1 = nh;
+ lc->s2 = nullptr;
+ }
+
+ // Last round's latch_clone: append Ocopy to re-def header phi.tos, jmp points to exit_bb
+ {
+ CopyState& last = copies[static_cast<size_t>(N - 1)];
+ Blk* lc = last.last_blk;
+ for (Phi* p : header_phis) {
+ Ref final_val = remap_tmp(phi_arg_for_pred(p, L.latch), last.tmp_map);
+ Ins re_def;
+ re_def.op = Ocopy;
+ re_def.cls = static_cast<uint>(p->cls) & 3u;
+ re_def.to = p->to; // Original header phi.to (now dead, SSA def monopolized)
+ re_def.arg[0] = final_val;
+ re_def.arg[1] = Ref::none();
+ addins(ctx, &lc->ins, &lc->nins, &re_def);
+ }
+ lc->jmp.type = Jjmp;
+ lc->jmp.arg = Ref::none();
+ lc->s1 = L.exit_bb;
+ lc->s2 = nullptr;
+ }
+
+ // Point preheader to copy_0.first; clear original header's pred references (leave for fillcfg
+ // to rebuild)
+ Blk* new_entry = copies[0].first_blk;
+ if (L.preheader->s1 == L.header)
+ L.preheader->s1 = new_entry;
+ if (L.preheader->s2 == L.header)
+ L.preheader->s2 = new_entry;
+
+ // Fix exit_bb phi incoming edges: replace header with last copy's latch_clone
+ Blk* new_last = copies[static_cast<size_t>(N - 1)].last_blk;
+ for (Phi* p = L.exit_bb->phi; p; p = p->link) {
+ for (uint kk = 0; kk < p->narg; kk++) {
+ if (p->blk[kk] == L.header)
+ p->blk[kk] = new_last;
+ }
+ }
+
+ // Link chain: preheader -> copy_0_blocks -> ... -> copy_{N-1}_blocks -> original preheader.link
+ Blk* saved_link = L.preheader->link;
+ Blk** plink = &L.preheader->link;
+ for (int64_t k = 0; k < N; k++) {
+ const CopyState& st = copies[static_cast<size_t>(k)];
+ // Link in L.body order
+ for (Blk* b : L.body) {
+ Blk* nb = st.blk_map.at(b->id);
+ *plink = nb;
+ plink = &nb->link;
+ }
+ }
+ *plink = saved_link;
+
+ if (debug['U']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> unroll full N=%lld in %s (header b%u, body=%zu, ins=%d)\n",
+ static_cast<long long>(N), fn->name, L.header->id, L.body.size(),
+ count_body_ins(L));
+ }
+}
+
+// Partial Unroll
+
+Blk* partial_unroll(Ctx* ctx, Fn* fn, const LoopShape& L, const Induction& ind, int F) {
+ // body-defined tmps
+ std::unordered_set<uint> body_defined;
+ for (Blk* b : L.body) {
+ for (Phi* p = b->phi; p; p = p->link)
+ if (rtype(p->to) == RTmp)
+ body_defined.insert(p->to.val);
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (rtype(i->to) == RTmp && i->to.val != 0)
+ body_defined.insert(i->to.val);
+ }
+
+ // header phi list
+ std::vector<Phi*> header_phis;
+ for (Phi* p = L.header->phi; p; p = p->link)
+ header_phis.push_back(p);
+
+ static int unroll_seq_p = 0;
+ int my_seq = unroll_seq_p++;
+
+ // Create main_head, build its phi list first (loopback edge filled later)
+ Blk* main_head = newblk(ctx);
+ main_head->name = strf(ctx, PFn, "u%d.main.head", my_seq);
+ main_head->id = fn->nblk++;
+
+ std::unordered_map<uint, Ref> head_to_main; // original phi.to.val -> main_head phi.to
+ std::vector<Phi*> main_phis;
+ Phi** ptail = &main_head->phi;
+ for (Phi* p : header_phis) {
+ Phi* pm = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ pm->cls = p->cls;
+ pm->visit = 0;
+ const char* prfx =
+ (rtype(p->to) == RTmp && fn->tmp[p->to.val].name) ? fn->tmp[p->to.val].name : "u";
+ pm->to = newtmp(ctx, prfx, p->cls, fn);
+ pm->narg = 2;
+ pm->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn));
+ pm->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn));
+ pm->arg[0] = phi_arg_for_pred(p, L.preheader);
+ pm->blk[0] = L.preheader;
+ pm->arg[1] = Ref::none();
+ pm->blk[1] = nullptr;
+ pm->link = nullptr;
+ *ptail = pm;
+ ptail = &pm->link;
+ main_phis.push_back(pm);
+ if (rtype(p->to) == RTmp)
+ head_to_main[p->to.val] = pm->to;
+ }
+
+ // Clone body F times, same as full_unroll (except clone0 entry uses main_head phi.to)
+ std::vector<CopyState> copies(static_cast<size_t>(F));
+ std::unordered_map<uint, Ref> prev_latch_tmp_map;
+ for (int k = 0; k < F; k++) {
+ CopyState& st = copies[static_cast<size_t>(k)];
+ for (Blk* b : L.body) {
+ Blk* nb = newblk(ctx);
+ nb->name = strf(ctx, PFn, "u%d.%d.%s", my_seq, k, b->name ? b->name : "blk");
+ nb->id = fn->nblk++;
+ st.blk_map[b->id] = nb;
+ }
+ st.first_blk = st.blk_map[L.header->id];
+ st.last_blk = st.blk_map[L.latch->id];
+ for (uint tv : body_defined) {
+ const char* prfx = fn->tmp[tv].name ? fn->tmp[tv].name : "u";
+ st.tmp_map[tv] = newtmp(ctx, prfx, fn->tmp[tv].cls, fn);
+ }
+ for (Phi* p : header_phis) {
+ Ref entry_val;
+ if (k == 0) {
+ auto it = head_to_main.find(p->to.val);
+ entry_val =
+ (it != head_to_main.end()) ? it->second : phi_arg_for_pred(p, L.preheader);
+ } else {
+ entry_val = remap_tmp(phi_arg_for_pred(p, L.latch), prev_latch_tmp_map);
+ }
+ if (rtype(p->to) == RTmp)
+ st.tmp_map[p->to.val] = entry_val;
+ }
+ for (Blk* b : L.body) {
+ Blk* dst = st.blk_map[b->id];
+ bool is_h = (b == L.header);
+ bool is_l = (b == L.latch);
+ clone_body_blk(ctx, st, b, dst, is_h, is_l && !is_h);
+ }
+ prev_latch_tmp_map = st.tmp_map;
+ }
+
+ // Link: copy_k.last -> copy_{k+1}.first
+ for (int k = 0; k < F - 1; k++) {
+ Blk* lc = copies[static_cast<size_t>(k)].last_blk;
+ lc->jmp.type = Jjmp;
+ lc->jmp.arg = Ref::none();
+ lc->s1 = copies[static_cast<size_t>(k + 1)].first_blk;
+ lc->s2 = nullptr;
+ }
+ // Last latch -> main_head (back-edge)
+ Blk* last_latch = copies[static_cast<size_t>(F - 1)].last_blk;
+ last_latch->jmp.type = Jjmp;
+ last_latch->jmp.arg = Ref::none();
+ last_latch->s1 = main_head;
+ last_latch->s2 = nullptr;
+
+ // Fill main_head phi's loopback incoming edge
+ for (size_t idx = 0; idx < header_phis.size(); idx++) {
+ Phi* p = header_phis[idx];
+ Phi* pm = main_phis[idx];
+ pm->arg[1] =
+ remap_tmp(phi_arg_for_pred(p, L.latch), copies[static_cast<size_t>(F - 1)].tmp_map);
+ pm->blk[1] = last_latch;
+ }
+
+ // main_head guard: adj_bound = bound - (F-1)*step; cmp(i_main, adj)
+ main_head->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn));
+ main_head->nins = 0;
+ int kls = (ind.cmp_op == Ocsltl || ind.cmp_op == Ocslel) ? Kl : Kw;
+ int64_t C = static_cast<int64_t>(F - 1) * ind.step;
+ Ref adj_ref;
+ if (ind.bound_const) {
+ adj_ref = getcon(ctx, ind.bound - C, fn);
+ } else {
+ Ref adj_tmp = newtmp(ctx, "u", kls, fn);
+ Ins sub;
+ sub.op = Osub;
+ sub.cls = static_cast<uint>(kls) & 3u;
+ sub.to = adj_tmp;
+ sub.arg[0] = ind.bound_ref;
+ sub.arg[1] = getcon(ctx, C, fn);
+ addins(ctx, &main_head->ins, &main_head->nins, &sub);
+ adj_ref = adj_tmp;
+ }
+ Ref iv_main = head_to_main.at(ind.iv_ref.val);
+ Ref gcmp = newtmp(ctx, "u", Kw, fn);
+ Ins cmpi;
+ cmpi.op = static_cast<uint>(ind.cmp_op);
+ cmpi.cls = static_cast<uint>(ind.cmp_cls) & 3u;
+ cmpi.to = gcmp;
+ cmpi.arg[0] = iv_main;
+ cmpi.arg[1] = adj_ref;
+ addins(ctx, &main_head->ins, &main_head->nins, &cmpi);
+ main_head->jmp.type = Jjnz;
+ main_head->jmp.arg = gcmp;
+ main_head->s1 = copies[0].first_blk;
+ main_head->s2 = L.header; // remainder loop
+
+ // preheader -> main_head
+ if (L.preheader->s1 == L.header)
+ L.preheader->s1 = main_head;
+ if (L.preheader->s2 == L.header)
+ L.preheader->s2 = main_head;
+
+ // Remainder header phi's preheader edge: replace with main_head, value is pm.to
+ for (size_t idx = 0; idx < header_phis.size(); idx++) {
+ Phi* p = header_phis[idx];
+ Phi* pm = main_phis[idx];
+ for (uint kk = 0; kk < p->narg; kk++) {
+ if (p->blk[kk] == L.preheader) {
+ p->blk[kk] = main_head;
+ p->arg[kk] = pm->to;
+ }
+ }
+ }
+
+ // link chain: preheader -> main_head -> clones -> original preheader.link
+ Blk* saved_link = L.preheader->link;
+ L.preheader->link = main_head;
+ Blk** plink = &main_head->link;
+ for (int k = 0; k < F; k++) {
+ for (Blk* b : L.body) {
+ Blk* nb = copies[static_cast<size_t>(k)].blk_map.at(b->id);
+ *plink = nb;
+ plink = &nb->link;
+ }
+ }
+ *plink = saved_link;
+
+ if (debug['U']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> unroll partial F=%d in %s (header b%u, body=%zu, runtime_bound=%d)\n", F,
+ fn->name, L.header->id, L.body.size(), ind.bound_const ? 0 : 1);
+ }
+ return main_head;
+}
+
+// Pass driver
+
+bool unroll_pass(Ctx* ctx, Fn* fn, std::unordered_set<Blk*>& unrolled) {
+ auto loops = find_simple_loops(fn);
+ bool any = false;
+
+ for (auto& L : loops) {
+ // Prevent infinite partial unrolling by skipping already processed main_heads.
+ if (unrolled.count(L.header))
+ continue;
+
+ Induction ind;
+ if (!detect_induction(fn, L, ind))
+ continue;
+ int64_t N = ind.trip;
+ int body_ins = count_body_ins(L);
+
+ // Prioritize full unroll: trip known and <= kFullUnrollTripMax, and N*body within budget
+ if (N >= 1 && N <= kFullUnrollTripMax &&
+ static_cast<int64_t>(body_ins) * N <= kFullUnrollBudget) {
+ full_unroll(ctx, fn, L, N);
+ any = true;
+ break;
+ }
+
+ if (N == 0)
+ continue; // Loop doesn't execute, leave to simplcfg
+
+ // Otherwise try partial unroll: N=-1 (runtime bound) or N > kFullUnrollTripMax
+ // Skip if body is too large (F*body limit kPartialMaxBodyIns*F = 256 ins).
+ if (body_ins <= 0 || body_ins > kPartialMaxBodyIns)
+ continue;
+ Blk* mh = partial_unroll(ctx, fn, L, ind, kPartialFactor);
+ unrolled.insert(mh);
+ any = true;
+ break;
+ }
+ return any;
+}
+
+} // namespace
+
+void unroll(Ctx* ctx, Fn* fn) {
+ // Caller guarantees fillcfg + filldom + filluse are ready.
+ std::unordered_set<Blk*>
+ unrolled; // main_heads already partial-unrolled, persistent across fixpoints
+ int iter = 0;
+ for (; iter < kUnrollFixpointIters; iter++) {
+ if (!unroll_pass(ctx, fn, unrolled))
+ break;
+ // CFG modified, rebuild for next round.
+ fillcfg(ctx, fn);
+ filluse(ctx, fn);
+ filldom(fn);
+ }
+ if (debug['U'] && iter > 0) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> unroll %s: %d round%s\n", fn->name, iter, iter == 1 ? "" : "s");
+ }
+}
+
+} // namespace rcc
diff --git a/src/mid/unswitch.cc b/src/mid/unswitch.cc
new file mode 100644
index 0000000..fa8bf73
--- /dev/null
+++ b/src/mid/unswitch.cc
@@ -0,0 +1,447 @@
+/* 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 unswitch.cc
+/// @brief Loop Unswitching.
+///
+/// See docs/unswitch.md for architectural notes.
+
+#include "rcc/all.h"
+
+#include <cassert>
+#include <cstdio>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+namespace rcc {
+
+namespace {
+
+constexpr int kUnswitchMaxBodyIns = 64;
+constexpr int kUnswitchFixpointIters = 3;
+
+// Loop Identification
+struct LoopShape {
+ Blk* header;
+ Blk* preheader;
+ Blk* latch;
+ Blk* exit_bb;
+ std::vector<Blk*> body; // includes header / latch
+};
+
+// Find simple loops (derived from unroll.cc).
+std::vector<LoopShape> find_simple_loops(Fn* fn) {
+ std::vector<LoopShape> out;
+ for (Blk* b = fn->start; b; b = b->link)
+ b->visit = ~0u;
+
+ for (uint i = 0; i < fn->nblk; i++) {
+ Blk* hd = fn->rpo[i];
+ if (hd->id == ~0u)
+ continue;
+
+ int back_cnt = 0, pre_cnt = 0;
+ uint latch_idx = 0, pre_idx = 0;
+ for (uint k = 0; k < hd->npred; k++) {
+ Blk* p = hd->pred[k];
+ if (p->id == ~0u)
+ continue;
+ if (p->id >= i) {
+ back_cnt++;
+ latch_idx = k;
+ } else {
+ pre_cnt++;
+ pre_idx = k;
+ }
+ }
+ if (back_cnt != 1 || pre_cnt != 1)
+ continue;
+
+ Blk* latch = hd->pred[latch_idx];
+ Blk* preheader = hd->pred[pre_idx];
+
+ // Reverse DFS to collect body
+ std::vector<Blk*> body = {hd};
+ std::vector<Blk*> stack = {latch};
+ hd->visit = hd->id;
+ while (!stack.empty()) {
+ Blk* n = stack.back();
+ stack.pop_back();
+ if (n->visit == hd->id)
+ continue;
+ n->visit = hd->id;
+ body.push_back(n);
+ for (uint k = 0; k < n->npred; k++)
+ stack.push_back(n->pred[k]);
+ }
+
+ // single-exit gate
+ if (hd->jmp.type != Jjnz)
+ continue;
+ Blk* exit_bb = hd->s2;
+ if (!exit_bb || !hd->s1)
+ continue;
+
+ auto in_body = [&](Blk* x) -> bool {
+ for (Blk* bb : body)
+ if (bb == x)
+ return true;
+ return false;
+ };
+ if (in_body(exit_bb))
+ continue;
+ if (!in_body(hd->s1))
+ continue;
+
+ bool ok = true;
+ for (Blk* bb : body) {
+ if (bb == hd)
+ continue;
+ if (isret(bb->jmp.type)) {
+ ok = false;
+ break;
+ }
+ auto check = [&](Blk* s) -> bool { return !s || in_body(s); };
+ if (!check(bb->s1) || !check(bb->s2)) {
+ ok = false;
+ break;
+ }
+ }
+ if (!ok)
+ continue;
+
+ LoopShape L;
+ L.header = hd;
+ L.preheader = preheader;
+ L.latch = latch;
+ L.exit_bb = exit_bb;
+ L.body = std::move(body);
+ out.push_back(std::move(L));
+ }
+ return out;
+}
+
+// Loop Invariance
+bool ref_is_invariant(Fn* fn, const LoopShape& L, Ref r) {
+ if (rtype(r) == RCon)
+ return true;
+ if (rtype(r) != RTmp)
+ return false;
+ if (r.val >= static_cast<uint>(fn->ntmp))
+ return false;
+ uint bid = fn->tmp[r.val].bid;
+ if (bid == ~0u)
+ return true;
+ for (Blk* b : L.body)
+ if (b->id == bid)
+ return false;
+ return true;
+}
+
+// Find a hoistable Jjnz branch. Returns nullptr if no candidate.
+Blk* find_unswitch_blk(Fn* fn, const LoopShape& L) {
+ for (Blk* b : L.body) {
+ if (b == L.header)
+ continue; // header.Jjnz is loop exit, cannot hoist
+ if (b == L.latch)
+ continue; // Jjnz on latch is usually back-edge directed, skip to simplify analysis
+ if (b->jmp.type != Jjnz)
+ continue;
+ if (rtype(b->jmp.arg) != RTmp)
+ continue; // RCon left to simplcfg
+ if (!ref_is_invariant(fn, L, b->jmp.arg))
+ continue;
+ if (!b->s1 || !b->s2 || b->s1 == b->s2)
+ continue;
+ bool s1_in = false, s2_in = false;
+ for (Blk* bb : L.body) {
+ if (bb == b->s1)
+ s1_in = true;
+ if (bb == b->s2)
+ s2_in = true;
+ }
+ if (!s1_in || !s2_in)
+ continue;
+ return b;
+ }
+ return nullptr;
+}
+
+int count_body_ins(const LoopShape& L) {
+ int n = 0;
+ for (Blk* b : L.body) {
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (i->op != Onop)
+ n++;
+ for (Phi* p = b->phi; p; p = p->link)
+ (void)p, n++;
+ }
+ return n;
+}
+
+// Clone Infrastructure (derived from unroll.cc with minor adaptations)
+struct CopyState {
+ std::unordered_map<uint, Blk*> blk_map; // body blk.id -> clone
+ std::unordered_map<uint, Ref> tmp_map; // body-defined tmp.val -> clone Ref
+};
+
+Ref remap_tmp(Ref r, const std::unordered_map<uint, Ref>& m) {
+ if (rtype(r) != RTmp)
+ return r;
+ auto it = m.find(r.val);
+ return (it != m.end()) ? it->second : r;
+}
+
+// Generic block clone.
+void clone_blk(Ctx* ctx, const CopyState& st, Blk* src, Blk* dst) {
+ dst->phi = nullptr;
+ Phi** tail = &dst->phi;
+ for (Phi* sp = src->phi; sp; sp = sp->link) {
+ Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi)));
+ np->to = remap_tmp(sp->to, st.tmp_map);
+ np->cls = sp->cls;
+ np->visit = 0;
+ np->narg = sp->narg;
+ np->arg = static_cast<Ref*>(vnew(ctx, sp->narg, sizeof(Ref), PFn));
+ np->blk = static_cast<Blk**>(vnew(ctx, sp->narg, sizeof(Blk*), PFn));
+ for (uint k = 0; k < sp->narg; k++) {
+ np->arg[k] = remap_tmp(sp->arg[k], st.tmp_map);
+ auto it = st.blk_map.find(sp->blk[k]->id);
+ np->blk[k] = (it != st.blk_map.end()) ? it->second : sp->blk[k];
+ }
+ np->link = nullptr;
+ *tail = np;
+ tail = &np->link;
+ }
+
+ dst->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn));
+ dst->nins = 0;
+ for (Ins* si = src->ins; si < &src->ins[src->nins]; si++) {
+ Ins ni;
+ ni.op = si->op;
+ ni.cls = si->cls;
+ ni.to = remap_tmp(si->to, st.tmp_map);
+ ni.arg[0] = remap_tmp(si->arg[0], st.tmp_map);
+ ni.arg[1] = remap_tmp(si->arg[1], st.tmp_map);
+ addins(ctx, &dst->ins, &dst->nins, &ni);
+ }
+
+ auto map_succ = [&](Blk* s) -> Blk* {
+ if (!s)
+ return nullptr;
+ auto it = st.blk_map.find(s->id);
+ return (it != st.blk_map.end()) ? it->second : s;
+ };
+ dst->jmp.type = src->jmp.type;
+ dst->jmp.arg = remap_tmp(src->jmp.arg, st.tmp_map);
+ dst->s1 = map_succ(src->s1);
+ dst->s2 = map_succ(src->s2);
+}
+
+CopyState clone_loop(Ctx* ctx, Fn* fn, const LoopShape& L, const char* tag) {
+ CopyState st;
+
+ std::unordered_set<uint> body_defined;
+ for (Blk* b : L.body) {
+ for (Phi* p = b->phi; p; p = p->link)
+ if (rtype(p->to) == RTmp)
+ body_defined.insert(p->to.val);
+ for (Ins* i = b->ins; i < &b->ins[b->nins]; i++)
+ if (rtype(i->to) == RTmp && i->to.val != 0)
+ body_defined.insert(i->to.val);
+ }
+
+ for (Blk* b : L.body) {
+ Blk* nb = newblk(ctx);
+ nb->name = strf(ctx, PFn, "%s.%s", tag, b->name ? b->name : "blk");
+ nb->id = fn->nblk++;
+ st.blk_map[b->id] = nb;
+ }
+
+ for (uint tv : body_defined) {
+ const char* prfx = fn->tmp[tv].name ? fn->tmp[tv].name : "u";
+ st.tmp_map[tv] = newtmp(ctx, prfx, fn->tmp[tv].cls, fn);
+ }
+
+ for (Blk* b : L.body)
+ clone_blk(ctx, st, b, st.blk_map[b->id]);
+
+ return st;
+}
+
+// Remove dangling phi incoming edges from deactivated successors.
+void remove_phi_entry(Blk* succ, Blk* old_pred) {
+ for (Phi* p = succ->phi; p; p = p->link) {
+ uint w = 0;
+ for (uint k = 0; k < p->narg; k++) {
+ if (p->blk[k] == old_pred)
+ continue;
+ if (w != k) {
+ p->arg[w] = p->arg[k];
+ p->blk[w] = p->blk[k];
+ }
+ w++;
+ }
+ p->narg = w;
+ }
+}
+
+void do_unswitch(Ctx* ctx, Fn* fn, const LoopShape& L, Blk* unsw_blk) {
+ Ref cond = unsw_blk->jmp.arg;
+ Blk* orig_s1 = unsw_blk->s1;
+ Blk* orig_s2 = unsw_blk->s2;
+
+ CopyState A = clone_loop(ctx, fn, L, "uA");
+ CopyState B = clone_loop(ctx, fn, L, "uB");
+
+ Blk* header_A = A.blk_map.at(L.header->id);
+ Blk* header_B = B.blk_map.at(L.header->id);
+ Blk* unsw_A = A.blk_map.at(unsw_blk->id);
+ Blk* unsw_B = B.blk_map.at(unsw_blk->id);
+
+ // In A, collapse to true branch (orig.s1)
+ unsw_A->jmp.type = Jjmp;
+ unsw_A->jmp.arg = Ref::none();
+ unsw_A->s1 = A.blk_map.at(orig_s1->id);
+ unsw_A->s2 = nullptr;
+ // orig.s2 clone in A is no longer reached by unsw_A; must remove phi edges from unsw_A
+ remove_phi_entry(A.blk_map.at(orig_s2->id), unsw_A);
+
+ // In B, collapse to false branch (orig.s2)
+ unsw_B->jmp.type = Jjmp;
+ unsw_B->jmp.arg = Ref::none();
+ unsw_B->s1 = B.blk_map.at(orig_s2->id);
+ unsw_B->s2 = nullptr;
+ remove_phi_entry(B.blk_map.at(orig_s1->id), unsw_B);
+
+ // guard block: Jjnz cond, header_A, header_B
+ Blk* guard = newblk(ctx);
+ guard->name = strf(ctx, PFn, "uG.%s", L.header->name ? L.header->name : "");
+ guard->id = fn->nblk++;
+ guard->phi = nullptr;
+ guard->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn));
+ guard->nins = 0;
+ guard->jmp.type = Jjnz;
+ guard->jmp.arg = cond;
+ guard->s1 = header_A;
+ guard->s2 = header_B;
+
+ // preheader -> guard (replaces original preheader -> header)
+ if (L.preheader->s1 == L.header)
+ L.preheader->s1 = guard;
+ if (L.preheader->s2 == L.header)
+ L.preheader->s2 = guard;
+
+ // Update header_A / header_B phi.blk[k]==preheader to guard
+ auto fix_hdr_pred = [&](Blk* hdr_clone) {
+ for (Phi* p = hdr_clone->phi; p; p = p->link)
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == L.preheader)
+ p->blk[k] = guard;
+ };
+ fix_hdr_pred(header_A);
+ fix_hdr_pred(header_B);
+
+ // exit_bb phi: split blk==L.header incoming edges into two
+ Blk* exit = L.exit_bb;
+ for (Phi* p = exit->phi; p; p = p->link) {
+ uint header_entries = 0;
+ for (uint k = 0; k < p->narg; k++)
+ if (p->blk[k] == L.header)
+ header_entries++;
+ if (header_entries == 0)
+ continue;
+
+ uint new_narg = p->narg + header_entries; // Expand each into 2 = +1
+ Ref* new_arg = static_cast<Ref*>(vnew(ctx, new_narg, sizeof(Ref), PFn));
+ Blk** new_blk = static_cast<Blk**>(vnew(ctx, new_narg, sizeof(Blk*), PFn));
+ uint w = 0;
+ for (uint k = 0; k < p->narg; k++) {
+ if (p->blk[k] == L.header) {
+ new_arg[w] = remap_tmp(p->arg[k], A.tmp_map);
+ new_blk[w] = header_A;
+ w++;
+ new_arg[w] = remap_tmp(p->arg[k], B.tmp_map);
+ new_blk[w] = header_B;
+ w++;
+ } else {
+ new_arg[w] = p->arg[k];
+ new_blk[w] = p->blk[k];
+ w++;
+ }
+ }
+ p->narg = new_narg;
+ p->arg = new_arg;
+ p->blk = new_blk;
+ }
+
+ // link chain: preheader -> guard -> A.body -> B.body -> original preheader.link
+ Blk* saved = L.preheader->link;
+ L.preheader->link = guard;
+ Blk** plink = &guard->link;
+ for (Blk* b : L.body) {
+ *plink = A.blk_map.at(b->id);
+ plink = &(*plink)->link;
+ }
+ for (Blk* b : L.body) {
+ *plink = B.blk_map.at(b->id);
+ plink = &(*plink)->link;
+ }
+ *plink = saved;
+
+ if (debug['W']) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> unswitch %s: hdr b%u, unsw b%u, body=%zu\n", fn->name, L.header->id,
+ unsw_blk->id, L.body.size());
+ }
+}
+
+bool unswitch_pass(Ctx* ctx, Fn* fn) {
+ auto loops = find_simple_loops(fn);
+ for (auto& L : loops) {
+ Blk* u = find_unswitch_blk(fn, L);
+ if (!u)
+ continue;
+ if (count_body_ins(L) > kUnswitchMaxBodyIns)
+ continue;
+ do_unswitch(ctx, fn, L, u);
+ return true; // Process one per pass to avoid scanning mutated CFG
+ }
+ return false;
+}
+
+} // namespace
+
+void unswitch(Ctx* ctx, Fn* fn) {
+ // Caller should have done fillcfg + filluse; filldom not needed (only Tmp.bid is used).
+ int iter = 0;
+ for (; iter < kUnswitchFixpointIters; iter++) {
+ if (!unswitch_pass(ctx, fn))
+ break;
+ fillcfg(ctx, fn);
+ filluse(ctx, fn);
+ }
+ if (debug['W'] && iter > 0) {
+ FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr;
+ fprintf(out, "> unswitch %s: %d round%s\n", fn->name, iter, iter == 1 ? "" : "s");
+ }
+}
+
+} // namespace rcc
diff --git a/src/util.cc b/src/util.cc
new file mode 100644
index 0000000..8c5ce13
--- /dev/null
+++ b/src/util.cc
@@ -0,0 +1,778 @@
+/* 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 util.cc
+/// @brief Infrastructure: Memory management, growable arrays.
+
+#include "rcc/all.h"
+
+#include <cstdarg>
+#include <vector>
+
+namespace rcc {
+
+// Global Read-Only Resources
+
+Typ* typ;
+Target T = {};
+
+char debug['Z' + 1] = {};
+Op optab[NOp];
+
+namespace {
+struct OptabInit {
+ OptabInit() {
+ for (size_t i = 0; i < NOp; ++i) {
+ const OpInfo& info = kops[i];
+ Op& o = optab[i];
+ o.name = const_cast<char*>(info.name);
+ for (int a = 0; a < 2; ++a)
+ for (int b = 0; b < 4; ++b)
+ o.argcls[a][b] = info.argcls[a][b];
+ o.canfold = (info.flags & Fold) != 0;
+ o.hasid = (info.flags & Id) != 0;
+ o.idval = (info.flags & IdV) != 0;
+ o.commutes = (info.flags & Comm) != 0;
+ o.assoc = (info.flags & Assoc) != 0;
+ o.idemp = (info.flags & Idemp) != 0;
+ o.cmpeqwl = (info.flags & CmpEW) != 0;
+ o.cmplgtewl = (info.flags & CmpLW) != 0;
+ o.eqval = (info.flags & EqV) != 0;
+ o.pinned = (info.flags & Pin) != 0;
+ }
+ }
+} optab_init_;
+} // namespace
+
+// SubPool: per-Fn Memory Pool
+
+void* subpool_alloc(SubPool* sp, size_t n) {
+ if (n == 0)
+ return nullptr;
+ void* p = emalloc(n);
+ sp->chunks.push_back(p);
+ return p;
+}
+
+void subpool_free(SubPool* sp) {
+ for (void* p : sp->chunks)
+ free(p);
+ sp->chunks.clear();
+}
+
+// Ctx Lifecycle
+
+void* emalloc(size_t n) {
+ void* p = calloc(1, n);
+ if (!p)
+ die("emalloc, out of memory");
+ return p;
+}
+
+void Ctx::init() {
+ assert(!insb && "Ctx::init called twice");
+ insb = static_cast<Ins*>(emalloc(NIns * sizeof(Ins)));
+ curi = insb + NIns;
+}
+
+void Ctx::bind(Fn* f) {
+ assert(f && f->_subpool && "bind requires fn with attached SubPool");
+ subpool = f->_subpool;
+ curi = insb + NIns;
+ load_curf = nullptr;
+ load_inum = 0;
+ load_ilog = nullptr;
+ load_nlog = 0;
+ ssa_namel = nullptr;
+ gvn_con01[0] = Ref::none();
+ gvn_con01[1] = Ref::none();
+ gvn_tbl = nullptr;
+ gvn_tbln = 0;
+ rega_regu = 0;
+ rega_tmp = nullptr;
+ rega_npm = 0;
+ rega_loop = 0;
+ rega_stmov = 0;
+ rega_stblk = 0;
+ emit_stash = nullptr;
+ spill_fp_live_peak = 0;
+ fn = f;
+}
+
+void Ctx::release() {
+ if (subpool) {
+ subpool_free(subpool);
+ delete subpool;
+ subpool = nullptr;
+ }
+ fn = nullptr;
+}
+
+Ctx::~Ctx() {
+ if (subpool) {
+ subpool_free(subpool);
+ delete subpool;
+ }
+ if (insb)
+ free(insb);
+ if (out_buf)
+ fclose(out_buf);
+ if (dbg_buf)
+ fclose(dbg_buf);
+ // ssa.cc Name pool is allocated via emalloc and reused across functions, ~Ctx must drain it.
+ ssa_drain_namel(this);
+}
+
+// alloc / freeall
+
+void* alloc(Ctx* ctx, size_t n) {
+ assert(ctx && ctx->subpool);
+ return subpool_alloc(ctx->subpool, n);
+}
+
+void freeall(Ctx* ctx) {
+ assert(ctx && ctx->subpool);
+ subpool_free(ctx->subpool);
+}
+
+// Growable Arrays (vnew / vgrow / vfree)
+
+struct ArrayHdr {
+ ulong magic;
+ ulong cap;
+ size_t esz;
+ Pool pool;
+};
+
+static constexpr ulong AHDR_MAGIC = 0xA77A7007u;
+static constexpr ulong VMIN_CAP = 2;
+
+/// Retreat from user pointer to header
+static ArrayHdr* hdr_of(void* p) {
+ auto* h = static_cast<ArrayHdr*>(p) - 1;
+ assert(h->magic == AHDR_MAGIC);
+ return h;
+}
+
+void* vnew(Ctx* ctx, ulong len, size_t esz, Pool pl) {
+ ulong cap = VMIN_CAP;
+ while (cap < len)
+ cap *= 2;
+
+ size_t total = sizeof(ArrayHdr) + cap * esz;
+ void* raw = (pl == PHeap) ? emalloc(total) : alloc(ctx, total);
+
+ auto* h = static_cast<ArrayHdr*>(raw);
+ h->magic = AHDR_MAGIC;
+ h->cap = cap;
+ h->esz = esz;
+ h->pool = pl;
+ return h + 1;
+}
+
+void vfree(void* p) {
+ ArrayHdr* h = hdr_of(p);
+ if (h->pool == PHeap) {
+ h->magic = 0;
+ free(h);
+ }
+}
+
+void vgrow(Ctx* ctx, void* vp, ulong len) {
+ void** pp = static_cast<void**>(vp);
+ ArrayHdr* h = hdr_of(*pp);
+
+ if (h->cap >= len)
+ return;
+
+ void* nv = vnew(ctx, len, h->esz, h->pool);
+ memcpy(nv, *pp, h->cap * h->esz);
+ vfree(*pp);
+ *pp = nv;
+}
+
+// String Pool (intern / str)
+
+uint32_t hash(const char* s) {
+ uint32_t h = 5381;
+ for (; *s; s++)
+ h = ((h << 5) + h) ^ static_cast<uchar>(*s); // djb2 xor variant
+ return h;
+}
+
+/// Hash table slot
+struct StrSlot {
+ uint32_t hash;
+ uint32_t id;
+};
+
+static constexpr uint32_t SLOT_EMPTY = UINT32_MAX;
+static constexpr size_t HTAB_INIT = 1024;
+
+static std::vector<char> char_buf; // Continuous character storage
+static std::vector<uint32_t> id_to_off; // ID -> char_buf offset
+static std::vector<StrSlot> htab; // Open addressing hash table
+static uint32_t htab_used; // Number of occupied slots
+
+/// Initialize hash table (triggered automatically on first intern call)
+static void htab_init() {
+ htab.assign(HTAB_INIT, {SLOT_EMPTY, 0});
+ htab_used = 0;
+}
+
+/// Grow and rehash when load factor exceeds 70%
+static void htab_grow() {
+ size_t new_sz = htab.size() * 2;
+ std::vector<StrSlot> old;
+ old.swap(htab);
+ htab.assign(new_sz, {SLOT_EMPTY, 0});
+
+ uint32_t mask = static_cast<uint32_t>(new_sz - 1);
+ for (auto& s : old) {
+ if (s.hash == SLOT_EMPTY)
+ continue;
+ uint32_t pos = s.hash & mask;
+ while (htab[pos].hash != SLOT_EMPTY)
+ pos = (pos + 1) & mask;
+ htab[pos] = s;
+ }
+}
+
+uint32_t intern(const char* s) {
+ if (htab.empty())
+ htab_init();
+
+ uint32_t h = hash(s);
+ uint32_t mask = static_cast<uint32_t>(htab.size() - 1);
+ uint32_t pos = h & mask;
+
+ // Linear probing search
+ while (htab[pos].hash != SLOT_EMPTY) {
+ if (htab[pos].hash == h) {
+ // hash identical, compare actual string
+ uint32_t off = id_to_off[htab[pos].id];
+ if (strcmp(s, char_buf.data() + off) == 0)
+ return htab[pos].id;
+ }
+ pos = (pos + 1) & mask;
+ }
+
+ // Not found, insert new string
+ uint32_t new_id = static_cast<uint32_t>(id_to_off.size());
+ uint32_t off = static_cast<uint32_t>(char_buf.size());
+ size_t len = strlen(s);
+ char_buf.insert(char_buf.end(), s, s + len + 1); // Includes '\0'
+ id_to_off.push_back(off);
+
+ htab[pos] = {h, new_id};
+ htab_used++;
+
+ // Load factor check
+ if (htab_used * 10 > htab.size() * 7)
+ htab_grow();
+
+ return new_id;
+}
+
+char* str(uint32_t id) {
+ assert(id < id_to_off.size());
+ return char_buf.data() + id_to_off[id];
+}
+
+// strf: Format string and allocate to specified memory pool
+
+char* strf(Ctx* ctx, Pool pl, const char* fmt, ...) {
+ va_list ap, ap2;
+ va_start(ap, fmt);
+ va_copy(ap2, ap);
+ int n = vsnprintf(nullptr, 0, fmt, ap);
+ va_end(ap);
+
+ char* p = static_cast<char*>((pl == PFn) ? alloc(ctx, static_cast<size_t>(n + 1))
+ : emalloc(static_cast<size_t>(n + 1)));
+ vsnprintf(p, static_cast<size_t>(n + 1), fmt, ap2);
+ va_end(ap2);
+ return p;
+}
+
+// die
+
+// die context handover (emit phase has no explicit Ctx*, bypassed via thread_local)
+static thread_local Ctx* tl_die_ctx = nullptr;
+
+void set_die_ctx(Ctx* c) {
+ tl_die_ctx = c;
+}
+
+const char* die_ctx_str(Ctx* ctx, Ins* cur) {
+ static thread_local char buf[2048];
+ char* p = buf;
+ char* end = buf + sizeof(buf);
+ if (!ctx)
+ ctx = tl_die_ctx;
+ if (!ctx || !ctx->fn) {
+ snprintf(buf, sizeof(buf), "no_fn");
+ return buf;
+ }
+ Fn* fn = ctx->fn;
+
+ int fp_par = 0, gp_par = 0;
+ if (fn->start)
+ for (uint j = 0; j < fn->start->nins; ++j) {
+ Ins* ii = &fn->start->ins[j];
+ if (ii->op != Ocopy || !isreg(ii->arg[0]))
+ break;
+ if (KBASE(ii->cls) == 1)
+ ++fp_par;
+ else
+ ++gp_par;
+ }
+ int ncall = 0, max_loop = 0;
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (b->loop > max_loop)
+ max_loop = b->loop;
+ for (uint j = 0; j < b->nins; ++j)
+ if (b->ins[j].op == Ocall)
+ ++ncall;
+ }
+ p += snprintf(p, static_cast<size_t>(end - p),
+ "nblk=%u ntmp=%d ncon=%d slot=%d fpar=%d gpar=%d ncall=%d mxloop=%d fplive=%d",
+ fn->nblk, fn->ntmp, fn->ncon, fn->slot, fp_par, gp_par, ncall, max_loop,
+ ctx->spill_fp_live_peak);
+
+ if (!cur)
+ return buf;
+ Blk* host = nullptr;
+ uint pos = 0;
+ for (Blk* b = fn->start; b; b = b->link) {
+ if (cur >= b->ins && cur < b->ins + b->nins) {
+ host = b;
+ pos = static_cast<uint>(cur - b->ins);
+ break;
+ }
+ }
+ if (!host) {
+ snprintf(p, static_cast<size_t>(end - p), " host=none");
+ return buf;
+ }
+ p += snprintf(p, static_cast<size_t>(end - p), " blk%u/L%d nins=%u idx=%u ", host->id,
+ host->loop, host->nins, pos);
+ uint lo = pos > 10 ? pos - 10 : 0;
+ uint hi = pos + 10 < host->nins ? pos + 10 : host->nins - 1;
+ for (uint j = lo; j <= hi && p < end - 32; ++j) {
+ Ins* ii = &host->ins[j];
+ p += snprintf(p, static_cast<size_t>(end - p), "%s%d/%c,", j == pos ? "*" : "", ii->op,
+ "wlsd"[ii->cls]);
+ }
+ return buf;
+}
+
+[[noreturn]] void die_(const char* file, const char* s, ...) {
+ va_list ap;
+ fprintf(stderr, "%s: dying: ", file);
+ va_start(ap, s);
+ vfprintf(stderr, s, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+ abort();
+}
+
+[[noreturn]] void err(char* s, ...) {
+ va_list ap;
+ fprintf(stderr, "rcc: error: ");
+ va_start(ap, s);
+ vfprintf(stderr, s, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+ abort();
+}
+
+// BSet Bitset Operations
+
+void bsinit(Ctx* ctx, BSet* bs, uint n) {
+ n = (n + NBit - 1) / NBit;
+ bs->nt = n;
+ bs->t = static_cast<bits*>(alloc(ctx, n * sizeof(bits)));
+}
+
+void bszero(BSet* bs) {
+ memset(bs->t, 0, bs->nt * sizeof(bits));
+}
+
+uint bscount(BSet* bs) {
+ uint c = 0;
+ for (uint i = 0; i < bs->nt; i++) {
+ bits b = bs->t[i];
+ // Bitwise count: Hamming Weight (divide and conquer)
+ b = b - ((b >> 1) & 0x5555555555555555ULL);
+ b = (b & 0x3333333333333333ULL) + ((b >> 2) & 0x3333333333333333ULL);
+ b = (b + (b >> 4)) & 0x0f0f0f0f0f0f0f0fULL;
+ c += static_cast<uint>((b * 0x0101010101010101ULL) >> 56);
+ }
+ return c;
+}
+
+void bsset(BSet* bs, uint elt) {
+ assert(elt < bs->nt * NBit);
+ bs->t[elt / NBit] |= BIT(elt % NBit);
+}
+
+void bsclr(BSet* bs, uint elt) {
+ assert(elt < bs->nt * NBit);
+ bs->t[elt / NBit] &= ~BIT(elt % NBit);
+}
+
+void bscopy(BSet* a, BSet* b) {
+ assert(a->nt == b->nt);
+ memcpy(a->t, b->t, a->nt * sizeof(bits));
+}
+
+void bsunion(BSet* a, BSet* b) {
+ assert(a->nt == b->nt);
+ for (uint i = 0; i < a->nt; i++)
+ a->t[i] |= b->t[i];
+}
+
+void bsinter(BSet* a, BSet* b) {
+ assert(a->nt == b->nt);
+ for (uint i = 0; i < a->nt; i++)
+ a->t[i] &= b->t[i];
+}
+
+void bsdiff(BSet* a, BSet* b) {
+ assert(a->nt == b->nt);
+ for (uint i = 0; i < a->nt; i++)
+ a->t[i] &= ~b->t[i];
+}
+
+int bsequal(BSet* a, BSet* b) {
+ assert(a->nt == b->nt);
+ for (uint i = 0; i < a->nt; i++)
+ if (a->t[i] != b->t[i])
+ return 0;
+ return 1;
+}
+
+/// Find index of lowest set bit in 64-bit word. For single bit (pow2) input, equivalent to log2.
+int lowest_bit(bits b) {
+ // De Bruijn sequence method
+ static const int debruijn[64] = {
+ 0, 1, 2, 53, 3, 7, 54, 27, 4, 38, 41, 8, 34, 55, 48, 28, 62, 5, 39, 46, 44, 42,
+ 22, 9, 24, 35, 59, 56, 49, 18, 29, 11, 63, 52, 6, 26, 37, 40, 33, 47, 61, 45, 43, 21,
+ 23, 58, 17, 10, 51, 25, 36, 32, 60, 20, 57, 16, 50, 31, 19, 15, 30, 14, 13, 12,
+ };
+ return debruijn[((b & (~b + 1)) * 0x022fdd63cc95386dULL) >> 58];
+}
+
+int bsiter(BSet* bs, int* elt) {
+ uint i = static_cast<uint>(*elt);
+ uint t = i / NBit;
+ if (t >= bs->nt)
+ return 0;
+
+ bits b = bs->t[t] & ~(BIT(i % NBit) - 1);
+ while (!b) {
+ if (++t >= bs->nt)
+ return 0;
+ b = bs->t[t];
+ }
+ *elt = static_cast<int>(NBit * t + static_cast<uint>(lowest_bit(b)));
+ return 1;
+}
+
+// Instruction Operation Helpers
+
+void addins(Ctx* ctx, Ins** pi, uint* ni, Ins* ins) {
+ if (ins->op == Onop)
+ return;
+ vgrow(ctx, pi, *ni + 1);
+ (*pi)[*ni] = *ins;
+ (*ni)++;
+}
+
+void addbins(Ctx* ctx, Ins** pi, uint* ni, Blk* b) {
+ for (uint j = 0; j < b->nins; j++)
+ addins(ctx, pi, ni, &b->ins[j]);
+}
+
+void emit(Ctx* ctx, int op, int k, Ref to, Ref a0, Ref a1) {
+ if (ctx->curi == ctx->insb)
+ die("emit, too many instructions (NIns=%d) op=%d cls=%d "
+ "to=t%d/v%u a0=t%d/v%u a1=t%d/v%u || %s",
+ NIns, op, k, rtype(to), to.val, rtype(a0), a0.val, rtype(a1), a1.val,
+ die_ctx_str(ctx, nullptr));
+ --ctx->curi;
+ ctx->curi->op = static_cast<uint>(op);
+ ctx->curi->cls = static_cast<uint>(k);
+ ctx->curi->to = to;
+ ctx->curi->arg[0] = a0;
+ ctx->curi->arg[1] = a1;
+}
+
+void emiti(Ctx* ctx, Ins i) {
+ emit(ctx, i.op, i.cls, i.to, i.arg[0], i.arg[1]);
+}
+
+Ins* icpy(Ins* dst, Ins* src, ulong n) {
+ if (n)
+ memmove(dst, src, n * sizeof(Ins));
+ return dst + n;
+}
+
+void idup(Ctx* ctx, Blk* b, Ins* src, ulong n) {
+ vgrow(ctx, &b->ins, n);
+ icpy(b->ins, src, n);
+ b->nins = static_cast<uint>(n);
+}
+
+void igroup(Blk* b, Ins* i, Ins** i0, Ins** i1) {
+ Ins* ib = b->ins;
+ Ins* ie = ib + b->nins;
+
+ switch (i->op) {
+ case Oblit0:
+ *i0 = i;
+ *i1 = i + 2;
+ return;
+ case Oblit1:
+ *i0 = i - 1;
+ *i1 = i + 1;
+ return;
+ case Osel0:
+ case Osel1:
+ // Backtrack to the leading sel0 from anywhere in the group, then consume consecutive
+ // sel1s forward
+ while (i->op == Osel1)
+ --i;
+ assert(i->op == Osel0);
+ *i0 = i;
+ for (++i; i < ie && i->op == Osel1; ++i)
+ ;
+ *i1 = i;
+ return;
+ }
+
+ if (ispar(i->op)) {
+ while (i > ib && ispar((i - 1)->op))
+ --i;
+ *i0 = i;
+ while (i < ie && ispar(i->op))
+ ++i;
+ *i1 = i;
+ } else if (i->op == Ocall || isarg(i->op)) {
+ while (i > ib && isarg((i - 1)->op))
+ --i;
+ *i0 = i;
+ while (i < ie && i->op != Ocall)
+ ++i;
+ assert(i < ie);
+ *i1 = i + 1;
+ } else {
+ *i0 = i;
+ *i1 = i + 1;
+ }
+}
+
+int argcls(Ins* i, int n) {
+ return optab[i->op].argcls[n][i->cls];
+}
+
+// Tmp / Con / Comparison Helpers
+
+Ref newtmp(Ctx* ctx, const char* prfx, int k, Fn* fn) {
+ static int cnt;
+ int t = fn->ntmp++;
+ vgrow(ctx, &fn->tmp, static_cast<ulong>(fn->ntmp));
+ memset(&fn->tmp[t], 0, sizeof(Tmp));
+ if (prfx)
+ fn->tmp[t].name = strf(ctx, PFn, "%s.%d", prfx, ++cnt);
+ fn->tmp[t].cls = static_cast<short>(k);
+ fn->tmp[t].slot = -1;
+ fn->tmp[t].nuse = 1;
+ fn->tmp[t].ndef = 1;
+ return Ref::tmp(static_cast<uint>(t));
+}
+
+void chuse(Ctx*, Ref r, int du, Fn* fn) {
+ if (rtype(r) == RTmp)
+ fn->tmp[r.val].nuse += static_cast<uint>(du);
+}
+
+int symeq(Sym a, Sym b) {
+ return a.type == b.type && a.id == b.id;
+}
+
+Ref newcon(Ctx* ctx, Con* c0, Fn* fn) {
+ for (int i = 1; i < fn->ncon; i++) {
+ Con* c1 = &fn->con[i];
+ if (c0->type == c1->type && symeq(c0->sym, c1->sym) && c0->bits.i == c1->bits.i)
+ return Ref::con(static_cast<uint>(i));
+ }
+ int i = fn->ncon++;
+ vgrow(ctx, &fn->con, static_cast<ulong>(fn->ncon));
+ fn->con[i] = *c0;
+ return Ref::con(static_cast<uint>(i));
+}
+
+Ref getcon(Ctx* ctx, int64_t val, Fn* fn) {
+ for (int i = 1; i < fn->ncon; i++)
+ if (fn->con[i].type == Con::CBits && fn->con[i].bits.i == val)
+ return Ref::con(static_cast<uint>(i));
+ int i = fn->ncon++;
+ vgrow(ctx, &fn->con, static_cast<ulong>(fn->ncon));
+ memset(&fn->con[i], 0, sizeof(Con));
+ fn->con[i].type = Con::CBits;
+ fn->con[i].bits.i = val;
+ return Ref::con(static_cast<uint>(i));
+}
+
+int addcon(Con* c0, Con* c1, int m) {
+ if (m != 1 && c1->type == Con::CAddr)
+ return 0;
+ if (c0->type == Con::CUndef) {
+ *c0 = *c1;
+ c0->bits.i *= m;
+ } else {
+ if (c1->type == Con::CAddr) {
+ if (c0->type == Con::CAddr)
+ return 0;
+ c0->type = Con::CAddr;
+ c0->sym = c1->sym;
+ }
+ c0->bits.i += c1->bits.i * m;
+ }
+ return 1;
+}
+
+int isconbits(Fn* fn, Ref r, int64_t* v) {
+ if (rtype(r) == RCon) {
+ Con* c = &fn->con[r.val];
+ if (c->type == Con::CBits) {
+ *v = c->bits.i;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+int iscmp(int op, int* pk, int* pc) {
+ if (Ocmpw <= op && op <= Ocmpw1) {
+ *pc = op - Ocmpw;
+ *pk = Kw;
+ } else if (Ocmpl <= op && op <= Ocmpl1) {
+ *pc = op - Ocmpl;
+ *pk = Kl;
+ } else if (Ocmps <= op && op <= Ocmps1) {
+ *pc = NCmpI + op - Ocmps;
+ *pk = Ks;
+ } else if (Ocmpd <= op && op <= Ocmpd1) {
+ *pc = NCmpI + op - Ocmpd;
+ *pk = Kd;
+ } else
+ return 0;
+ return 1;
+}
+
+/// Negation / Swap table for comparison operations
+static const int cmptab[][2] = {
+ /* negate swap */
+ /* Cieq */ {Cine, Cieq},
+ /* Cine */ {Cieq, Cine},
+ /* Cisge */ {Cislt, Cisle},
+ /* Cisgt */ {Cisle, Cislt},
+ /* Cisle */ {Cisgt, Cisge},
+ /* Cislt */ {Cisge, Cisgt},
+ /* Ciuge */ {Ciult, Ciule},
+ /* Ciugt */ {Ciule, Ciult},
+ /* Ciule */ {Ciugt, Ciuge},
+ /* Ciult */ {Ciuge, Ciugt},
+};
+
+static const int fcmptab[][2] = {
+ /* negate swap */
+ /* Cfeq */ {-1, Cfeq},
+ /* Cfge */ {-1, Cfle},
+ /* Cfgt */ {-1, Cflt},
+ /* Cfle */ {-1, Cfge},
+ /* Cflt */ {-1, Cfgt},
+ /* Cfne */ {-1, Cfne},
+ /* Cfo */ {-1, Cfo},
+ /* Cfuo */ {-1, Cfuo},
+};
+
+int cmpop(int c) {
+ assert(0 <= c && c < NCmp);
+ if (c < NCmpI)
+ return cmptab[c][1];
+ return NCmpI + fcmptab[c - NCmpI][1];
+}
+
+int cmpwlneg(int op) {
+ if (INRANGE(op, Ocmpw, Ocmpw1))
+ return cmptab[op - Ocmpw][0] + Ocmpw;
+ if (INRANGE(op, Ocmpl, Ocmpl1))
+ return cmptab[op - Ocmpl][0] + Ocmpl;
+ die("not a wl comparison");
+}
+
+int clsmerge(short* pk, short k) {
+ short k1 = *pk;
+ if (k1 == Kx) {
+ *pk = k;
+ return 0;
+ }
+ if ((k1 == Kw && k == Kl) || (k1 == Kl && k == Kw)) {
+ *pk = Kw;
+ return 0;
+ }
+ return k1 != k;
+}
+
+int phicls(int t, Tmp* tmp) {
+ int t1 = tmp[t].phi;
+ if (!t1)
+ return t;
+ t1 = phicls(t1, tmp);
+ tmp[t].phi = t1;
+ return t1;
+}
+
+uint phiargn(Phi* p, Blk* b) {
+ if (p)
+ for (uint n = 0; n < p->narg; n++)
+ if (p->blk[n] == b)
+ return n;
+ return ~0u;
+}
+
+Ref phiarg(Phi* p, Blk* b) {
+ uint n = phiargn(p, b);
+ assert(n != ~0u && "block not found");
+ return p->arg[n];
+}
+
+void dumpts(BSet* bs, Tmp* tmp, FILE* f) {
+ int t;
+ fprintf(f, "[");
+ for (t = Tmp0; bsiter(bs, &t); t++)
+ fprintf(f, " %s", tmp[t].name);
+ fprintf(f, " ]\n");
+}
+
+} // namespace rcc