aboutsummaryrefslogtreecommitdiff
path: root/src/mid
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.001@gmail.com>2026-09-15 08:16:43 +0800
committerRichard Qin <richard.qin.001@gmail.com>2026-09-15 08:16:43 +0800
commit4022e5c95210631e4aceb08eaf9d9071aa5aefc4 (patch)
treea5922559836e45fb3459a3d070367912e1035b1a /src/mid
downloadrcc-4022e5c95210631e4aceb08eaf9d9071aa5aefc4.tar.gz
rcc-4022e5c95210631e4aceb08eaf9d9071aa5aefc4.tar.xz
Set up a git repoHEADmaster
Diffstat (limited to 'src/mid')
-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
30 files changed, 11935 insertions, 0 deletions
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