diff options
Diffstat (limited to 'rcc-legacy/src')
| -rw-r--r-- | rcc-legacy/src/mid/load.cc | 556 | ||||
| -rw-r--r-- | rcc-legacy/src/mid/mem.cc | 653 | ||||
| -rw-r--r-- | rcc-legacy/src/mid/ssa.cc | 512 | ||||
| -rw-r--r-- | rcc-legacy/src/mid/storemrg.cc | 231 |
4 files changed, 1952 insertions, 0 deletions
diff --git a/rcc-legacy/src/mid/load.cc b/rcc-legacy/src/mid/load.cc new file mode 100644 index 0000000..a26f847 --- /dev/null +++ b/rcc-legacy/src/mid/load.cc @@ -0,0 +1,556 @@ +/* Source code for RCC-Legacy. + Copyright (C) 2026 Richard Qin + +This file is part of RCC. + +RCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +RCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with RCC; see the file LICENSE. If not see +<http://www.gnu.org/licenses/>. */ + +/// @file load.cc +/// @brief Load/store meta information + load-forwarding optimization (loadopt). +/// +/// See docs/load.md for architectural notes. + +#include "rcc/all.h" + +#include <cstdlib> + +namespace rcc { + +// loadsz / storesz - also used by alias.cc / mem.cc + +int loadsz(Ins* l) { + switch (l->op) { + case Oloadsb: + case Oloadub: + return 1; + case Oloadsh: + case Oloaduh: + return 2; + case Oloadsw: + case Oloaduw: + return 4; + case Oload: + return KWIDE(l->cls) ? 8 : 4; + } + die("loadsz: not a load op"); +} + +int storesz(Ins* s) { + switch (s->op) { + case Ostoreb: + return 1; + case Ostoreh: + return 2; + case Ostorew: + case Ostores: + return 4; + case Ostorel: + case Ostored: + return 8; + } + die("storesz: not a store op"); +} + +namespace { + +struct Slice { + Ref ref; + int off; + short sz; + short cls; +}; + +struct Loc { + enum Type { LRoot, LLoad, LNoLoad } type; + uint off; + Blk* blk; +}; + +} // namespace + +struct Insert { + uint isphi : 1; + uint num : 31; + uint bid; + uint off; + Ins ins; // valid when !isphi + Slice slice; // valid when isphi + Phi* phi_node; // valid when isphi +}; + +namespace { + +// MASK(w): All-1 mask of width w bytes. Relies on unsigned overflow for w==8. +inline bits mskbits(int w) { + return (BIT(8 * w - 1) << 1) - 1; +} + +// Insert a deferred instruction, returning new tmp +Ref iins(Ctx* ctx, int cls, int op, Ref a0, Ref a1, Loc* l) { + vgrow(ctx, &ctx->load_ilog, ++ctx->load_nlog); + Insert* ist = &ctx->load_ilog[ctx->load_nlog - 1]; + ist->isphi = 0; + ist->num = ctx->load_inum++; + ist->bid = l->blk->id; + ist->off = l->off; + Ref to = newtmp(ctx, "ld", cls, ctx->load_curf); + ist->ins.op = static_cast<uint>(op); + ist->ins.cls = static_cast<uint>(cls); + ist->ins.to = to; + ist->ins.arg[0] = a0; + ist->ins.arg[1] = a1; + return to; +} + +// Cast *r to cls, possibly inserting multiple Ocast / Oextuw +void cast_ref(Ctx* ctx, Ref* r, int cls, Loc* l) { + if (rtype(*r) == RCon) + return; + assert(rtype(*r) == RTmp); + int cls0 = ctx->load_curf->tmp[r->val].cls; + if (cls0 == cls || (cls == Kw && cls0 == Kl)) + return; + if (KWIDE(cls0) < KWIDE(cls)) { + if (cls0 == Ks) + *r = iins(ctx, Kw, Ocast, *r, Ref::none(), l); + *r = iins(ctx, Kl, Oextuw, *r, Ref::none(), l); + if (cls == Kd) + *r = iins(ctx, Kd, Ocast, *r, Ref::none(), l); + } else { + if (cls0 == Kd && cls != Kl) + *r = iins(ctx, Kl, Ocast, *r, Ref::none(), l); + if (cls0 != Kd || cls != Kw) + *r = iins(ctx, cls, Ocast, *r, Ref::none(), l); + } +} + +void mask_ref(Ctx* ctx, int cls, Ref* r, bits msk, Loc* l) { + cast_ref(ctx, r, cls, l); + *r = iins(ctx, cls, Oand, *r, getcon(ctx, static_cast<int64_t>(msk), ctx->load_curf), l); +} + +// emit_load: Insert an actual load (fallback when forwarding fails) +Ref emit_load(Ctx* ctx, Slice sl, bits msk, Loc* l) { + int ld; + switch (sl.sz) { + case 1: + ld = Oloadub; + break; + case 2: + ld = Oloaduh; + break; + case 4: + ld = Oloaduw; + break; + case 8: + ld = Oload; + break; + default: + die("loadopt: emit_load on slice size %d", sl.sz); + } + bits all_msk = mskbits(sl.sz); + bool all = (msk == all_msk); + int cls = all ? sl.cls : (sl.sz > 4 ? Kl : Kw); + + Ref r = sl.ref; + if (rtype(r) == RTmp) { + Alias* a = &ctx->load_curf->tmp[r.val].alias; + // offset_unknown aliases will not match via slice - invariant broken if reached + assert(!a->offset_unknown); + switch (a->type) { + case Alias::ALoc: + case Alias::AEsc: + case Alias::AUnk: + r = Ref::tmp(static_cast<uint>(a->base)); + if (a->offset) { + Ref r1 = getcon(ctx, a->offset, ctx->load_curf); + r = iins(ctx, Kl, Oadd, r, r1, l); + } + break; + case Alias::ACon: + case Alias::ASym: { + Con c; + memset(&c, 0, sizeof c); + c.type = Con::CAddr; + c.sym = a->u.sym; + c.bits.i = a->offset; + r = newcon(ctx, &c, ctx->load_curf); + break; + } + default: + die("loadopt: emit_load on alias type %d", a->type); + } + } + r = iins(ctx, cls, ld, r, Ref::none(), l); + if (!all) + mask_ref(ctx, cls, &r, msk, l); + return r; +} + +// rebase: Replace sl.ref with alias base + off +void rebase(Ctx* ctx, Slice* sl) { + if (rtype(sl->ref) != RTmp) + return; + Alias* a = &ctx->load_curf->tmp[sl->ref.val].alias; + // offset_unknown aliases cannot be rebased + if (a->offset_unknown) + return; + if (static_cast<int16_t>(a->offset) != a->offset) + return; + if (a->type != Alias::ALoc && a->type != Alias::AEsc && a->type != Alias::AUnk) + return; + sl->ref = Ref::tmp(static_cast<uint>(a->base)); + sl->off = static_cast<int>(a->offset); +} + +// killsl: whether r is sl's base tmp (only valid for stack type) +bool killsl(Ctx* ctx, Ref r, Slice sl) { + if (rtype(sl.ref) != RTmp) + return false; + Alias* a = &ctx->load_curf->tmp[sl.ref.val].alias; + switch (a->type) { + case Alias::ALoc: + case Alias::AEsc: + case Alias::AUnk: + return req(Ref::tmp(static_cast<uint>(a->base)), r); + case Alias::ACon: + case Alias::ASym: + return false; + default: + die("loadopt: killsl on alias type %d", a->type); + } +} + +// qsort comparator: bid asc -> phi before ins -> off asc -> num asc +int icmp(const void* pa, const void* pb) { + const Insert* a = static_cast<const Insert*>(pa); + const Insert* b = static_cast<const Insert*>(pb); + int c; + if ((c = static_cast<int>(a->bid) - static_cast<int>(b->bid))) + return c; + if (a->isphi && b->isphi) + return 0; + if (a->isphi) + return -1; + if (b->isphi) + return 1; + if ((c = static_cast<int>(a->off) - static_cast<int>(b->off))) + return c; + return static_cast<int>(a->num) - static_cast<int>(b->num); +} + +Ref def(Ctx* ctx, Slice sl, bits msk, Blk* b, Ins* i, Loc* il); + +Ref try_def(Ctx* ctx, Slice sl, bits msk, Blk* b, Ins* i, Loc* il) { + // CFG traversal invariants + assert(dom(b, il->blk)); + + if (!i) + i = &b->ins[b->nins]; + int cls = (sl.sz > 4) ? Kl : Kw; + bits msks = mskbits(sl.sz); + + // Scan backward in block + while (i > b->ins) { + --i; + if (killsl(ctx, i->to, sl)) + return Ref::none(); + if (i->op == Ocall && escapes(sl.ref, ctx->load_curf)) + return Ref::none(); + + bool is_load = isload(i->op); + int sz; + Ref r1, r; + if (is_load) { + sz = loadsz(i); + r1 = i->arg[0]; + r = i->to; + } else if (isstore(i->op)) { + sz = storesz(i); + r1 = i->arg[1]; + r = i->arg[0]; + } else if (i->op == Oblit1) { + assert(rtype(i->arg[0]) == RInt); + sz = std::abs(rsval(i->arg[0])); + assert(i > b->ins); + --i; + assert(i->op == Oblit0); + r1 = i->arg[1]; + r = Ref::none(); // blit does not use r + } else { + continue; + } + + int off; + switch (alias(sl.ref, sl.off, sl.sz, r1, sz, &off, ctx->load_curf)) { + case MustAlias: { + Slice sl1 = sl; + if (i->op == Oblit0) { + // adjust blit source slice + sl1.ref = i->arg[0]; + if (off >= 0) { + assert(off < sz); + sl1.off = off; + sz -= off; + off = 0; + } else { + sl1.off = 0; + sl1.sz = static_cast<short>(sl1.sz + off); + } + if (sz > sl1.sz) + sz = sl1.sz; + assert(sz <= 8); + sl1.sz = static_cast<short>(sz); + } + + bits msk1; + int op; + if (off < 0) { + off = -off; + msk1 = (mskbits(sz) << (8 * off)) & msks; + op = Oshl; + } else { + msk1 = (mskbits(sz) >> (8 * off)) & msks; + op = Oshr; + } + if ((msk1 & msk) == 0) + continue; + + if (i->op == Oblit0) { + r = def(ctx, sl1, mskbits(static_cast<int>(sl1.sz)), b, i, il); + if (req(r, Ref::none())) + return Ref::none(); + } + + if (off) { + int cls1 = cls; + if (op == Oshr && off + sl.sz > 4) + cls1 = Kl; + cast_ref(ctx, &r, cls1, il); + Ref r2 = getcon(ctx, 8 * off, ctx->load_curf); + r = iins(ctx, cls1, op, r, r2, il); + } + if ((msk1 & msk) != msk1 || off + sz < sl.sz) + mask_ref(ctx, cls, &r, msk1 & msk, il); + if ((msk & ~msk1) != 0) { + Ref r2 = def(ctx, sl, msk & ~msk1, b, i, il); + if (req(r2, Ref::none())) + return Ref::none(); + r = iins(ctx, cls, Oor, r, r2, il); + } + if (msk == msks) + cast_ref(ctx, &r, sl.cls, il); + return r; + } + case MayAlias: + if (is_load) + continue; + return Ref::none(); + case NoAlias: + continue; + default: + die("loadopt: alias() returned unexpected value"); + } + } + + // Block head: check if ilog already has a phi for this block and slice + for (Insert* ist = ctx->load_ilog; ist < &ctx->load_ilog[ctx->load_nlog]; ++ist) { + if (!ist->isphi || ist->bid != b->id) + continue; + if (!req(ist->slice.ref, sl.ref)) + continue; + if (ist->slice.off != sl.off) + continue; + if (ist->slice.sz != sl.sz) + continue; + Ref r = ist->phi_node->to; + if (msk != msks) + mask_ref(ctx, cls, &r, msk, il); + else + cast_ref(ctx, &r, sl.cls, il); + return r; + } + + // Check if real phi kills sl + for (Phi* p = b->phi; p; p = p->link) + if (killsl(ctx, p->to, sl)) + return Ref::none(); + + // Predecessors + if (b->npred == 0) + return Ref::none(); + if (b->npred == 1) { + Blk* bp = b->pred[0]; + assert(bp->loop >= il->blk->loop); + Loc l = *il; + if (bp->s2) + l.type = Loc::LNoLoad; + Ref r1 = def(ctx, sl, msk, bp, nullptr, &l); + if (req(r1, Ref::none())) + return Ref::none(); + return r1; + } + + // Multiple predecessors: insert new phi into log, recurse on predecessors + Ref r = newtmp(ctx, "ld", sl.cls, ctx->load_curf); + Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + memset(p, 0, sizeof *p); + vgrow(ctx, &ctx->load_ilog, ++ctx->load_nlog); + Insert* ist = &ctx->load_ilog[ctx->load_nlog - 1]; + ist->isphi = 1; + ist->bid = b->id; + ist->slice = sl; + ist->phi_node = p; + p->to = r; + p->cls = static_cast<short>(sl.cls); + p->narg = b->npred; + p->arg = static_cast<Ref*>(vnew(ctx, p->narg, sizeof(Ref), PFn)); + p->blk = static_cast<Blk**>(vnew(ctx, p->narg, sizeof(Blk*), PFn)); + for (uint np = 0; np < b->npred; ++np) { + Blk* bp = b->pred[np]; + Loc l; + if (!bp->s2 && il->type != Loc::LNoLoad && bp->loop < il->blk->loop) + l.type = Loc::LLoad; + else + l.type = Loc::LNoLoad; + l.blk = bp; + l.off = bp->nins; + Ref r1 = def(ctx, sl, msks, bp, nullptr, &l); + if (req(r1, Ref::none())) + return Ref::none(); + p->arg[np] = r1; + p->blk[np] = bp; + // XXX QBE note "multiplicity in predecessors" - same bp in b->pred multiple times not + // handled. + } + if (msk != msks) + mask_ref(ctx, cls, &r, msk, il); + return r; +} + +Ref def(Ctx* ctx, Slice sl, bits msk, Blk* b, Ins* i, Loc* il) { + uint oldl = ctx->load_nlog; + int oldt = ctx->load_curf->ntmp; + Ref r = try_def(ctx, sl, msk, b, i, il); + if (!req(r, Ref::none())) + return r; + // Rollback log and fresh tmp (vgrow does not shrink, capacity kept for reuse) + ctx->load_nlog = oldl; + ctx->load_curf->ntmp = oldt; + if (il->type != Loc::LLoad) + return Ref::none(); + return emit_load(ctx, sl, msk, il); +} + +} // namespace + +void loadopt(Ctx* ctx, Fn* fn) { + ctx->load_curf = fn; + ctx->load_ilog = static_cast<Insert*>(vnew(ctx, 0, sizeof(Insert), PHeap)); + ctx->load_nlog = 0; + ctx->load_inum = 0; + + // solve each load + for (Blk* b = fn->start; b; b = b->link) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) { + if (!isload(i->op)) + continue; + int sz = loadsz(i); + Slice sl; + sl.ref = i->arg[0]; + sl.off = 0; + sl.sz = static_cast<short>(sz); + sl.cls = static_cast<short>(i->cls); + Loc l; + l.type = Loc::LRoot; + l.off = static_cast<uint>(i - b->ins); + l.blk = b; + rebase(ctx, &sl); + i->arg[1] = def(ctx, sl, mskbits(sz), b, i, &l); + } + } + + // sort and rebuild each block + qsort(ctx->load_ilog, ctx->load_nlog, sizeof(Insert), icmp); + vgrow(ctx, &ctx->load_ilog, ctx->load_nlog + 1); + ctx->load_ilog[ctx->load_nlog].bid = fn->nblk; // sentinel: stops ist->bid == n check + + Ins* ib = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PHeap)); + Insert* ist = ctx->load_ilog; + for (uint n = 0; n < fn->nblk; ++n) { + Blk* b = fn->rpo[n]; + + // Push block's phi inserts to b->phi head + for (; ist->bid == n && ist->isphi; ++ist) { + ist->phi_node->link = b->phi; + b->phi = ist->phi_node; + } + + uint ni = 0; + uint nt = 0; + for (;;) { + Ins* i; + if (ist->bid == n && ist->off == ni) { + i = &ist->ins; + ++ist; + } else { + if (ni == b->nins) + break; + i = &b->ins[ni++]; + if (isload(i->op) && !req(i->arg[1], Ref::none())) { + // Solved load -> rewrite op + arg + int ext = Oextsb + (static_cast<int>(i->op) - Oloadsb); + switch (i->op) { + case Oloadsb: + case Oloadub: + case Oloadsh: + case Oloaduh: + i->op = static_cast<uint>(ext); + break; + case Oloadsw: + case Oloaduw: + if (i->cls == Kl) { + i->op = static_cast<uint>(ext); + break; + } + // fall through: under Kw sub-word is equivalent to full word -> direct + // copy + [[fallthrough]]; + case Oload: + i->op = Ocopy; + break; + default: + die("loadopt: unreachable in solved-load rewrite"); + } + i->arg[0] = i->arg[1]; + i->arg[1] = Ref::none(); + } + } + vgrow(ctx, &ib, ++nt); + ib[nt - 1] = *i; + } + idup(ctx, b, ib, nt); + } + vfree(ib); + vfree(ctx->load_ilog); + ctx->load_ilog = nullptr; + + if (debug['M']) { + fprintf(ctx->dbg_buf, "\n> After load elimination:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +} // namespace rcc diff --git a/rcc-legacy/src/mid/mem.cc b/rcc-legacy/src/mid/mem.cc new file mode 100644 index 0000000..12b9ef8 --- /dev/null +++ b/rcc-legacy/src/mid/mem.cc @@ -0,0 +1,653 @@ +/* Source code for RCC-Legacy. + Copyright (C) 2026 Richard Qin + +This file is part of RCC. + +RCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +RCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with RCC; see the file LICENSE. If not see +<http://www.gnu.org/licenses/>. */ + +/// @file mem.cc +/// @brief Memory optimizations: `promote` and `coalesce`. +/// +/// See docs/mem.md for architectural notes. + +#include "rcc/all.h" + +#include <climits> +#include <cstdlib> +#include <cstring> + +namespace rcc { + +// promote slots. + +namespace { + +/// Describes the result of a promote compatibility scan. +struct UniformFit { + int sz; // Shared access width; -1 means "undetermined" (only in all-load state) + int k; // Shared store cls; -1 means "no store" (pure read slot, dies later) +}; + +/// Scan all uses of alloc tmp to check if it can be promoted. Returns true and fills *fit on +/// success. +bool check_uniform(Tmp* t, Ref alloc_ref, UniformFit* fit) { + int s = -1, k = -1; + for (Use* u = t->use; u < &t->use[t->nuse]; u++) { + if (u->type != Use::UIns) + return false; + Ins* l = u->u.ins; + + if (isload(l->op)) { + int ls = loadsz(l); + if (s != -1 && s != ls) + return false; + s = ls; + continue; + } + + if (isstore(l->op)) { + // Must be store(value, addr=alloc): addr in arg[1], value in arg[0] + if (!req(alloc_ref, l->arg[1]) || req(alloc_ref, l->arg[0])) + return false; + int ss = storesz(l); + if (s != -1 && s != ss) + return false; + int sk = optab[l->op].argcls[0][0]; + if (k != -1 && k != sk) + return false; + s = ss; + k = sk; + continue; + } + + return false; // Non load/store use (addr passed out, compared, etc.) + } + fit->sz = s; + fit->k = k; + return true; +} + +inline Ins make_nop() { + Ins n{}; + n.op = Onop; + return n; +} + +/// Rewrite uses to copy / cast / ext (see docs/mem.md). +void rewrite_uses(Tmp* t, int k, Fn* fn) { + Use* ue = &t->use[t->nuse]; + for (Use* u = t->use; u != ue; u++) { + Ins* l = u->u.ins; + + if (isstore(l->op)) { + // store value, addr → copy value to addr-as-def + l->cls = static_cast<uint>(k); + l->op = Ocopy; + l->to = l->arg[1]; + l->arg[1] = Ref::none(); + t->nuse--; + t->ndef++; + continue; + } + + // Check lock. + if (k == -1) + die("promote: slot %%%s is read but never stored to", fn->tmp[l->arg[0].val].name); + + bool extend = false; + switch (l->op) { + case Oloadsw: + case Oloaduw: + if (k == Kl) { + extend = true; + break; + } + [[fallthrough]]; + case Oload: + if (KBASE(k) != KBASE(static_cast<int>(l->cls))) + l->op = Ocast; + else + l->op = Ocopy; + break; + default: + extend = true; + break; + } + if (extend) + l->op = static_cast<uint>(Oextsb + (static_cast<int>(l->op) - Oloadsb)); + } +} + +} // namespace + +void promote(Ctx* ctx, Fn* fn) { + Blk* b = fn->start; + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (i->op < Oalloc || i->op > Oalloc1) + continue; + // Assumption NAlign == 3. + assert(rtype(i->to) == RTmp); + Tmp* t = &fn->tmp[i->to.val]; + if (t->ndef != 1) + continue; + + UniformFit fit; + if (!check_uniform(t, i->to, &fit)) + continue; + + // Kill alloc, rewrite all uses. + *i = make_nop(); + t->ndef--; + rewrite_uses(t, fit.k, fn); + } + + if (debug['M']) { + fprintf(ctx->dbg_buf, "\n> After slot promotion:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +// coalesce slots. + +namespace { + +/// Half-open interval [a, b). b == 0 means "empty interval" (slot not yet radd'ed). +struct Range { + int a, b; +}; + +/// store-record: a store instruction recorded on a slot + its ip. +struct Store { + int ip; + Ins* i; +}; + +struct Slot { + int t; // corresponding tmp number + int sz; // alloc size (bytes) + bits m; // accumulated write mask for this slot (from fillalias) + bits l; // read mask accumulated during reverse block traversal + Range r; // live range (covers all active ips) + Slot* s; // fused representative (s == this means self is rep); nullptr if not fused + Store* st; // array of store records on this slot (PHeap-vnew) + int nst; +}; + +inline bool rin(Range r, int n) { + return r.a <= n && n < r.b; +} + +inline bool rovlap(Range r0, Range r1) { + // Overlap check. + return r0.b && r1.b && r0.a < r1.b && r1.a < r0.b; +} + +void radd(Range* r, int n) { + if (!r->b) { + r->a = n; + r->b = n + 1; + } else if (n < r->a) { + r->a = n; + } else if (n >= r->b) { + r->b = n + 1; + } +} + +/// Translate ref r to its slot (if any) and offset. Returns false if r does not point to a coalesce +/// candidate. +bool slot_of(Slot** ps, int64_t* off, Ref r, Fn* fn, Slot* sl) { + Alias a; + getalias(&a, r, fn); + if (a.type != Alias::ALoc) + return false; + Tmp* t = &fn->tmp[a.base]; + if (t->visit < 0) + return false; // tmp not in sl[] + *off = a.offset; + *ps = &sl[t->visit]; + return true; +} + +/// Encountered a read of x high-order bytes from r during reverse RPO. +void load(Ref r, bits x, int ip, Fn* fn, Slot* sl) { + int64_t off; + Slot* s; + if (!slot_of(&s, &off, r, fn, sl)) + return; + s->l |= x << off; + s->l &= s->m; + if (s->l) + radd(&s->r, ip); +} + +/// Encountered a write of x bytes to r during reverse RPO. +/// Encountered a write. +void store(Ref r, bits x, int ip, Ins* i, Ctx* ctx, Fn* fn, Slot* sl) { + int64_t off; + Slot* s; + if (!slot_of(&s, &off, r, fn, sl)) + return; + if (s->l) { + // Currently has live bytes: store is valid, advance live range. + radd(&s->r, ip); + s->l &= ~(x << off); + } else { + // No live bytes: store is a potential dead store, record in slot.st[]. + vgrow(ctx, &s->st, static_cast<ulong>(++s->nst)); + s->st[s->nst - 1].ip = ip; + s->st[s->nst - 1].i = i; + } +} + +/// qsort comparator: descending by sz; ascending by r.a if sz equal (earlier first). +int scmp(const void* pa, const void* pb) { + const Slot* a = static_cast<const Slot*>(pa); + const Slot* b = static_cast<const Slot*>(pb); + if (a->sz != b->sz) + return b->sz - a->sz; + return a->r.a - b->r.a; +} + +/// loopiter callback (see docs/mem.md). +void maxrpo(Blk* hd, Blk* b) { + if (hd->loop < static_cast<int>(b->id)) + hd->loop = static_cast<int>(b->id); +} + +struct SlotList { + Slot* sl; + int nsl; +}; + +SlotList collect_slots(Ctx* ctx, Fn* fn) { + SlotList sls; + sls.nsl = 0; + sls.sl = static_cast<Slot*>(vnew(ctx, 0, sizeof(Slot), PHeap)); + for (int n = Tmp0; n < fn->ntmp; n++) { + Tmp* t = &fn->tmp[n]; + t->visit = -1; + // Qualification check. + if (t->alias.type != Alias::ALoc) + continue; + if (t->alias.slot != &t->alias) + continue; + if (t->bid != fn->start->id) + continue; + if (t->alias.u.loc.sz == -1) + continue; + + t->visit = sls.nsl; + vgrow(ctx, &sls.sl, static_cast<ulong>(++sls.nsl)); + Slot* s = &sls.sl[sls.nsl - 1]; + s->t = n; + s->sz = t->alias.u.loc.sz; + s->m = t->alias.u.loc.m; + s->l = 0; + s->r = Range{0, 0}; + s->s = nullptr; + s->st = static_cast<Store*>(vnew(ctx, 0, sizeof(Store), PHeap)); + s->nst = 0; + } + return sls; +} + +struct LiveOut { + Range* br; // br[n] is the entry/exit ip range of the n-th RPO block; PHeap, freed at coalesce + // end. + Ins** bl; // blit list (PHeap vnew) + int nbl; + int ip; // final ip (minimum); used for debugging output restoration. +}; + +LiveOut compute_liveness(Ctx* ctx, Fn* fn, Slot* sl, int nsl) { + LiveOut lo; + lo.nbl = 0; + lo.bl = static_cast<Ins**>(vnew(ctx, 0, sizeof(Ins*), PHeap)); + lo.br = static_cast<Range*>(emalloc(static_cast<size_t>(fn->nblk) * sizeof(Range))); + + // Initialize b->loop for maxrpo + for (Blk* b = fn->start; b; b = b->link) + b->loop = -1; + loopiter(fn, maxrpo); + + int ip = INT_MAX - 1; + for (int n = static_cast<int>(fn->nblk) - 1; n >= 0; n--) { + Blk* b = fn->rpo[n]; + + // succ array (s1, s2 deduplicated, terminated by nullptr) + Blk* succ[3]; + succ[0] = b->s1; + succ[1] = b->s2; + succ[2] = nullptr; + + lo.br[n].b = ip--; + + // For each slot: inherit live bytes from successor entries. + for (Slot* s = sl; s < &sl[nsl]; s++) { + s->l = 0; + for (Blk** ps = succ; *ps; ps++) { + int m = static_cast<int>((*ps)->id); + if (m > n && rin(s->r, lo.br[m].a)) { + s->l = s->m; + radd(&s->r, ip); + } + } + } + + // Process Jretc jmp arg: treat as full slot read. + if (b->jmp.type == Jretc) + load(b->jmp.arg, static_cast<bits>(-1), --ip, fn, sl); + + // Reverse traversal of instructions + for (Ins* i = &b->ins[b->nins]; i != b->ins;) { + --i; + Ref* arg = i->arg; + + if (i->op == Oargc) { + // by-value struct arg: mark entire slot as read + load(arg[1], static_cast<bits>(-1), --ip, fn, sl); + } + if (isload(i->op)) { + bits x = BIT(loadsz(i)) - 1; + load(arg[0], x, --ip, fn, sl); + } + if (isstore(i->op)) { + bits x = BIT(storesz(i)) - 1; + store(arg[1], x, ip--, i, ctx, fn, sl); + } + if (i->op == Oblit0) { + assert((i + 1)->op == Oblit1); + assert(rtype((i + 1)->arg[0]) == RInt); + int sz = std::abs(rsval((i + 1)->arg[0])); + bits x = sz >= static_cast<int>(NBit) ? static_cast<bits>(-1) : BIT(sz) - 1; + store(arg[1], x, ip--, i, ctx, fn, sl); + load(arg[0], x, ip, fn, sl); + vgrow(ctx, &lo.bl, static_cast<ulong>(++lo.nbl)); + lo.bl[lo.nbl - 1] = i; + } + } + + // Block entry update. + for (Slot* s = sl; s < &sl[nsl]; s++) + if (s->l) { + radd(&s->r, ip); + if (b->loop != -1) { + assert(b->loop >= n); + radd(&s->r, lo.br[b->loop].b - 1); + } + } + lo.br[n].a = ip; + } + + lo.ip = ip; + return lo; +} + +void kill_dead_stores(Slot* sl, int nsl) { + for (Slot* s = sl; s < &sl[nsl]; s++) { + for (int n = 0; n < s->nst; n++) { + if (rin(s->r, s->st[n].ip)) + continue; + // ip not in r -> dead store + Ins* i = s->st[n].i; + if (i->op == Oblit0) + *(i + 1) = make_nop(); + *i = make_nop(); + } + } +} + +/// Kills empty interval slots recursively (see docs/mem.md). +struct KillStats { + int new_nsl; + uint total; + uint freed; +}; + +KillStats kill_empty_slots(Ctx* ctx, Slot* sl, int nsl, Fn* fn) { + int* stk = static_cast<int*>(vnew(ctx, 0, sizeof(int), PHeap)); + int n = 0; + uint total = 0, freed = 0; + + // Compact sl[]. + Slot* s0 = sl; + for (Slot* s = sl; s < &sl[nsl]; s++) { + total += static_cast<uint>(s->sz); + if (!s->r.b) { + vfree(s->st); + vgrow(ctx, &stk, static_cast<ulong>(++n)); + stk[n - 1] = s->t; + freed += static_cast<uint>(s->sz); + } else { + *s0++ = *s; + } + } + int new_nsl = static_cast<int>(s0 - sl); + + if (debug['M']) { + fputs("\n> Slot coalescing:\n", ctx->dbg_buf); + if (n) { + fputs("\tkill [", ctx->dbg_buf); + for (int m = 0; m < n; m++) + fprintf(ctx->dbg_buf, " %%%s", fn->tmp[stk[m]].name); + fputs(" ]\n", ctx->dbg_buf); + } + } + + // Propagate kill. + while (n--) { + Tmp* t = &fn->tmp[stk[n]]; + assert(t->ndef == 1 && t->def); + Ins* i = t->def; + if (isload(i->op)) { + // This tmp is a pointer loaded; change to copy UNDEF so subsequent fold cleans it + // naturally. + i->op = Ocopy; + i->arg[0] = Ref::undef(); + continue; + } + *i = make_nop(); + for (Use* u = t->use; u < &t->use[t->nuse]; u++) { + if (u->type == Use::UJmp) { + Blk* b = fn->rpo[u->bid]; + assert(isret(b->jmp.type)); + b->jmp.type = Jret0; + b->jmp.arg = Ref::none(); + continue; + } + assert(u->type == Use::UIns); + i = u->u.ins; + if (!req(i->to, Ref::none())) { + assert(rtype(i->to) == RTmp); + vgrow(ctx, &stk, static_cast<ulong>(++n)); + stk[n - 1] = static_cast<int>(i->to.val); + } else if (isarg(i->op)) { + // Only Oargc reaches here: change its arg[1] to CON_Z to crash the callee + // intentionally. + assert(i->op == Oargc); + i->arg[1] = Ref::con_z(); + } else { + if (i->op == Oblit0) + *(i + 1) = make_nop(); + *i = make_nop(); + } + } + } + vfree(stk); + + return KillStats{new_nsl, total, freed}; +} + +uint fuse_slots(Slot* sl, int nsl) { + qsort(sl, static_cast<size_t>(nsl), sizeof(Slot), scmp); + uint fused = 0; + + for (int n = 0; n < nsl; n++) { + Slot* s0 = &sl[n]; + if (s0->s) + continue; + s0->s = s0; + Range r = s0->r; + + for (Slot* s = s0 + 1; s < &sl[nsl]; s++) { + if (s->s || !s->r.b) + continue; + if (rovlap(r, s->r)) { + // Scan for collision. + bool conflict = false; + for (int m = n; &sl[m] < s; m++) { + if (sl[m].s == s0 && rovlap(sl[m].r, s->r)) { + conflict = true; + break; + } + } + if (conflict) + continue; + } + radd(&r, s->r.a); + radd(&r, s->r.b - 1); + s->s = s0; + fused += static_cast<uint>(s->sz); + } + } + return fused; +} + +void substitute_fused_slots(Slot* sl, int nsl, Fn* fn) { + for (Slot* s = sl; s < &sl[nsl]; s++) { + Tmp* t = &fn->tmp[s->t]; + // Reset visit. + t->visit = static_cast<int>(s - sl); + assert(t->ndef == 1 && t->def); + if (s->s == s) + continue; + + // Erase alloc instruction of this slot + *t->def = make_nop(); + + Tmp* ts = &fn->tmp[s->s->t]; + assert(t->bid == ts->bid); + + // Guarantee dominance. + if (t->def < ts->def) { + *t->def = *ts->def; + *ts->def = make_nop(); + ts->def = t->def; + } + + // Rewrite all uses to point to the representative tmp + for (Use* u = t->use; u < &t->use[t->nuse]; u++) { + if (u->type == Use::UJmp) { + Blk* b = fn->rpo[u->bid]; + b->jmp.arg = Ref::tmp(static_cast<uint>(s->s->t)); + continue; + } + assert(u->type == Use::UIns); + Ref* arg = u->u.ins->arg; + for (int n = 0; n < 2; n++) + if (req(arg[n], Ref::tmp(static_cast<uint>(s->t)))) + arg[n] = Ref::tmp(static_cast<uint>(s->s->t)); + } + } +} + +void fix_blits(Ins** bl, int nbl, Slot* sl, Fn* fn) { + for (int n = 0; n < nbl; n++) { + Ins* i = bl[n]; + if (i->op != Oblit0) + continue; // Already cleaned up in some round + Slot *sa, *sb; + int64_t off0, off1; + if (!slot_of(&sa, &off0, i->arg[0], fn, sl)) + continue; + if (!slot_of(&sb, &off1, i->arg[1], fn, sl)) + continue; + if (sa->s != sb->s) + continue; + + // Both ends fused into same slot + if (off0 < off1) { + // Reverse blit: negative size to tell backend to reverse memmove. + int sz = rsval((i + 1)->arg[0]); + assert(sz >= 0); + (i + 1)->arg[0] = Ref::integer(-sz); + } else if (off0 == off1) { + // Self-blit: delete entirely. + *i = make_nop(); + *(i + 1) = make_nop(); + } + } +} + +void print_fuse_groups(Ctx* ctx, Slot* sl, int nsl, int ip, Fn* fn, uint freed, uint fused, + uint total) { + FILE* f = ctx->dbg_buf; + for (Slot* s0 = sl; s0 < &sl[nsl]; s0++) { + if (s0->s != s0) + continue; + fprintf(f, "\tfuse (% 3db) [", s0->sz); + for (Slot* s = s0; s < &sl[nsl]; s++) { + if (s->s != s0) + continue; + fprintf(f, " %%%s", fn->tmp[s->t].name); + if (s->r.b) + fprintf(f, "[%d,%d)", s->r.a - ip, s->r.b - ip); + else + fputs("{}", f); + } + fputs(" ]\n", f); + } + fprintf(f, "\tsums %u/%u/%u (killed/fused/total)\n\n", freed, fused, total); + printfn(fn, f); +} + +} // namespace + +void coalesce(Ctx* ctx, Fn* fn) { + // Collect candidate slots + SlotList sls = collect_slots(ctx, fn); + Slot* sl = sls.sl; + int nsl = sls.nsl; + + // Single reverse RPO to compute live ranges + LiveOut lo = compute_liveness(ctx, fn, sl, nsl); + free(lo.br); + + // Kill out-of-range stores + kill_dead_stores(sl, nsl); + + // Kill empty interval slots and propagate use-chain cleanup + KillStats ks = kill_empty_slots(ctx, sl, nsl, fn); + nsl = ks.new_nsl; + + // Fuse slots + uint fused = fuse_slots(sl, nsl); + + // Redirect def/use of fused slots to representatives + substitute_fused_slots(sl, nsl, fn); + + // Fix instructions that became same-slot self-blits + fix_blits(lo.bl, lo.nbl, sl, fn); + vfree(lo.bl); + + if (debug['M']) + print_fuse_groups(ctx, sl, nsl, lo.ip, fn, ks.freed, fused, ks.total); + + for (Slot* s = sl; s < &sl[nsl]; s++) + vfree(s->st); + vfree(sl); +} + +} // namespace rcc diff --git a/rcc-legacy/src/mid/ssa.cc b/rcc-legacy/src/mid/ssa.cc new file mode 100644 index 0000000..75e2773 --- /dev/null +++ b/rcc-legacy/src/mid/ssa.cc @@ -0,0 +1,512 @@ +/* Source code for RCC-Legacy. + Copyright (C) 2026 Richard Qin + +This file is part of RCC. + +RCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +RCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with RCC; see the file LICENSE. If not see +<http://www.gnu.org/licenses/>. */ + +/// @file ssa.cc +/// @brief Use chain population / SSA construction (Cytron-style) / SSA invariant validation. +/// +/// See docs/ssa.md for architectural notes. + +#include "rcc/all.h" + +namespace rcc { + +// Use chain append + +namespace { + +inline void use_append(Ctx* ctx, Tmp* tmp, decltype(Use::type) ty, Blk* b, Phi* phi, Ins* ins) { + if (!tmp->use) + return; + uint n = tmp->nuse++; + vgrow(ctx, &tmp->use, tmp->nuse); + Use* u = &tmp->use[n]; + u->type = ty; + u->bid = b->id; + if (ty == Use::UPhi) + u->u.phi = phi; + else if (ty == Use::UIns) + u->u.ins = ins; + // UJmp has no extra payload +} + +} // namespace + +void adduse_ins(Ctx* ctx, Tmp* tmp, Blk* b, Ins* ins) { + use_append(ctx, tmp, Use::UIns, b, nullptr, ins); +} +void adduse_phi(Ctx* ctx, Tmp* tmp, Blk* b, Phi* phi) { + use_append(ctx, tmp, Use::UPhi, b, phi, nullptr); +} +void adduse_jmp(Ctx* ctx, Tmp* tmp, Blk* b) { + use_append(ctx, tmp, Use::UJmp, b, nullptr, nullptr); +} + +// filluse - Recompute def / use / cls / width / phi-equiv-class for each tmp + +namespace { + +/// Derive width from def instruction op (sub-word width info, for spill storage selection). +int derive_width(Ins* i) { + int op = i->op; + if (isparbh(op)) + return Tmp::Wsb + (op - Oparsb); + if (isload(op) && op != Oload) + return Tmp::Wsb + (op - Oloadsb); + if (isext(op)) + return Tmp::Wsb + (op - Oextsb); + int kx, cx; + if (iscmp(op, &kx, &cx)) + return Tmp::Wub; + return Tmp::WFull; +} + +} // namespace + +void filluse(Ctx* ctx, Fn* fn) { + Tmp* tmp = fn->tmp; + + // Reset metadata for each user tmp; first call creates empty vnew head for use[]. + for (int t = Tmp0; t < fn->ntmp; t++) { + tmp[t].def = nullptr; + tmp[t].bid = ~0u; + tmp[t].ndef = 0; + tmp[t].nuse = 0; + tmp[t].cls = 0; + tmp[t].phi = 0; + tmp[t].width = Tmp::WFull; + if (!tmp[t].use) + tmp[t].use = static_cast<Use*>(vnew(ctx, 0, sizeof(Use), PFn)); + } + + // Iterate blocks, register phi def/use, ins def/use, jmp arg use. + for (Blk* b = fn->start; b; b = b->link) { + // phi + for (Phi* p = b->phi; p; p = p->link) { + assert(rtype(p->to) == RTmp); + int td = p->to.val; + tmp[td].bid = b->id; + tmp[td].ndef++; + tmp[td].cls = p->cls; + + int td_cls = phicls(td, fn->tmp); + for (uint a = 0; a < p->narg; a++) { + if (rtype(p->arg[a]) != RTmp) + continue; + int ta = p->arg[a].val; + adduse_phi(ctx, &tmp[ta], b, p); + int ta_cls = phicls(ta, fn->tmp); + if (ta_cls != td_cls) + tmp[ta_cls].phi = td_cls; + } + } + + // Instruction def + double arg use + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (!req(i->to, Ref::none())) { + assert(rtype(i->to) == RTmp); + int td = i->to.val; + int w = derive_width(i); + // Under Kw cls, Wsw/Wuw is equivalent to WFull (high bits don't exist anyway) + if ((w == Tmp::Wsw || w == Tmp::Wuw) && i->cls == Kw) + w = Tmp::WFull; + tmp[td].width = static_cast<decltype(tmp[td].width)>(w); + tmp[td].def = i; + tmp[td].bid = b->id; + tmp[td].ndef++; + tmp[td].cls = static_cast<short>(i->cls); + } + for (int m = 0; m < 2; m++) { + if (rtype(i->arg[m]) != RTmp) + continue; + int ta = i->arg[m].val; + adduse_ins(ctx, &tmp[ta], b, i); + } + } + + // Terminating jump arg + if (rtype(b->jmp.arg) == RTmp) + adduse_jmp(ctx, &tmp[b->jmp.arg.val], b); + } +} + +// Name Pool - per-Ctx, emalloc nodes reused across fn, drained at ~Ctx + +struct Name { + Ref r; + Blk* b; + Name* up; +}; + +namespace { + +Name* nnew(Ctx* ctx, Ref r, Blk* b, Name* up) { + Name* n; + if (ctx->ssa_namel) { + n = ctx->ssa_namel; + ctx->ssa_namel = n->up; + } else { + // Reused across fn, so not in SubPool (SubPool frees with fn, list would dangle) + n = static_cast<Name*>(emalloc(sizeof(Name))); + } + n->r = r; + n->b = b; + n->up = up; + return n; +} + +void nfree(Ctx* ctx, Name* n) { + n->up = ctx->ssa_namel; + ctx->ssa_namel = n; +} + +} // namespace + +void ssa_drain_namel(Ctx* ctx) { + Name* n = ctx->ssa_namel; + while (n) { + Name* nxt = n->up; + free(n); + n = nxt; + } + ctx->ssa_namel = nullptr; +} + +// Rename helpers: rendef registers new def, getstk queries currently visible name, renblk DFS along +// dom tree + +namespace { + +Ref fresh_rename(Ctx* ctx, int t, Fn* fn) { + return newtmp(ctx, fn->tmp[t].name, fn->tmp[t].cls, fn); +} + +inline bool needs_rename(Fn* fn, int t) { + return fn->tmp[t].visit != 0; +} + +/// Change a def to a fresh name. +void rendef(Ctx* ctx, Ref* r, Blk* b, Name** stk, Fn* fn) { + if (req(*r, Ref::none())) + return; + int t = r->val; + if (!needs_rename(fn, t)) + return; + Ref r1 = fresh_rename(ctx, t, fn); + fn->tmp[r1.val].visit = t; // Record "this fresh name originates from original tmp t" + stk[t] = nnew(ctx, r1, b, stk[t]); + *r = r1; +} + +/// Query currently visible name of tmp t at b. +Ref getstk(Ctx* ctx, int t, Blk* b, Name** stk) { + Name* n = stk[t]; + while (n && !dom(n->b, b)) { + Name* dead = n; + n = n->up; + nfree(ctx, dead); + } + stk[t] = n; + return n ? n->r : Ref::undef(); +} + +void renblk(Ctx* ctx, Blk* b, Name** stk, Fn* fn) { + // Rename phi-def + for (Phi* p = b->phi; p; p = p->link) + rendef(ctx, &p->to, b, stk, fn); + + // Instructions: rename args first (read old -> write new), then rendef def + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + for (int m = 0; m < 2; m++) { + if (rtype(i->arg[m]) != RTmp) + continue; + int t = i->arg[m].val; + if (needs_rename(fn, t)) + i->arg[m] = getstk(ctx, t, b, stk); + } + rendef(ctx, &i->to, b, stk, fn); + } + + // jmp arg + if (rtype(b->jmp.arg) == RTmp) { + int t = b->jmp.arg.val; + if (needs_rename(fn, t)) + b->jmp.arg = getstk(ctx, t, b, stk); + } + + // Append arg from this block to phi of each successor s + Blk* succs[2] = {b->s1, (b->s2 == b->s1) ? nullptr : b->s2}; + for (Blk* s : succs) { + if (!s) + continue; + for (Phi* p = s->phi; p; p = p->link) { + int orig_t = p->to.val; + int src_t = fn->tmp[orig_t].visit; // phi.to was inserted by phiins, visit=source tmp + if (!src_t) + continue; + uint k = p->narg++; + vgrow(ctx, &p->arg, p->narg); + vgrow(ctx, &p->blk, p->narg); + p->arg[k] = getstk(ctx, src_t, b, stk); + p->blk[k] = b; + } + } + + // Recurse dom-tree children + for (Blk* s = b->dom; s; s = s->dlink) + renblk(ctx, s, stk, fn); +} + +// phiins - Cytron-style phi insertion + +/// Scan all blocks, handle intra-block multi-defs of tmp t. +short collect_real_defs(Ctx* ctx, Fn* fn, int t, BSet* on_worklist, Blk** be, Blk*** bp_io) { + short k = Kx; + Ref t_ref = Ref::tmp(static_cast<uint>(t)); + + for (Blk* b = fn->start; b; b = b->link) { + b->visit = 0; + Ref running = Ref::none(); + + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + // Rewrite uses appearing after running to running name + if (!req(running, Ref::none())) { + if (req(i->arg[0], t_ref)) + i->arg[0] = running; + if (req(i->arg[1], t_ref)) + i->arg[1] = running; + } + if (!req(i->to, t_ref)) + continue; + + if (!bshas(b->out, static_cast<uint>(t))) { + // Def that doesn't escape block at the end: fresh naming in-place + running = fresh_rename(ctx, t, fn); + i->to = running; + } else { + // Truly escaping def: push b to worklist + if (!bshas(on_worklist, b->id)) { + bsset(on_worklist, b->id); + *--(*bp_io) = b; + } + if (clsmerge(&k, static_cast<short>(i->cls))) + die("ssa: incompatible classes for %%%s", fn->tmp[t].name); + } + } + (void)be; + + // Block tail jmp arg also follows running + if (!req(running, Ref::none()) && req(b->jmp.arg, t_ref)) + b->jmp.arg = running; + } + return k; +} + +void phiins(Ctx* ctx, Fn* fn) { + BSet on_worklist[1], original_defs[1]; + bsinit(ctx, on_worklist, fn->nblk); + bsinit(ctx, original_defs, fn->nblk); + + // worklist is a stack, growing backwards from blist end. bp == be means empty. + Blk** blist = static_cast<Blk**>(emalloc(fn->nblk * sizeof(Blk*))); + Blk** be = blist + fn->nblk; + + for (int t = Tmp0; t < fn->ntmp; t++) { + fn->tmp[t].visit = 0; + if (fn->tmp[t].phi != 0) + continue; // Not head of phi-equiv-class, skip (meaning tmp is already handled by its + // representative) + + // Fast path: single def and (def at entry, or all uses in same block as def) -> no phi and + // rename needed + if (fn->tmp[t].ndef == 1) { + bool same = true; + uint defb = fn->tmp[t].bid; + Use* uses = fn->tmp[t].use; + for (uint n = 0; n < fn->tmp[t].nuse; n++) + if (uses[n].bid != defb) { + same = false; + break; + } + if (same || defb == fn->start->id) + continue; + } + + bszero(on_worklist); + Blk** bp = be; + short k = collect_real_defs(ctx, fn, t, on_worklist, be, &bp); + + // original_defs is used later to check "did phi block originally have def of t" + bscopy(original_defs, on_worklist); + + // Phase 2: IDF main loop + while (bp != be) { + fn->tmp[t].visit = t; // Mark: t actually entered renaming + Blk* b = *bp++; + bsclr(on_worklist, b->id); + + for (uint n = 0; n < b->nfron; n++) { + Blk* a = b->fron[n]; + if (a->visit++ != 0) + continue; // Already inserted phi for a in this round + if (!bshas(a->in, static_cast<uint>(t))) + continue; // t not live at a entry, no phi needed + + Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + memset(p, 0, sizeof(Phi)); + p->cls = k; + p->to = Ref::tmp(static_cast<uint>(t)); + p->link = a->phi; + p->arg = static_cast<Ref*>(vnew(ctx, 0, sizeof(Ref), PFn)); + p->blk = static_cast<Blk**>(vnew(ctx, 0, sizeof(Blk*), PFn)); + a->phi = p; + + // a is now also a def block of t (phi form), push a to worklist to continue IDF + if (bshas(original_defs, a->id)) + continue; + if (bshas(on_worklist, a->id)) + continue; + bsset(on_worklist, a->id); + *--bp = a; + } + } + } + + free(blist); +} + +} // namespace + +// ssa() - Main entry: filldom -> fillfron -> filllive -> phiins -> renblk + +void ssa(Ctx* ctx, Fn* fn) { + // stk[] is allocated by "ntmp before rename". + int ntmp0 = fn->ntmp; + Name** stk = static_cast<Name**>(emalloc(static_cast<size_t>(ntmp0) * sizeof(Name*))); + + // Internal filllive call in ssa() is only for phiins, avoiding leak to -d L dump + int saved_L = debug['L']; + debug['L'] = 0; + + filldom(fn); + + if (debug['N']) { + FILE* f = ctx->dbg_buf; + fprintf(f, "\n> Dominators:\n"); + for (Blk* bb = fn->start; bb; bb = bb->link) { + if (!bb->dom) + continue; + fprintf(f, "%10s:", bb->name); + for (Blk* c = bb->dom; c; c = c->dlink) + fprintf(f, " %s", c->name); + fputc('\n', f); + } + } + + fillfron(ctx, fn); + filllive(ctx, fn); + phiins(ctx, fn); + renblk(ctx, fn->start, stk, fn); + + // Reclaim all leftover Names on stk[] to free list (only walking ntmp0 range) + for (int t = ntmp0; --t >= 0;) { + Name* n = stk[t]; + while (n) { + Name* nx = n->up; + nfree(ctx, n); + n = nx; + } + } + free(stk); + + debug['L'] = static_cast<char>(saved_L); + + if (debug['N']) { + fprintf(ctx->dbg_buf, "\n> After SSA construction:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +// ssacheck - SSA invariant validation + +namespace { + +bool phi_arg_dominated_by(Phi* p, Blk* def_blk, Ref defref) { + for (uint n = 0; n < p->narg; n++) { + if (!req(p->arg[n], defref)) + continue; + if (!dom(def_blk, p->blk[n])) + return false; + } + return true; +} + +[[noreturn]] void ssa_err(Tmp* t, Blk* ub) { + if (t->visit) + die("ssa: %%%s violates SSA invariant", t->name); + die("ssa: %%%s used undefined in @%s", t->name, ub->name); +} + +/// Verify all uses of t are dominated by (def_blk, def_ins). def_ins = nullptr means phi-def. +void verify_uses(Fn* fn, Blk* def_blk, Ins* def_ins, Tmp* t, Ref defref) { + for (Use* u = t->use; u < &t->use[t->nuse]; u++) { + Blk* ub = fn->rpo[u->bid]; + + if (u->type == Use::UPhi) { + if (!phi_arg_dominated_by(u->u.phi, def_blk, defref)) + ssa_err(t, ub); + continue; + } + + if (ub == def_blk) { + // Intra-block: ins-use must be after ins-def (pointer order; valid comparison in same + // b->ins array) + if (def_ins && u->type == Use::UIns && u->u.ins <= def_ins) + ssa_err(t, ub); + // UJmp is at block end, definitely after def; no extra check needed + } else { + if (!sdom(def_blk, ub)) + ssa_err(t, ub); + } + } +} + +} // namespace + +void ssacheck(Fn* fn) { + // Global check: each tmp <=1 def; use must have def + for (Tmp* t = &fn->tmp[Tmp0]; t - fn->tmp < fn->ntmp; t++) { + if (t->ndef > 1) + die("ssa: temporary %%%s defined more than once", t->name); + if (t->nuse > 0 && t->ndef == 0) + ssa_err(t, fn->rpo[t->use[0].bid]); + } + + // Each def's use must be dominated by it + for (Blk* b = fn->start; b; b = b->link) { + for (Phi* p = b->phi; p; p = p->link) + verify_uses(fn, b, nullptr, &fn->tmp[p->to.val], p->to); + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (rtype(i->to) != RTmp) + continue; + verify_uses(fn, b, i, &fn->tmp[i->to.val], i->to); + } + } +} + +} // namespace rcc diff --git a/rcc-legacy/src/mid/storemrg.cc b/rcc-legacy/src/mid/storemrg.cc new file mode 100644 index 0000000..65b4bd4 --- /dev/null +++ b/rcc-legacy/src/mid/storemrg.cc @@ -0,0 +1,231 @@ +/* Source code for RCC-Legacy. + Copyright (C) 2026 Richard Qin + +This file is part of RCC. + +RCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +RCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with RCC; see the file LICENSE. If not see +<http://www.gnu.org/licenses/>. */ + +/// @file storemrg.cc +/// @brief Fuses adjacent 4B stores into 8B storel. +/// +/// See docs/storemrg.md for architectural notes. + +#include "rcc/all.h" + +#include <vector> + +namespace rcc { + +namespace { + +// Read 32-bit integer constant value from Ref r (put into *val_out). +bool get_int_con32(Fn* fn, Ref r, uint32_t* val_out) { + if (rtype(r) != RCon) + return false; + Con* c = &fn->con[r.val]; + if (c->type != Con::CBits || c->flt) + return false; + *val_out = static_cast<uint32_t>(c->bits.i); + return true; +} + +bool ref_eq(Ref a, Ref b) { + return a.type == b.type && a.val == b.val; +} + +// Decompose addr into (base, offset). +void decompose_addr(Fn* fn, Ref addr, Ref* base_out, int64_t* off_out) { + *base_out = addr; + *off_out = 0; + if (rtype(addr) != RTmp) + return; + Tmp* t = &fn->tmp[addr.val]; + if (!t->def) + return; + Ins* def = t->def; + if (def->op != Oadd) + return; + Ref ra = def->arg[0], rb = def->arg[1]; + auto try_split = [&](Ref tmp_r, Ref con_r) -> bool { + if (rtype(tmp_r) != RTmp || rtype(con_r) != RCon) + return false; + Con* c = &fn->con[con_r.val]; + if (c->type != Con::CBits || c->flt) + return false; + *base_out = tmp_r; + *off_out = c->bits.i; + return true; + }; + if (try_split(ra, rb)) + return; + try_split(rb, ra); +} + +bool con_is_aligned8(Fn* fn, Ref r) { + if (rtype(r) != RCon) + return false; + Con* c = &fn->con[r.val]; + return c->type == Con::CBits && !c->flt && (c->bits.i & 7) == 0; +} + +bool tmp_in_align8(const std::vector<bool>& a8, Ref r) { + if (rtype(r) != RTmp) + return false; + if (r.val >= a8.size()) + return false; + return a8[r.val]; +} + +// Optimistic fixpoint: compute aligned8 set. +std::vector<bool> compute_aligned8(Fn* fn) { + std::vector<bool> a8(static_cast<size_t>(fn->ntmp), true); + bool changed = true; + while (changed) { + changed = false; + for (Blk* b = fn->start; b; b = b->link) { + for (Phi* p = b->phi; p; p = p->link) { + if (rtype(p->to) != RTmp) + continue; + uint tid = p->to.val; + if (tid >= a8.size() || !a8[tid]) + continue; + bool ok = p->narg > 0; + for (uint k = 0; k < p->narg; k++) { + Ref r = p->arg[k]; + if (tmp_in_align8(a8, r)) + continue; + if (con_is_aligned8(fn, r)) + continue; + ok = false; + break; + } + if (!ok) { + a8[tid] = false; + changed = true; + } + } + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (rtype(i->to) != RTmp) + continue; + uint tid = i->to.val; + if (tid >= a8.size() || !a8[tid]) + continue; + bool ok = false; + int op = i->op; + if (op == Oalloc8 || op == Oalloc16) { + ok = true; + } else if (op == Ocopy) { + if (tmp_in_align8(a8, i->arg[0])) + ok = true; + } else if (op == Oadd) { + if ((tmp_in_align8(a8, i->arg[0]) && con_is_aligned8(fn, i->arg[1])) || + (tmp_in_align8(a8, i->arg[1]) && con_is_aligned8(fn, i->arg[0]))) + ok = true; + } + // All other ops (Omul / Oload / Opar / Ocall / ...) are unconditionally demoted. + if (!ok) { + a8[tid] = false; + changed = true; + } + } + } + } + return a8; +} + +} // namespace + +void storemrg(Ctx* ctx, Fn* fn) { + std::vector<bool> a8 = compute_aligned8(fn); + + int merged = 0; + for (Blk* b = fn->start; b; b = b->link) { + struct Pending { + uint idx; + Ref base; + int64_t off; + uint32_t val; // 32-bit store value + }; + std::vector<Pending> pending; + auto flush = [&]() { pending.clear(); }; + + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + int op = i->op; + + // Memory barrier: any load / call / other store flushes pending. + if (op == Ocall || isload(op)) { + flush(); + continue; + } + + uint32_t val32 = 0; + if (op == Ostorew && get_int_con32(fn, i->arg[0], &val32)) { + Ref base; + int64_t off; + decompose_addr(fn, i->arg[1], &base, &off); + if (tmp_in_align8(a8, base)) { + bool matched = false; + for (auto it = pending.begin(); it != pending.end(); ++it) { + bool prev_is_lower = (it->off + 4 == off) && (it->off & 7) == 0; + bool curr_is_lower = (off + 4 == it->off) && (off & 7) == 0; + // Must be same base + same 32-bit value + lower offset 8B-aligned pair. + if (ref_eq(it->base, base) && it->val == val32 && + (prev_is_lower || curr_is_lower)) { + uint lo_idx = prev_is_lower ? it->idx : k; + uint hi_idx = prev_is_lower ? k : it->idx; + b->ins[lo_idx].op = Ostorel; + if (val32 != 0) { + // Assemble 64-bit duplicated constant. + uint64_t combined = static_cast<uint64_t>(val32) | + (static_cast<uint64_t>(val32) << 32); + b->ins[lo_idx].arg[0] = + getcon(ctx, static_cast<int64_t>(combined), fn); + } + // When val32==0, RCon{0} has identical bit pattern for Kw/Kl. + b->ins[hi_idx] = Ins{}; + b->ins[hi_idx].op = Onop; + pending.erase(it); + merged++; + matched = true; + break; + } + } + if (!matched) + pending.push_back({k, base, off, val32}); + continue; + } + // base not in align8 set -> cannot prove no-alias with pending, flush. + flush(); + continue; + } + + if (isstore(op)) { + flush(); + continue; + } + + // Pure instructions have no memory side-effects, pending untouched. + } + } + + if (debug['M']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "\n> storemrg %s: merged %d storew pairs → storel\n", fn->name, merged); + } +} + +} // namespace rcc |
