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