/* 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 gvn.cc
/// @brief Global Value Numbering (GVN) - Cross-block deduplication + jnz simplification + dead
/// block rescue.
///
/// See docs/gvn.md for Global Value Numbering architectural notes.
#include "rcc/all.h"
namespace rcc {
namespace {
// Instruction hashing.
inline uint mix(uint x0, uint x1) {
return x0 + 31 * x1;
}
inline uint rhash(Ref r) {
return mix(r.type, r.val);
}
uint ihash(Ins* i) {
uint h = mix(i->op, i->cls);
h = mix(h, rhash(i->arg[0]));
h = mix(h, rhash(i->arg[1]));
return h;
}
bool ieq(Ins* a, Ins* b) {
return a->op == b->op && a->cls == b->cls && req(a->arg[0], b->arg[0]) &&
req(a->arg[1], b->arg[1]);
}
// Lookup instruction.
Ins* gvndup(Ctx* ctx, Ins* i, int insert) {
uint idx = ihash(i) % ctx->gvn_tbln;
for (Ins* ii; (ii = ctx->gvn_tbl[idx]);) {
if (ieq(i, ii))
return ii;
if (++idx >= ctx->gvn_tbln)
idx = 0;
}
if (insert)
ctx->gvn_tbl[idx] = i;
return nullptr;
}
// Use replacement.
void replaceuse(Ctx* ctx, Fn* fn, Use* u, Ref r1, Ref r2) {
Tmp* t2 = (rtype(r2) == RTmp) ? &fn->tmp[r2.val] : nullptr;
Blk* b = fn->rpo[u->bid];
switch (u->type) {
case Use::UPhi: {
Phi* p = u->u.phi;
for (Ref* pr = p->arg; pr < &p->arg[p->narg]; ++pr)
if (req(*pr, r1))
*pr = r2;
if (t2)
adduse_phi(ctx, t2, b, p);
break;
}
case Use::UIns: {
Ins* i = u->u.ins;
for (int n = 0; n < 2; ++n)
if (req(i->arg[n], r1))
i->arg[n] = r2;
if (t2)
adduse_ins(ctx, t2, b, i);
break;
}
case Use::UJmp:
if (req(b->jmp.arg, r1))
b->jmp.arg = r2;
if (t2)
adduse_jmp(ctx, t2, b);
break;
case Use::UXXX:
die("unreachable");
}
}
void replaceuses(Ctx* ctx, Fn* fn, Ref r1, Ref r2) {
assert(rtype(r1) == RTmp);
Tmp* t1 = &fn->tmp[r1.val];
for (Use* u = t1->use; u < &t1->use[t1->nuse]; ++u)
replaceuse(ctx, fn, u, r1, r2);
t1->nuse = 0;
}
// Nullify instruction.
void killins(Ctx* ctx, Fn* fn, Ins* i, Ref r) {
replaceuses(ctx, fn, i->to, r);
*i = {};
i->op = Onop;
}
// Handle degraded phi.
void dedupphi(Ctx* ctx, Fn* fn, Blk* b) {
Phi** pp = &b->phi;
for (Phi* p; (p = *pp);) {
Ref r = phicopyref(ctx, fn, b, p);
if (req(r, Ref::none())) {
pp = &p->link;
} else {
replaceuses(ctx, fn, p->to, r);
p->to = Ref::none();
*pp = p->link;
}
}
}
// Instruction normalization.
int rcmp(Ref a, Ref b) {
if (rtype(a) != rtype(b))
return rtype(a) - rtype(b);
return static_cast(a.val) - static_cast(b.val);
}
void normins(Ctx* ctx, Fn* fn, Ins* i) {
int64_t v;
// Truncate to 32 bits.
for (int n = 0; n < 2; ++n)
if (!KWIDE(argcls(i, n)) && isconbits(fn, i->arg[n], &v) && (v & 0xffffffffLL) != v)
i->arg[n] = getcon(ctx, v & 0xffffffffLL, fn);
// Commutative op sort.
if (optab[i->op].commutes && rcmp(i->arg[0], i->arg[1]) > 0) {
Ref r = i->arg[1];
i->arg[1] = i->arg[0];
i->arg[0] = r;
}
}
// Negate constant.
int negcon(int cls, Con* c) {
Con z = {};
z.type = Con::CBits;
z.bits.i = 0;
return foldint(c, Osub, cls, &z, c);
}
// Merge constants.
void assoccon(Ctx* ctx, Fn* fn, Blk* b, Ins* i1) {
int op = (i1->op == Osub) ? Oadd : static_cast(i1->op);
if (!optab[op].assoc || KBASE(i1->cls) != 0 || rtype(i1->arg[0]) != RTmp ||
rtype(i1->arg[1]) != RCon)
return;
Con c1 = fn->con[i1->arg[1].val];
Tmp* t2 = &fn->tmp[i1->arg[0].val];
if (!t2->def)
return;
Ins* i2 = t2->def;
if (op != (i2->op == Osub ? Oadd : static_cast(i2->op)) || rtype(i2->arg[1]) != RCon)
return;
Con c2 = fn->con[i2->arg[1].val];
assert(KBASE(i2->cls) == 0);
assert(KWIDE(i2->cls) >= KWIDE(i1->cls));
// Unify as add.
if (i1->op == Osub && negcon(i1->cls, &c1))
return;
if (i2->op == Osub && negcon(i2->cls, &c2))
return;
Con c;
if (foldint(&c, op, i1->cls, &c1, &c2))
return;
// Revert to sub.
if (op == Oadd && c.type == Con::CBits &&
((i1->cls == Kl && c.bits.i < 0) ||
(i1->cls == Kw && static_cast(c.bits.i) < 0))) {
int fail = negcon(i1->cls, &c);
assert(fail == 0);
(void)fail;
op = Osub;
}
i1->op = static_cast(op);
i1->arg[0] = i2->arg[0];
i1->arg[1] = newcon(ctx, &c, fn);
adduse_ins(ctx, &fn->tmp[i1->arg[0].val], b, i1);
}
// Deduplication.
void dedupins(Ctx* ctx, Fn* fn, Blk* b, Ins* i) {
normins(ctx, fn, i);
if (i->op == Onop || pinned(i))
return;
// sel skip.
assert(i->op != Osel0);
assert(!req(i->to, Ref::none()));
assoccon(ctx, fn, b, i);
Ref r = copyref(ctx, fn, b, i);
if (!req(r, Ref::none())) {
killins(ctx, fn, i, r);
return;
}
r = foldref(ctx, fn, i);
if (!req(r, Ref::none())) {
killins(ctx, fn, i, r);
return;
}
Ins* i1 = gvndup(ctx, i, 1);
if (i1)
killins(ctx, fn, i, i1->to);
}
// jnz simplification.
// Check cmp eq/ne 0.
int cmpeqz(Fn* fn, Ref r, Ref* arg, int* cls, int* eqval) {
if (rtype(r) != RTmp)
return 0;
Ins* i = fn->tmp[r.val].def;
if (i && optab[i->op].cmpeqwl && req(i->arg[1], Ref::con_z())) {
*arg = i->arg[0];
*cls = argcls(i, 0);
*eqval = optab[i->op].eqval;
return 1;
}
return 0;
}
// Check branch dominance.
int branchdom(Fn* fn, Blk* bif, Blk* bbr1, Blk* bbr2, Blk* b) {
assert(bif->jmp.type == Jjnz);
return b != bif && dom(bbr1, b) && !reachesnotvia(fn, bbr2, b, bif);
}
// Check 0-branch.
int domzero(Fn* fn, Blk* d, Blk* b, int* z) {
if (branchdom(fn, d, d->s1, d->s2, b)) {
*z = 0;
return 1;
}
if (branchdom(fn, d, d->s2, d->s1, b)) {
*z = 1;
return 1;
}
return 0;
}
// Actual width class.
int usecls(Use* u, Ref r, int cls) {
switch (u->type) {
case Use::UIns: {
int k = Kx; // widest
if (req(u->u.ins->arg[0], r))
k = argcls(u->u.ins, 0);
if (req(u->u.ins->arg[1], r) && (k == Kx || !KWIDE(k)))
k = argcls(u->u.ins, 1);
return k == Kx ? cls : k;
}
case Use::UPhi:
if (req(u->u.phi->to, Ref::none()))
return cls; // eliminated
return u->u.phi->cls;
case Use::UJmp:
return Kw;
default:
break;
}
die("unreachable");
}
// Replace with 0.
void propjnz0(Ctx* ctx, Fn* fn, Blk* bif, Blk* s0, Blk* snon0, Ref r, int cls) {
if (s0->npred != 1 || rtype(r) != RTmp)
return;
Tmp* t = &fn->tmp[r.val];
for (Use* u = t->use; u < &t->use[t->nuse]; ++u) {
Blk* b = fn->rpo[u->bid];
// Match width.
if (usecls(u, r, cls) == cls && branchdom(fn, bif, s0, snon0, b))
replaceuse(ctx, fn, u, r, Ref::con_z());
}
}
void dedupjmp(Ctx* ctx, Fn* fn, Blk* b) {
if (b->jmp.type != Jjnz)
return;
// Propagate jmp arg.
propjnz0(ctx, fn, b, b->s2, b->s1, b->jmp.arg, Kw);
// Propagate underlying arg.
Ref arg;
int cls, eqval;
if (cmpeqz(fn, b->jmp.arg, &arg, &cls, &eqval)) {
Blk* ps[2] = {b->s1, b->s2};
propjnz0(ctx, fn, b, ps[eqval ^ 1], ps[eqval], arg, cls);
}
// Collapse jnz.
int64_t v = 1;
int z = 0;
if (b->s1 == b->s2 || isconbits(fn, b->jmp.arg, &v) || zeroval(fn, b, b->jmp.arg, Kw, &z)) {
if (v == 0 || z)
b->s1 = b->s2;
b->s2 = nullptr; // Dead branch live instructions are rescued later in rebuildcfg
b->jmp.type = Jjmp;
b->jmp.arg = Ref::none();
}
}
// Rebuild CFG.
void rebuildcfg(Ctx* ctx, Fn* fn) {
uint nblk = fn->nblk;
Blk** rpo = static_cast(emalloc(nblk * sizeof rpo[0]));
memcpy(rpo, fn->rpo, nblk * sizeof rpo[0]);
fillcfg(ctx, fn); // Dead blocks are marked with id == ~0u here
Blk* s = fn->start;
for (uint n = 0; n < nblk; ++n) {
Blk* b = rpo[n];
if (b->id != ~0u)
continue; // Still reachable after GVN, skip
assert(b != s);
for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i)
if (!optab[i->op].pinned && gvndup(ctx, i, 0) == i)
addins(ctx, &s->ins, &s->nins, i);
}
free(rpo);
}
} // namespace
// zeroval infer.
int zeroval(Fn* fn, Blk* b, Ref r, int cls, int* z) {
Ref arg;
int cls1, eqval;
for (Blk* d = b->idom; d; d = d->idom) {
if (d->jmp.type != Jjnz)
continue;
if (req(r, d->jmp.arg) && cls == Kw && domzero(fn, d, b, z))
return 1;
if (cmpeqz(fn, d->jmp.arg, &arg, &cls1, &eqval) && req(r, arg) && cls == cls1 &&
domzero(fn, d, b, z)) {
*z ^= eqval;
return 1;
}
}
return 0;
}
// gvn entry.
void gvn(Ctx* ctx, Fn* fn) {
ctx->gvn_con01[0] = getcon(ctx, 0, fn);
ctx->gvn_con01[1] = getcon(ctx, 1, fn);
// Zero out width memo.
for (Blk* b = fn->start; b; b = b->link)
for (Phi* p = b->phi; p; p = p->link)
p->visit = 0;
fillloop(fn);
narrowpars(ctx, fn);
filluse(ctx, fn);
ssacheck(fn);
uint nins = 0;
for (Blk* b = fn->start; b; b = b->link) {
b->visit = 0;
nins += b->nins;
}
ctx->gvn_tbln = nins + nins / 2;
ctx->gvn_tbl = static_cast(emalloc(ctx->gvn_tbln * sizeof ctx->gvn_tbl[0]));
for (uint n = 0; n < fn->nblk; ++n) {
Blk* b = fn->rpo[n];
dedupphi(ctx, fn, b);
for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i)
dedupins(ctx, fn, b, i);
dedupjmp(ctx, fn, b);
}
rebuildcfg(ctx, fn);
free(ctx->gvn_tbl);
ctx->gvn_tbl = nullptr;
ctx->gvn_tbln = 0;
if (debug['G']) {
fprintf(ctx->dbg_buf, "\n> After GVN:\n");
printfn(fn, ctx->dbg_buf);
}
}
} // namespace rcc