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