From 4022e5c95210631e4aceb08eaf9d9071aa5aefc4 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Tue, 15 Sep 2026 08:16:43 +0800 Subject: Set up a git repo --- src/mid/abi.cc | 45 ++ src/mid/alias.cc | 445 +++++++++++++++++++ src/mid/cfg.cc | 582 +++++++++++++++++++++++++ src/mid/copy.cc | 360 ++++++++++++++++ src/mid/dce.cc | 149 +++++++ src/mid/dead_loop.cc | 227 ++++++++++ src/mid/divopt.cc | 189 ++++++++ src/mid/fold.cc | 423 ++++++++++++++++++ src/mid/gcm.cc | 373 ++++++++++++++++ src/mid/gvn.cc | 414 ++++++++++++++++++ src/mid/ifopt.cc | 127 ++++++ src/mid/inline.cc | 484 +++++++++++++++++++++ src/mid/licm.cc | 213 +++++++++ src/mid/live.cc | 198 +++++++++ src/mid/load.cc | 556 ++++++++++++++++++++++++ src/mid/loop_idiom.cc | 401 +++++++++++++++++ src/mid/loopinfo.cc | 226 ++++++++++ src/mid/loopswap.cc | 469 ++++++++++++++++++++ src/mid/lsr.cc | 1001 +++++++++++++++++++++++++++++++++++++++++++ src/mid/mem.cc | 653 ++++++++++++++++++++++++++++ src/mid/mulopt.cc | 208 +++++++++ src/mid/reassoc.cc | 436 +++++++++++++++++++ src/mid/reduction_unroll.cc | 528 +++++++++++++++++++++++ src/mid/scalar_repl.cc | 528 +++++++++++++++++++++++ src/mid/sccp.cc | 449 +++++++++++++++++++ src/mid/ssa.cc | 512 ++++++++++++++++++++++ src/mid/storemrg.cc | 231 ++++++++++ src/mid/tre.cc | 214 +++++++++ src/mid/unroll.cc | 847 ++++++++++++++++++++++++++++++++++++ src/mid/unswitch.cc | 447 +++++++++++++++++++ 30 files changed, 11935 insertions(+) create mode 100644 src/mid/abi.cc create mode 100644 src/mid/alias.cc create mode 100644 src/mid/cfg.cc create mode 100644 src/mid/copy.cc create mode 100644 src/mid/dce.cc create mode 100644 src/mid/dead_loop.cc create mode 100644 src/mid/divopt.cc create mode 100644 src/mid/fold.cc create mode 100644 src/mid/gcm.cc create mode 100644 src/mid/gvn.cc create mode 100644 src/mid/ifopt.cc create mode 100644 src/mid/inline.cc create mode 100644 src/mid/licm.cc create mode 100644 src/mid/live.cc create mode 100644 src/mid/load.cc create mode 100644 src/mid/loop_idiom.cc create mode 100644 src/mid/loopinfo.cc create mode 100644 src/mid/loopswap.cc create mode 100644 src/mid/lsr.cc create mode 100644 src/mid/mem.cc create mode 100644 src/mid/mulopt.cc create mode 100644 src/mid/reassoc.cc create mode 100644 src/mid/reduction_unroll.cc create mode 100644 src/mid/scalar_repl.cc create mode 100644 src/mid/sccp.cc create mode 100644 src/mid/ssa.cc create mode 100644 src/mid/storemrg.cc create mode 100644 src/mid/tre.cc create mode 100644 src/mid/unroll.cc create mode 100644 src/mid/unswitch.cc (limited to 'src/mid') 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 +. */ + +/// @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 +. */ + +/// @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 + +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(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(NBit) || off < 0 || off >= static_cast(NBit)) + m = static_cast(-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(NBit)) + a->u.loc.sz = static_cast(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 +. */ + +/// @file cfg.cc +/// @brief CFG structure analysis and simplification passes. +/// +/// See docs/cfg.md for architectural notes. + +#include "rcc/all.h" + +#include + +namespace rcc { + +// Block construction + +Blk* newblk(Ctx* ctx) { + auto* b = static_cast(alloc(ctx, sizeof(Blk))); + memset(b, 0, sizeof *b); + b->ins = static_cast(vnew(ctx, 0, sizeof(Ins), PFn)); + b->pred = static_cast(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(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(phi->cls); + copy.to = phi->to; + copy.arg[0] = phiarg(phi, p); + copy.arg[1] = Ref::none(); + addins(ctx, &p->ins, &p->nins, ©); + } + 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(emalloc(fn->nblk * sizeof(JmpSnap))); + auto* empty = static_cast(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 +. */ + +/// @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 + +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 +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(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(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(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(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(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(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(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 +. */ + +/// @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 + +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_def(static_cast(ntmp), nullptr); + std::vector ins_def(static_cast(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(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(ntmp)) + ins_def[i->to.val] = i; + } + } + + std::vector alive(static_cast(ntmp), 0); + std::vector wl; + auto mark = [&](Ref r) { + if (rtype(r) == RTmp && r.val < static_cast(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(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(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 +. */ + +/// @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 +#include + +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(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& bivs, Ref r) { + for (size_t k = 0; k < bivs.size(); k++) + if (req(bivs[k].to, r)) + return static_cast(k); + return -1; +} + +void collect_body_defs(const Loop& L, std::vector& 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(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 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 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 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 +. */ + +/// @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 + +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(M_u); + m.s = static_cast(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(d_abs)); + int64_t M64 = static_cast(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(c->bits.i); + if (d == 0 || d == static_cast(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((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(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(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(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 +. */ + +/// @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 +#include + +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(c->bits.i); + return static_cast(v) == (w ? k : static_cast(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(static_cast(l.u)); + r.u = static_cast(static_cast(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 +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("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(static_cast(cl->bits.i)); + break; + case Ouwtof: + xd = static_cast(static_cast(cl->bits.i)); + break; + case Osltof: + xd = static_cast(static_cast(cl->bits.i)); + break; + case Oultof: + xd = static_cast(static_cast(cl->bits.i)); + break; + case Oexts: + xd = static_cast(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(static_cast(cl->bits.i)); + break; + case Ouwtof: + xs = static_cast(static_cast(cl->bits.i)); + break; + case Osltof: + xs = static_cast(static_cast(cl->bits.i)); + break; + case Oultof: + xs = static_cast(static_cast(cl->bits.i)); + break; + case Otruncd: + xs = static_cast(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(INT64_MIN) + : static_cast(static_cast(INT32_MIN)); + if (iscon(cr, w, static_cast(-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(op - Ocmps, cl->bits.s, cr->bits.s); + } else if (op >= Ocmpd && op <= Ocmpd1) { + x = foldcmp_flt(op - Ocmpd, cl->bits.d, cr->bits.d); + } else { + // Shift count mask. + const uint64_t shmask = static_cast(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(l.s / r.s) + : static_cast(static_cast(l.s) / + static_cast(r.s)); + break; + case Orem: + x = w ? static_cast(l.s % r.s) + : static_cast(static_cast(l.s) % + static_cast(r.s)); + break; + case Oudiv: + x = w ? l.u / r.u : static_cast(l.u) / static_cast(r.u); + break; + case Ourem: + x = w ? l.u % r.u : static_cast(l.u) % static_cast(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((w ? l.s : static_cast(l.s)) >> (r.u & shmask)); + break; + case Oshr: + x = (w ? l.u : static_cast(l.u)) >> (r.u & shmask); + break; + case Oshl: + x = l.u << (r.u & shmask); + break; + + case Oextsb: + x = static_cast(static_cast(l.u)); + break; + case Oextub: + x = static_cast(l.u); + break; + case Oextsh: + x = static_cast(static_cast(l.u)); + break; + case Oextuh: + x = static_cast(l.u); + break; + case Oextsw: + x = static_cast(static_cast(l.u)); + break; + case Oextuw: + x = static_cast(l.u); + break; + + case Ostosi: + x = w ? static_cast(static_cast(cl->bits.s)) + : static_cast(static_cast(cl->bits.s)); + break; + case Ostoui: + x = w ? static_cast(cl->bits.s) + : static_cast(static_cast(cl->bits.s)); + break; + case Odtosi: + x = w ? static_cast(static_cast(cl->bits.d)) + : static_cast(static_cast(cl->bits.d)); + break; + case Odtoui: + x = w ? static_cast(cl->bits.d) + : static_cast(static_cast(cl->bits.d)); + break; + case Ocast: + x = l.u; + break; + default: + die("foldint: unsupported op %d", op); + } + } + + // Write back. + res->type = static_casttype)>(ctyp); + res->sym = sym; + std::memset(&res->bits, 0, sizeof res->bits); + res->bits.i = static_cast(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 +. */ + +/// @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(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(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(&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 +. */ + +/// @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(a.val) - static_cast(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(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(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(c.bits.i) < 0))) { + int fail = negcon(i1->cls, &c); + assert(fail == 0); + (void)fail; + op = Osub; + } + + i1->op = static_cast(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(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(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 +. */ + +/// @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(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(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(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 +. */ + +/// @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 +#include +#include +#include +#include +#include +#include + +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& 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 collect_call_counts(const std::vector& fns) { + std::unordered_map 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> build_call_graph(const std::vector& fns) { + std::unordered_map> 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 find_cycle_members( + const std::vector& fns, const std::unordered_map>& g) { + std::unordered_set cyclic; + std::unordered_map idx_of, lowlink; + std::unordered_set on_stack; + std::vector stk; + int counter = 0; + + std::function 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 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 tmp; // callee tmp id → caller Ref (RTmp) + std::vector con; // callee con id → caller Ref (RCon) + std::vector 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& par_args, Blk* b_post, std::vector& rets) { + // phi + Phi** ptail = &dst->phi; + *ptail = nullptr; + for (Phi* sp = src->phi; sp; sp = sp->link) { + Phi* np = static_cast(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(vnew(ctx, sp->narg, sizeof(Ref), PFn)); + np->blk = static_cast(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(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(par_args.size())); + ni.op = Ocopy; + ni.cls = si->cls; + ni.to = remap_ref(si->to, m); + ni.arg[0] = par_args[static_cast(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(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 arg_refs; + arg_refs.reserve(static_cast(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(callee->ntmp), Ref::none()); + m.con.assign(static_cast(callee->ncon), Ref::none()); + m.blk.assign(static_cast(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(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(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 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(b->nins) - call_idx - 1; + if (tail_n > 0) { + for (int k = call_idx + 1; k < static_cast(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(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 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(alloc(ctx, sizeof(Phi))); + np->to = call_to; + np->cls = static_cast(call_ins->cls); + np->visit = 0; + np->narg = static_cast(rets.size()); + np->arg = static_cast(vnew(ctx, np->narg, sizeof(Ref), PFn)); + np->blk = static_cast(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& fns, const std::unordered_set& 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(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& 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 +. */ + +/// @file licm.cc +/// @brief Phase A Loop Invariant Code Motion (LICM). +/// +/// See docs/licm.md for architectural notes. + +#include "rcc/all.h" + +#include + +namespace rcc { + +namespace { + +struct LoopInfo { + Blk* header; + std::vector 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& loops, std::vector& blk_loop) { + loops.clear(); + blk_loop.assign(static_cast(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(cur)].header->id < + loops[static_cast(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(loops.size()); + loops.push_back({hd, {hd}, nullptr}); + refine_inner(hd->id, lidx); + + // DFS body collect. + std::vector 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(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& 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 loops; + std::vector 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(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 +. */ + +/// @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(fn->ntmp)); + bsinit(ctx, edge_set, static_cast(fn->ntmp)); + + // Initialize sets. + for (Blk* b = fn->start; b; b = b->link) { + bsinit(ctx, b->in, static_cast(fn->ntmp)); + bsinit(ctx, b->out, static_cast(fn->ntmp)); + bsinit(ctx, b->gen, static_cast(fn->ntmp)); + } + + // Fixpoint loop. + bool changed = true; + while (changed) { + changed = false; + + // Reverse rpo scan. + for (int n = static_cast(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(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(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 +. */ + +/// @file load.cc +/// @brief Load/store meta information + load-forwarding optimization (loadopt). +/// +/// See docs/load.md for architectural notes. + +#include "rcc/all.h" + +#include + +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(op); + ist->ins.cls = static_cast(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(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(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(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(a->base)); + sl->off = static_cast(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(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(pa); + const Insert* b = static_cast(pb); + int c; + if ((c = static_cast(a->bid) - static_cast(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(a->off) - static_cast(b->off))) + return c; + return static_cast(a->num) - static_cast(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(sl1.sz + off); + } + if (sz > sl1.sz) + sz = sl1.sz; + assert(sz <= 8); + sl1.sz = static_cast(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(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(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(sl.cls); + p->narg = b->npred; + p->arg = static_cast(vnew(ctx, p->narg, sizeof(Ref), PFn)); + p->blk = static_cast(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(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(sz); + sl.cls = static_cast(i->cls); + Loc l; + l.type = Loc::LRoot; + l.off = static_cast(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(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(i->op) - Oloadsb); + switch (i->op) { + case Oloadsb: + case Oloadub: + case Oloadsh: + case Oloaduh: + i->op = static_cast(ext); + break; + case Oloadsw: + case Oloaduw: + if (i->cls == Kl) { + i->op = static_cast(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 +. */ + +/// @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 +#include +#include +#include + +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(c->bits.i); + uint8_t b = bits & 0xff; + uint64_t splat = static_cast(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(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> 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(k) == iv_dst_inc || static_cast(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(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(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(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(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 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 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 +. */ +/// @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(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(fn->ntmp)) + return nullptr; + return fn->tmp[r.val].def; +} + +void find_loops(Fn* fn, std::vector& 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 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& 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(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 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 +. */ + +/// @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 +#include +#include + +namespace rcc { + +namespace { + +// Helper: reaches +bool reaches(Fn* fn, Ref r, Ref target, std::unordered_set& visiting, int budget) { + if (req(r, target)) + return true; + if (budget <= 0) + return false; + if (rtype(r) != RTmp || r.val >= static_cast(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 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(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(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(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 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 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 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& 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 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 +. */ + +/// @file lsr.cc +/// @brief Loop Strength Reduction (LSR). +/// +/// See docs/lsr.md for architectural notes. + +#include "rcc/all.h" +#include "rcc/loopinfo.h" + +#include +#include +#include +#include +#include +#include + +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(static_cast(rtype(r))) << 32) | + static_cast(r.val); +} + +int find_biv(const std::vector& bivs, Ref r) { + for (size_t k = 0; k < bivs.size(); k++) + if (req(bivs[k].to, r)) + return static_cast(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& bivs, + std::vector& created_ivs) { + std::vector 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(m->cls) == bivs[static_cast(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(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; + + 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(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(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(alloc(ctx, sizeof(Phi))); + std::memset(p, 0, sizeof(Phi)); + p->to = jt; + p->cls = static_cast(cls); + p->narg = 2; + p->arg = static_cast(vnew(ctx, 2, sizeof(Ref), PFn)); + p->blk = static_cast(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(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& bivs, + std::vector& created_ivs) { + // Invariance check (memoized) + std::unordered_map inv_memo; // -1 uncomputed, 0 false, 1 true + std::function 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> scev_memo; + std::function(Ref)> scev = [&](Ref r) -> std::optional { + 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 ans = std::nullopt; + + auto combine = [](Scev a, Scev b, int sign) -> std::optional { + 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(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 mat_memo; + std::function 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(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(d->cls); + Ref nt = newtmp(ctx, "lsr", cls, fn); + Ins nins{}; + nins.op = d->op; + nins.cls = static_cast(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 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 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(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(alloc(ctx, sizeof(Phi))); + std::memset(ph, 0, sizeof(Phi)); + ph->to = pt; + ph->cls = static_cast(cls); + ph->narg = 2; + ph->arg = static_cast(vnew(ctx, 2, sizeof(Ref), PFn)); + ph->blk = static_cast(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(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& bivs, + const std::vector& 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(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(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(fn->ntmp)) + self_inc = fn->tmp[a.val].def; + break; + } + } + } + if (biv_to.val >= static_cast(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(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 inv_memo; + std::function 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 mat_memo; + std::function 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(d->cls); + Ref nt = newtmp(ctx, "lsr.lftr", cls, fn); + Ins ni{}; + ni.op = d->op; + ni.cls = static_cast(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(i); + best_abs = a; + } + } + if (best < 0) + return 0; + + const CreatedIV& iv = created_ivs[static_cast(best)]; + int64_t i0 = bivs[static_cast(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(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(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(op) & 0x3fffffffu; + ni.cls = static_cast(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(final_op) & 0x3fffffffu; + cmp->cls = static_cast(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 bivs; + detect_bivs(fn, L, bivs); + if (bivs.empty()) + return false; + + std::vector 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(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 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 +. */ + +/// @file mem.cc +/// @brief Memory optimizations: `promote` and `coalesce`. +/// +/// See docs/mem.md for architectural notes. + +#include "rcc/all.h" + +#include +#include +#include + +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(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(l->cls))) + l->op = Ocast; + else + l->op = Ocopy; + break; + default: + extend = true; + break; + } + if (extend) + l->op = static_cast(Oextsb + (static_cast(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(++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(pa); + const Slot* b = static_cast(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(b->id)) + hd->loop = static_cast(b->id); +} + +struct SlotList { + Slot* sl; + int nsl; +}; + +SlotList collect_slots(Ctx* ctx, Fn* fn) { + SlotList sls; + sls.nsl = 0; + sls.sl = static_cast(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(++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(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(vnew(ctx, 0, sizeof(Ins*), PHeap)); + lo.br = static_cast(emalloc(static_cast(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(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((*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(-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(-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(NBit) ? static_cast(-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(++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(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(s->sz); + if (!s->r.b) { + vfree(s->st); + vgrow(ctx, &stk, static_cast(++n)); + stk[n - 1] = s->t; + freed += static_cast(s->sz); + } else { + *s0++ = *s; + } + } + int new_nsl = static_cast(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(++n)); + stk[n - 1] = static_cast(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(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(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(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(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(s->t)))) + arg[n] = Ref::tmp(static_cast(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 +. */ + +/// @file mulopt.cc +/// @brief Constant integer multiplication strength reduction. +/// +/// See docs/mulopt.md for architectural notes. + +#include "rcc/all.h" + +#include + +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(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( + 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(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((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(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 +. */ + +/// @file reassoc.cc +/// @brief Reassociation pass (LLVM rank-based style). +/// +/// See docs/reassoc.md for architectural notes. + +#include "rcc/all.h" + +#include +#include +#include +#include +#include +#include +#include + +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 absorbed; + std::unordered_map> + 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(d->cls) == cls; + } + + int64_t mask_val(int cls, int64_t v) { + return (cls == Kw) ? static_cast(static_cast(v)) : v; + } + + // Flatten nodes. + void collect(Ins* root, Family fam, int cls, std::vector& out) { + std::vector 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(UINT32_MAX) << 32) | r.val; + return (static_cast(r.val) << 32) | r.val; + } + + void sort_terms(std::vector& 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& refs, int bop, int cls, std::vector& 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(bop); + ni.cls = static_cast(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(op); + root->arg[0] = a0; + root->arg[1] = a1; + } + + // Add/Sub Reconstruction + void rebuild_addsub(Ins* root, int cls, std::vector& leaves) { + // Constant folding. + int64_t cacc = 0; + std::vector 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 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 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& pre = prelude[root]; + if (neg.empty()) { + if (pos.size() == 1) { + make_copy(root, pos[0]); + } else { + Ref acc = build_full(std::vector(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(len) > n_chainmax) + n_chainmax = static_cast(len); + } + + // Mul/And/Or/Xor Reconstruction + void rebuild_logic(Ins* root, Family fam, int cls, std::vector& 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 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 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& pre = prelude[root]; + Ref acc_ref = build_full(std::vector(refs.begin(), refs.end() - 1), bop, cls, pre); + make_bin(root, bop, acc_ref, refs.back()); + } + if (static_cast(refs.size()) > n_chainmax) + n_chainmax = static_cast(refs.size()); + } + + void process_root(Ins* root) { + Family fam = family_of(root->op); + int cls = static_cast(root->cls); + size_t before = absorbed.size(); + std::vector 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 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(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(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 +. */ + +/// @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 +#include +#include +#include + +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(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(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(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(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 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(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(alloc(ctx, sizeof(Phi))); + p->cls = static_cast(cls); + p->visit = 0; + p->to = to; + p->narg = 2; + p->arg = static_cast(vnew(ctx, 2, sizeof(Ref), PFn)); + p->blk = static_cast(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(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(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 s_refs(static_cast(K)), s_next_refs(static_cast(K)); + for (size_t i = 0; i < static_cast(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(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 v_refs(static_cast(K)); + for (size_t i = 0; i < static_cast(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(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(load_op); + li.cls = static_cast(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(K); i++) { + Ins oi{}; + oi.op = static_cast(op); + oi.cls = static_cast(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(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(op); + j1.cls = static_cast(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(op); + j2.cls = static_cast(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(op); + j3.cls = static_cast(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 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 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 +. */ + +/// @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 +#include + +namespace rcc { + +namespace { + +Ins make_nop_ins() { + Ins i = {}; + i.op = Onop; + return i; +} + +struct LoopInfo { + Blk* header; + std::vector 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& body) { + for (Blk* b : body) + if (b == x) + return true; + return false; +} + +bool find_loops(Fn* fn, std::vector& 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 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(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(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>& 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(r.val), v)); + return v; + } + return Ref::none(); + } + Ins* d = t->def; + if (!d || !is_pure_arith(static_cast(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(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(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 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(iload->cls); + + // Clone address chain to preheader, then issue load. + std::vector> 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(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(alloc(ctx, sizeof(Phi))); + std::memset(p, 0, sizeof *p); + p->to = phi_ref; + p->cls = static_cast(cls); + p->narg = 2; + p->arg = static_cast(vnew(ctx, 2, sizeof(Ref), PFn)); + p->blk = static_cast(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 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 +. */ + +/// @file sccp.cc +/// @brief Sparse Conditional Constant Propagation (Wegman-Zadeck). +/// +/// See docs/sccp.md for architectural notes. + +#include "rcc/all.h" + +#include +#include +#include +#include +#include + +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(fn->ncon) || b.val >= static_cast(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; // ntmp + std::vector blk_reachable; // nblk + // edge_exec pairs. + std::set> edge_exec; + std::vector cfg_wl; + std::vector 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(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(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(fn->ntmp), kTop); + blk_reachable.assign(static_cast(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(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 +. */ + +/// @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(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(w); + tmp[td].def = i; + tmp[td].bid = b->id; + tmp[td].ndef++; + tmp[td].cls = static_cast(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(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(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(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(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(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(t))) + continue; // t not live at a entry, no phi needed + + Phi* p = static_cast(alloc(ctx, sizeof(Phi))); + memset(p, 0, sizeof(Phi)); + p->cls = k; + p->to = Ref::tmp(static_cast(t)); + p->link = a->phi; + p->arg = static_cast(vnew(ctx, 0, sizeof(Ref), PFn)); + p->blk = static_cast(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(emalloc(static_cast(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(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 +. */ + +/// @file storemrg.cc +/// @brief Fuses adjacent 4B stores into 8B storel. +/// +/// See docs/storemrg.md for architectural notes. + +#include "rcc/all.h" + +#include + +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(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& 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 compute_aligned8(Fn* fn) { + std::vector a8(static_cast(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 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; + 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(val32) | + (static_cast(val32) << 32); + b->ins[lo_idx].arg[0] = + getcon(ctx, static_cast(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 +. */ + +/// @file tre.cc +/// @brief Tail Recursion Elimination (TRE) + +#include "rcc/all.h" + +#include +#include + +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 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(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(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 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(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 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(npar); + std::vector par_init_refs(upar); + std::vector par_orig_refs(upar); + std::vector par_clses(upar); + for (int i = 0; i < npar; i++) { + size_t ui = static_cast(i); + par_orig_refs[ui] = par_inss[ui]->to; + par_clses[ui] = static_cast(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(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(loop_header->nins) - npar; + if (remaining > 0) + std::memmove(loop_header->ins, loop_header->ins + npar, + static_cast(remaining) * sizeof(Ins)); + loop_header->nins = static_cast(remaining); + + // Insert npar phis at loop_header head + int nphi_args = 1 + static_cast(sites.size()); + for (int i = npar - 1; i >= 0; i--) { + size_t ui = static_cast(i); + auto* p = static_cast(alloc(ctx, sizeof(Phi))); + std::memset(p, 0, sizeof(Phi)); + p->to = par_orig_refs[ui]; + p->cls = static_cast(par_clses[ui]); + p->narg = static_cast(nphi_args); + p->arg = static_cast(vnew(ctx, static_cast(nphi_args), sizeof(Ref), PFn)); + p->blk = static_cast(vnew(ctx, static_cast(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(s.arg_first - s.b->ins); + s.b->nins = static_cast(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 +. */ + +/// @file unroll.cc +/// @brief Loop Unrolling. +/// +/// See docs/unroll.md for architectural notes. + +#include "rcc/all.h" + +#include +#include +#include +#include +#include +#include + +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 body; // includes header, latch +}; + +// Helper: find_simple_loops (see docs/unroll.md) +std::vector find_simple_loops(Fn* fn) { + std::vector 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 body = {hd}; + std::vector 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 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 blk_map; // body blk.id -> clone + std::unordered_map 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& 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(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(vnew(ctx, sp->narg, sizeof(Ref), PFn)); + np->blk = static_cast(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(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 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 header_phis; + for (Phi* p = L.header->phi; p; p = p->link) + header_phis.push_back(p); + + // Clone for each iteration + std::vector copies(static_cast(N)); + + // Map previous latch tmps + std::unordered_map 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(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(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(k)].last_blk; + Blk* nh = copies[static_cast(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(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(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(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(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(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 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 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 head_to_main; // original phi.to.val -> main_head phi.to + std::vector main_phis; + Phi** ptail = &main_head->phi; + for (Phi* p : header_phis) { + Phi* pm = static_cast(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(vnew(ctx, 2, sizeof(Ref), PFn)); + pm->blk = static_cast(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 copies(static_cast(F)); + std::unordered_map prev_latch_tmp_map; + for (int k = 0; k < F; k++) { + CopyState& st = copies[static_cast(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(k)].last_blk; + lc->jmp.type = Jjmp; + lc->jmp.arg = Ref::none(); + lc->s1 = copies[static_cast(k + 1)].first_blk; + lc->s2 = nullptr; + } + // Last latch -> main_head (back-edge) + Blk* last_latch = copies[static_cast(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(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(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(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(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(ind.cmp_op); + cmpi.cls = static_cast(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(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& 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(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 + 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 +. */ + +/// @file unswitch.cc +/// @brief Loop Unswitching. +/// +/// See docs/unswitch.md for architectural notes. + +#include "rcc/all.h" + +#include +#include +#include +#include +#include + +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 body; // includes header / latch +}; + +// Find simple loops (derived from unroll.cc). +std::vector find_simple_loops(Fn* fn) { + std::vector 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 body = {hd}; + std::vector 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(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 blk_map; // body blk.id -> clone + std::unordered_map tmp_map; // body-defined tmp.val -> clone Ref +}; + +Ref remap_tmp(Ref r, const std::unordered_map& 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(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(vnew(ctx, sp->narg, sizeof(Ref), PFn)); + np->blk = static_cast(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(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 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(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(vnew(ctx, new_narg, sizeof(Ref), PFn)); + Blk** new_blk = static_cast(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 -- cgit v1.2.3