/* 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 alias.cc
/// @brief Alias analysis: computes a conservative Alias descriptor for each tmp, serving downstream
/// load/mem optimizations.
///
/// See docs/alias.md for architectural notes.
#include "rcc/all.h"
#include
namespace rcc {
void getalias(Alias* a, Ref r, Fn* fn) {
switch (rtype(r)) {
case RTmp:
*a = fn->tmp[r.val].alias;
// astack: follow to root slot to get latest type (slot might have been upgraded to AEsc
// by esc())
if (astack(a->type))
a->type = a->slot->type;
assert(a->type != Alias::ABot);
return;
case RCon: {
Con* c = &fn->con[r.val];
if (c->type == Con::CAddr) {
a->type = Alias::ASym;
a->u.sym = c->sym;
} else {
a->type = Alias::ACon;
}
a->offset = c->bits.i;
a->offset_unknown = false;
a->slot = nullptr;
a->base = 0;
return;
}
default:
die("getalias: unreachable ref kind");
}
}
int alias(Ref p, int op, int sp, Ref q, int sq, int* delta, Fn* fn) {
Alias ap, aq;
getalias(&ap, p, fn);
getalias(&aq, q, fn);
ap.offset += op;
// sp, sq are guaranteed by caller to be <= 2^28, offset difference won't overflow int
*delta = static_cast(ap.offset - aq.offset);
bool ovlap = ap.offset < aq.offset + sq && aq.offset < ap.offset + sp;
// When offset_unknown, overlap is untrustworthy, all "same base + overlap" judgements must
// degrade
bool ovlap_known = !ap.offset_unknown && !aq.offset_unknown;
bool p_stack = astack(ap.type);
bool q_stack = astack(aq.type);
if (p_stack && q_stack)
return (ap.base == aq.base && ovlap) ? MustAlias : NoAlias;
if (ap.type == Alias::ASym && aq.type == Alias::ASym) {
if (!symeq(ap.u.sym, aq.u.sym))
return NoAlias;
if (!ovlap_known)
return MayAlias;
return ovlap ? MustAlias : NoAlias;
}
bool both_con = (ap.type == Alias::ACon && aq.type == Alias::ACon);
bool same_base = (ap.type == aq.type && ap.base == aq.base);
if (both_con || same_base) {
assert(ap.type == Alias::ACon || ap.type == Alias::AUnk);
if (!ovlap_known)
return MayAlias;
return ovlap ? MustAlias : NoAlias;
}
if (ap.type == Alias::AUnk && aq.type != Alias::ALoc)
return MayAlias;
if (aq.type == Alias::AUnk && ap.type != Alias::ALoc)
return MayAlias;
return NoAlias;
}
int escapes(Ref r, Fn* fn) {
if (rtype(r) != RTmp)
return 1;
Alias* a = &fn->tmp[r.val].alias;
// astack and slot is still ALoc -> didn't escape; otherwise (not astack or slot is AEsc) it
// escapes
return !astack(a->type) || a->slot->type == Alias::AEsc;
}
namespace {
void esc(Ref r, Fn* fn) {
assert(rtype(r) <= RType);
if (rtype(r) != RTmp)
return;
Alias* a = &fn->tmp[r.val].alias;
if (astack(a->type))
a->slot->type = Alias::AEsc;
}
void store(Ref r, int sz, Fn* fn) {
if (rtype(r) != RTmp)
return;
Alias* a = &fn->tmp[r.val].alias;
if (!a->slot)
return;
assert(astack(a->type));
int64_t off = a->offset;
bits m;
if (sz >= static_cast(NBit) || off < 0 || off >= static_cast(NBit))
m = static_cast(-1);
else
m = (BIT(sz) - 1) << off;
a->slot->u.loc.m |= m;
}
bool getalias_safe(Alias* a, Ref r, Fn* fn) {
switch (rtype(r)) {
case RTmp:
*a = fn->tmp[r.val].alias;
if (a->type == Alias::ABot)
return false;
if (astack(a->type))
a->type = a->slot->type;
return true;
case RCon: {
Con* c = &fn->con[r.val];
if (c->type == Con::CAddr) {
a->type = Alias::ASym;
a->u.sym = c->sym;
} else {
a->type = Alias::ACon;
}
a->offset = c->bits.i;
a->offset_unknown = false;
a->slot = nullptr;
a->base = 0;
return true;
}
default:
die("getalias_safe: unreachable ref kind");
}
}
bool alias_eq(const Alias& a, const Alias& b) {
if (a.type != b.type)
return false;
if (a.offset != b.offset)
return false;
if (a.offset_unknown != b.offset_unknown)
return false;
if (a.slot != b.slot)
return false;
if (a.base != b.base)
return false;
if (a.type == Alias::ASym) {
if (a.u.sym.type != b.u.sym.type)
return false;
if (a.u.sym.id != b.u.sym.id)
return false;
} else if (astack(a.type)) {
if (a.u.loc.sz != b.u.loc.sz)
return false;
if (a.u.loc.m != b.u.loc.m)
return false;
}
return true;
}
bool alias_meet(Alias* a, const Alias& b) {
Alias old = *a;
if (a->type == Alias::ABot) {
*a = b;
} else if (b.type == Alias::ABot) {
// Unchanged
} else {
// Same type + same identity: offset compatible
bool same_id = false;
if (a->type == b.type) {
if (astack(a->type))
same_id = (a->slot == b.slot);
else if (a->type == Alias::ASym)
same_id = symeq(a->u.sym, b.u.sym);
else if (a->type == Alias::ACon)
same_id = true; // ACon has no base/sym, purely based on offset
else if (a->type == Alias::AUnk)
same_id = (a->base == b.base);
}
if (same_id) {
// ASym path legitimately accepts offset_unknown; ALoc/AEsc do not (store-mask
// assumes offset is known), degrade to AUnk on offset mismatch.
bool both_offset_known = !a->offset_unknown && !b.offset_unknown;
bool offset_match = both_offset_known && a->offset == b.offset;
if (astack(a->type) && !offset_match) {
a->type = Alias::AUnk;
a->slot = nullptr;
a->offset = 0;
a->offset_unknown = false;
// base fixed by caller
} else {
if (!offset_match)
a->offset_unknown = true;
}
} else {
// Type or identity mismatch -> AUnk
a->type = Alias::AUnk;
a->slot = nullptr;
a->offset = 0;
a->offset_unknown = false;
// base fixed by caller
}
}
return !alias_eq(*a, old);
}
bool compute_phi(Phi* p, Fn* fn) {
assert(rtype(p->to) == RTmp);
Alias* a = &fn->tmp[p->to.val].alias;
Alias old = *a;
Alias acc;
std::memset(&acc, 0, sizeof acc);
acc.type = Alias::ABot;
for (uint k = 0; k < p->narg; k++) {
Alias arg;
if (!getalias_safe(&arg, p->arg[k], fn))
continue; // This arg is still ABot, skip and wait for next round
alias_meet(&acc, arg);
}
if (acc.type == Alias::ABot) {
// All args still ABot: retain ABot, doesn't count as change
return false;
}
// If meet degrades to AUnk, set base to phi's own tmp id (uniqueness, avoids sharing base with
// other AUnks)
if (acc.type == Alias::AUnk)
acc.base = p->to.val;
if (!alias_eq(*a, acc)) {
*a = acc;
return true;
}
(void)old;
return false;
}
struct InsResult {
bool consumed_next;
bool changed;
};
InsResult process_ins(Ins* i, Ins* end, Fn* fn) {
InsResult res{false, false};
Alias* a = nullptr;
Alias old;
bool has_def = !req(i->to, Ref::none());
if (has_def) {
assert(rtype(i->to) == RTmp);
a = &fn->tmp[i->to.val].alias;
old = *a;
if (isalloc(i->op)) {
// alloc: install only on first time (ABot), afterwards type might upgrade to AEsc via
// esc, but slot pointer, loc.sz, loc.m must be preserved (loc.m accumulates with OR
// across rounds).
if (a->type == Alias::ABot) {
a->type = Alias::ALoc;
a->slot = a; // Self reference = this tmp is the root slot
a->u.loc.sz = -1; // Unknown size placeholder
a->u.loc.m = 0;
if (rtype(i->arg[0]) == RCon) {
Con* c = &fn->con[i->arg[0].val];
int64_t x = c->bits.i;
if (c->type == Con::CBits && 0 <= x && x <= static_cast(NBit))
a->u.loc.sz = static_cast(x);
}
a->base = i->to.val;
a->offset = 0;
a->offset_unknown = false;
}
} else if (i->op == Ocopy || i->op == Oaddr) {
// Oaddr passes through alias: IRGen wraps `$sym` as `%t = addr $sym`, here
// we must propagate ASym identity from RCon CAddr to %t, otherwise alias() sees
// load/store addr as AUnk Tmp, losing "different globals don't alias".
Alias src;
if (getalias_safe(&src, i->arg[0], fn)) {
*a = src;
}
// src still ABot: don't touch *a, wait for next round
} else if (i->op == Oadd) {
Alias a0, a1;
bool ok0 = getalias_safe(&a0, i->arg[0], fn);
bool ok1 = getalias_safe(&a1, i->arg[1], fn);
if (ok0 && ok1) {
Alias newval;
std::memset(&newval, 0, sizeof newval);
newval.type = Alias::AUnk; // Default
newval.base = i->to.val;
if (a0.type == Alias::ACon) {
newval = a1;
newval.offset += a0.offset;
} else if (a1.type == Alias::ACon) {
newval = a0;
newval.offset += a1.offset;
} else if (a0.type == Alias::ASym) {
newval = a0;
newval.offset_unknown = true;
} else if (a1.type == Alias::ASym) {
newval = a1;
newval.offset_unknown = true;
}
// Both are neither ACon nor ASym -> keep default AUnk
*a = newval;
}
// Any operand still ABot: don't touch *a, wait for next round
} else {
// Other ops: def is always AUnk, one-time init
if (a->type == Alias::ABot) {
a->type = Alias::AUnk;
a->slot = nullptr;
a->base = i->to.val;
a->offset = 0;
a->offset_unknown = false;
}
}
if (!alias_eq(*a, old))
res.changed = true;
}
// 3. esc mark: when no def or def is already AUnk, arg is treated as escaping local
// abstraction.
// Skip on ABot -- def undetermined, decide next round.
// Multiple iterations: esc is monotonically accumulated (slot type one-way ALoc->AEsc),
// idempotent.
bool def_known_unk = has_def && a->type == Alias::AUnk;
if (!has_def || def_known_unk) {
if (i->op != Oblit0) {
if (!isload(i->op))
esc(i->arg[0], fn);
if (!isstore(i->op) && i->op != Oargc)
esc(i->arg[1], fn);
}
}
// 4. Memory write accounting: Oblit0/Oblit1 pair / normal store (multiple rounds OR accumulate
// idempotent)
if (i->op == Oblit0) {
Ins* i1 = i + 1;
assert(i1 < end && i1->op == Oblit1);
(void)end;
assert(rtype(i1->arg[0]) == RInt);
int sz = std::abs(rsval(i1->arg[0]));
store(i->arg[1], sz, fn);
res.consumed_next = true;
}
if (isstore(i->op))
store(i->arg[1], storesz(i), fn);
return res;
}
void escape_phi_args(Fn* fn) {
for (Blk* b = fn->start; b; b = b->link)
for (Phi* p = b->phi; p; p = p->link)
for (uint n = 0; n < p->narg; n++)
esc(p->arg[n], fn);
}
} // namespace
void fillalias(Fn* fn) {
for (int t = 0; t < fn->ntmp; t++) {
std::memset(&fn->tmp[t].alias, 0, sizeof(Alias));
fn->tmp[t].alias.type = Alias::ABot;
}
int iter = 0;
bool changed = true;
for (; iter < 16 && changed; ++iter) {
changed = false;
for (uint n = 0; n < fn->nblk; ++n) {
Blk* b = fn->rpo[n];
for (Phi* p = b->phi; p; p = p->link)
if (compute_phi(p, fn))
changed = true;
for (Ins* i = b->ins; i < &b->ins[b->nins];) {
InsResult r = process_ins(i, &b->ins[b->nins], fn);
if (r.changed)
changed = true;
i += r.consumed_next ? 2 : 1;
}
// Block tail jump arg considered escaped (Jretc exception: ret-by-copy pointer doesn't
// escape). Multiple rounds esc idempotent.
if (b->jmp.type != Jretc)
esc(b->jmp.arg, fn);
}
}
for (int t = 0; t < fn->ntmp; t++) {
Alias* a = &fn->tmp[t].alias;
if (a->type == Alias::ABot) {
a->type = Alias::AUnk;
a->base = t;
a->offset = 0;
a->offset_unknown = false;
a->slot = nullptr;
}
}
escape_phi_args(fn);
}
} // namespace rcc