diff options
| author | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-06-08 17:06:46 +0800 |
|---|---|---|
| committer | Richard-Qin-X <richard.qin.001@gmail.com> | 2026-06-08 17:06:46 +0800 |
| commit | f4126232d73bbcc1b9f8504a349f9a9913237616 (patch) | |
| tree | 1c4dbc7acbcc8fbcf3fe8ebd1573eb474ec43563 /rcc-legacy/src/mid/inline.cc | |
| parent | 2b1f6c1e2cc22eef6d1a7cdc1c852fddf534fcc7 (diff) | |
| download | rcc-project-f4126232d73bbcc1b9f8504a349f9a9913237616.tar.gz rcc-project-f4126232d73bbcc1b9f8504a349f9a9913237616.tar.xz | |
[legacy/mid] Add interprocedural passes: inlining and tail-recursion elimination
Diffstat (limited to 'rcc-legacy/src/mid/inline.cc')
| -rw-r--r-- | rcc-legacy/src/mid/inline.cc | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/rcc-legacy/src/mid/inline.cc b/rcc-legacy/src/mid/inline.cc new file mode 100644 index 0000000..60e2dc1 --- /dev/null +++ b/rcc-legacy/src/mid/inline.cc @@ -0,0 +1,484 @@ +/* Source code for RCC-Legacy. + Copyright (C) 2026 Richard Qin + +This file is part of RCC. + +RCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +RCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with RCC; see the file LICENSE. If not see +<http://www.gnu.org/licenses/>. */ + +/// @file inline.cc +/// @brief Function inlining (with SCC check to prevent recursion + fixpoint iteration). +/// +/// See docs/inline.md for architectural notes. + +#include "rcc/all.h" + +#include <algorithm> +#include <cstring> +#include <functional> +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +constexpr int kInlineLeafThreshold = 64; +constexpr int kInlineSingleCallerThreshold = 256; +constexpr int kInlineNonLeafThreshold = 48; +constexpr int kFixpointMaxIters = 16; +constexpr int kConstArgDiscount = 8; + +// Metrics + +bool is_leaf(Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (i->op == Ocall) + return false; + return true; +} + +int count_real_ins(Fn* fn) { + int n = 0; + for (Blk* b = fn->start; b; b = b->link) + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (i->op != Onop) + n++; + // phi also counts towards cost, one phi equals one ins + for (Blk* b = fn->start; b; b = b->link) + for (Phi* p = b->phi; p; p = p->link) + n++; + return n; +} + +Fn* lookup_callee(Ref fn_ref, Fn* caller, const std::vector<Fn*>& fns) { + if (rtype(fn_ref) != RCon) + return nullptr; + Con* c = &caller->con[fn_ref.val]; + if (c->type != Con::CAddr) + return nullptr; + const char* name = str(c->sym.id); + for (Fn* f : fns) + if (std::strcmp(f->name, name) == 0) + return f; + return nullptr; +} + +std::unordered_map<std::string, int> collect_call_counts(const std::vector<Fn*>& fns) { + std::unordered_map<std::string, int> cnt; + for (Fn* caller : fns) { + for (Blk* b = caller->start; b; b = b->link) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (i->op != Ocall) + continue; + if (rtype(i->arg[0]) != RCon) + continue; + Con* c = &caller->con[i->arg[0].val]; + if (c->type != Con::CAddr) + continue; + cnt[str(c->sym.id)]++; + } + } + } + return cnt; +} + +// Call Graph SCC + +std::unordered_map<Fn*, std::unordered_set<Fn*>> build_call_graph(const std::vector<Fn*>& fns) { + std::unordered_map<Fn*, std::unordered_set<Fn*>> g; + for (Fn* caller : fns) { + g[caller]; // Ensure node exists (even without outgoing edges) + for (Blk* b = caller->start; b; b = b->link) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (i->op != Ocall) + continue; + Fn* callee = lookup_callee(i->arg[0], caller, fns); + if (callee) + g[caller].insert(callee); + } + } + } + return g; +} + +std::unordered_set<Fn*> find_cycle_members( + const std::vector<Fn*>& fns, const std::unordered_map<Fn*, std::unordered_set<Fn*>>& g) { + std::unordered_set<Fn*> cyclic; + std::unordered_map<Fn*, int> idx_of, lowlink; + std::unordered_set<Fn*> on_stack; + std::vector<Fn*> stk; + int counter = 0; + + std::function<void(Fn*)> strongconnect = [&](Fn* v) { + idx_of[v] = counter; + lowlink[v] = counter; + counter++; + stk.push_back(v); + on_stack.insert(v); + + auto it = g.find(v); + if (it != g.end()) { + for (Fn* w : it->second) { + if (idx_of.find(w) == idx_of.end()) { + strongconnect(w); + lowlink[v] = std::min(lowlink[v], lowlink[w]); + } else if (on_stack.count(w)) { + lowlink[v] = std::min(lowlink[v], idx_of[w]); + } + } + } + + if (lowlink[v] == idx_of[v]) { + std::vector<Fn*> scc; + Fn* w; + do { + w = stk.back(); + stk.pop_back(); + on_stack.erase(w); + scc.push_back(w); + } while (w != v); + + if (scc.size() >= 2) { + for (Fn* f : scc) + cyclic.insert(f); + } else { + // Single-node SCC: only count if it has self-loop (direct recursion) + Fn* f = scc[0]; + auto it2 = g.find(f); + if (it2 != g.end() && it2->second.count(f)) + cyclic.insert(f); + } + } + }; + + for (Fn* f : fns) { + if (idx_of.find(f) == idx_of.end()) + strongconnect(f); + } + return cyclic; +} + +// Splice + +struct RemapTables { + std::vector<Ref> tmp; // callee tmp id → caller Ref (RTmp) + std::vector<Ref> con; // callee con id → caller Ref (RCon) + std::vector<Blk*> blk; // callee blk id → cloned Blk* in caller +}; + +Ref remap_ref(Ref r, const RemapTables& m) { + switch (rtype(r)) { + case RTmp: + if (r.val == 0) + return r; // Ref::none() + assert(r.val < m.tmp.size()); + return m.tmp[r.val]; + case RCon: + assert(r.val < m.con.size()); + return m.con[r.val]; + default: + return r; // RInt/RType/RSlot/RMem/RCall do not need remap + } +} + +struct RetEntry { + Blk* blk; // cloned ret block in caller + Ref val; // ret value (remapped), Ref::none() if void +}; + +void clone_blk_body(Ctx* ctx, Fn* caller, Blk* src, Blk* dst, const RemapTables& m, + const std::vector<Ref>& par_args, Blk* b_post, std::vector<RetEntry>& rets) { + // phi + Phi** ptail = &dst->phi; + *ptail = nullptr; + for (Phi* sp = src->phi; sp; sp = sp->link) { + Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + np->to = remap_ref(sp->to, m); + np->cls = sp->cls; + np->visit = 0; + np->narg = sp->narg; + np->arg = static_cast<Ref*>(vnew(ctx, sp->narg, sizeof(Ref), PFn)); + np->blk = static_cast<Blk**>(vnew(ctx, sp->narg, sizeof(Blk*), PFn)); + for (uint k = 0; k < sp->narg; k++) { + np->arg[k] = remap_ref(sp->arg[k], m); + np->blk[k] = m.blk[sp->blk[k]->id]; + } + np->link = nullptr; + *ptail = np; + ptail = &np->link; + } + + // ins: Opar -> Ocopy from par_args[k] + dst->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn)); + dst->nins = 0; + int par_idx = 0; + for (Ins* si = src->ins; si < &src->ins[src->nins]; si++) { + Ins ni; + if (ispar(si->op)) { + // Replace all uses of par tmp with arg ref in caller. Produces an Ocopy to maintain SSA + // def, later copy/gvn will eliminate. + assert(par_idx < static_cast<int>(par_args.size())); + ni.op = Ocopy; + ni.cls = si->cls; + ni.to = remap_ref(si->to, m); + ni.arg[0] = par_args[static_cast<size_t>(par_idx++)]; + ni.arg[1] = Ref::none(); + } else { + ni.op = si->op; + ni.cls = si->cls; + ni.to = remap_ref(si->to, m); + ni.arg[0] = remap_ref(si->arg[0], m); + ni.arg[1] = remap_ref(si->arg[1], m); + } + addins(ctx, &dst->ins, &dst->nins, &ni); + } + + // jmp: change isret to Jjmp pointing to b_post; pass through other jmps (s1/s2 updated by + // m.blk) + if (isret(src->jmp.type)) { + rets.push_back({dst, src->jmp.type == Jret0 ? Ref::none() : remap_ref(src->jmp.arg, m)}); + dst->jmp.type = Jjmp; + dst->jmp.arg = Ref::none(); + dst->s1 = b_post; + dst->s2 = nullptr; + } else { + dst->jmp.type = src->jmp.type; + dst->jmp.arg = remap_ref(src->jmp.arg, m); + dst->s1 = src->s1 ? m.blk[src->s1->id] : nullptr; + dst->s2 = src->s2 ? m.blk[src->s2->id] : nullptr; + } + // pred/rpo/dom etc. left for fillcfg to rebuild + (void)caller; +} + +void splice_one(Ctx* ctx, Fn* caller, Blk* b, int call_idx, Fn* callee) { + Ins* call_ins = &b->ins[call_idx]; + Ref call_to = call_ins->to; + int call_cls = static_cast<int>(call_ins->cls); + (void)call_cls; + + // Collect arg_refs: scan backwards until non-isarg + int arg_start = call_idx; + while (arg_start > 0 && isarg(b->ins[arg_start - 1].op)) + arg_start--; + std::vector<Ref> arg_refs; + arg_refs.reserve(static_cast<size_t>(call_idx - arg_start)); + for (int k = arg_start; k < call_idx; k++) + arg_refs.push_back(b->ins[k].arg[0]); + + // Three remap tables + RemapTables m; + m.tmp.assign(static_cast<size_t>(callee->ntmp), Ref::none()); + m.con.assign(static_cast<size_t>(callee->ncon), Ref::none()); + m.blk.assign(static_cast<size_t>(callee->nblk), nullptr); + + // Tmp: 0=none, [1..R] skipped (reg slots), use newtmp. User tmp starts at 32. + constexpr int kFirstUserTmp = + 0; // rcc IRGen does not use reg-tmp reservations, starts at 0; tmp[0]=R + for (int t = 1; t < callee->ntmp; t++) { + const char* prfx = callee->tmp[t].name ? callee->tmp[t].name : "il"; + m.tmp[static_cast<size_t>(t)] = newtmp(ctx, prfx, callee->tmp[t].cls, caller); + } + (void)kFirstUserTmp; + // Con: 0=undef (fixed), 1=const_zero (rcc convention), others deep-copy + m.con[0] = Ref::con(0); + for (int c = 1; c < callee->ncon; c++) + m.con[static_cast<size_t>(c)] = newcon(ctx, &callee->con[c], caller); + // Blk: all newblk. inline_module is called before run_midend, callee->rpo not built yet, + // traverse start->link chain; blk->id from IRGen used as remap key. + static int splice_seq = 0; + std::vector<Blk*> callee_blks; // Align with traversal order for phase 4 + callee_blks.reserve(callee->nblk); + for (Blk* cb = callee->start; cb; cb = cb->link) { + Blk* nb = newblk(ctx); + char buf[256]; + std::snprintf(buf, sizeof(buf), "i%d.%s", splice_seq, cb->name ? cb->name : "blk"); + nb->name = strf(ctx, PFn, "%s", buf); + nb->id = caller->nblk++; + assert(cb->id < callee->nblk); + m.blk[cb->id] = nb; + callee_blks.push_back(cb); + } + ++splice_seq; + + // Cut b: new b_post, takes over b->ins[call_idx+1..] / b->jmp / s1/s2 / link + Blk* b_post = newblk(ctx); + { + static int post_seq = 0; + char buf[64]; + std::snprintf(buf, sizeof(buf), "iret.%d", post_seq++); + b_post->name = strf(ctx, PFn, "%s", buf); + b_post->id = caller->nblk++; + } + b_post->phi = nullptr; + // Take over tail ins + int tail_n = static_cast<int>(b->nins) - call_idx - 1; + if (tail_n > 0) { + for (int k = call_idx + 1; k < static_cast<int>(b->nins); k++) + addins(ctx, &b_post->ins, &b_post->nins, &b->ins[k]); + } + b_post->jmp = b->jmp; + b_post->s1 = b->s1; + b_post->s2 = b->s2; + Blk* original_b_link = b->link; + // Truncate b to b_pre (up to arg_start), jmp = Jjmp to callee.start_copy + b->nins = static_cast<uint>(arg_start); + b->jmp.type = Jjmp; + b->jmp.arg = Ref::none(); + b->s1 = m.blk[callee->start->id]; + b->s2 = nullptr; + + // Deep copy each callee blk + std::vector<RetEntry> rets; + for (Blk* src : callee_blks) { + Blk* dst = m.blk[src->id]; + clone_blk_body(ctx, caller, src, dst, m, arg_refs, b_post, rets); + } + + // If not void, insert phi at head of b_post to merge ret value + if (!req(call_to, Ref::none()) && !rets.empty()) { + Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + np->to = call_to; + np->cls = static_cast<short>(call_ins->cls); + np->visit = 0; + np->narg = static_cast<uint>(rets.size()); + np->arg = static_cast<Ref*>(vnew(ctx, np->narg, sizeof(Ref), PFn)); + np->blk = static_cast<Blk**>(vnew(ctx, np->narg, sizeof(Blk*), PFn)); + for (size_t k = 0; k < rets.size(); k++) { + np->arg[k] = rets[k].val; + np->blk[k] = rets[k].blk; + } + np->link = b_post->phi; + b_post->phi = np; + } + + // Fix external phi refs: phi inputs in original b's successors from b -> b_post + auto fix_phis_in = [&](Blk* succ) { + if (!succ) + return; + for (Phi* p = succ->phi; p; p = p->link) + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == b) + p->blk[k] = b_post; + }; + fix_phis_in(b_post->s1); + fix_phis_in(b_post->s2); + + // Link chain: b -> callee blks -> b_post -> original_b_link + Blk* last_callee_blk = nullptr; + b->link = m.blk[callee->start->id]; // start_copy immediately follows b + for (Blk* src : callee_blks) { + Blk* cur = m.blk[src->id]; + if (last_callee_blk) + last_callee_blk->link = cur; + last_callee_blk = cur; + } + assert(last_callee_blk); + last_callee_blk->link = b_post; + b_post->link = original_b_link; +} + +// Pass driver + +bool inline_pass(Ctx* ctx, std::vector<Fn*>& fns, const std::unordered_set<Fn*>& in_cycle) { + auto call_counts = collect_call_counts(fns); + bool any_change = false; + + for (Fn* caller : fns) { + for (Blk* b = caller->start; b; b = b->link) { + // Splice modifies b->ins and b->link. After a splice, b becomes b_pre, and tail ins go + // to b_post. We only scan each b once, breaking on hit (remaining calls handled in next + // fixpoint round, ensuring b->ins pointer stability). + bool spliced_in_b = false; + for (int k = 0; k < static_cast<int>(b->nins); k++) { + if (b->ins[k].op != Ocall) + continue; + Fn* callee = lookup_callee(b->ins[k].arg[0], caller, fns); + if (!callee) + continue; // extern (sylib) + if (callee == caller) + continue; // Direct self-call + if (callee->vararg) + continue; + if (in_cycle.count(callee)) + continue; // Direct/indirect recursion: cannot be callee + int cost = count_real_ins(callee); + // const-arg discount: scan backward [arg_start..k-1] for Oarg with RCon + int arg_start = k; + while (arg_start > 0 && isarg(b->ins[arg_start - 1].op)) + arg_start--; + int const_args = 0; + for (int j = arg_start; j < k; j++) + if (rtype(b->ins[j].arg[0]) == RCon) + const_args++; + cost = std::max(0, cost - const_args * kConstArgDiscount); + bool single = false; + auto it = call_counts.find(callee->name); + if (it != call_counts.end() && it->second == 1) + single = true; + int threshold; + if (single) + threshold = kInlineSingleCallerThreshold; + else if (is_leaf(callee)) + threshold = kInlineLeafThreshold; + else + threshold = kInlineNonLeafThreshold; + if (cost > threshold) + continue; + + splice_one(ctx, caller, b, k, callee); + any_change = true; + spliced_in_b = true; + if (debug['I']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> inline %s into %s (cost=%d, threshold=%d)\n", callee->name, + caller->name, cost, threshold); + } + break; + } + if (spliced_in_b) { + // b is now b_pre, followed by callee blks and b_post. Continue safely handles this. + continue; + } + } + } + return any_change; +} + +} // namespace + +void inline_module(Ctx* ctx, std::vector<Fn*>& fns) { + auto graph = build_call_graph(fns); + auto in_cycle = find_cycle_members(fns, graph); + if (debug['I']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> inline_module: call-graph %zu fn, %zu in cycle\n", fns.size(), + in_cycle.size()); + } + int iter = 0; + for (; iter < kFixpointMaxIters; iter++) { + if (!inline_pass(ctx, fns, in_cycle)) + break; + } + if (debug['I']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> inline_module: %d round%s\n", iter, iter == 1 ? "" : "s"); + } +} + +} // namespace rcc |
