/* 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