/* 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 storemrg.cc
/// @brief Fuses adjacent 4B stores into 8B storel.
///
/// See docs/storemrg.md for architectural notes.
#include "rcc/all.h"
#include
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(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& 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 compute_aligned8(Fn* fn) {
std::vector a8(static_cast(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 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;
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(val32) |
(static_cast(val32) << 32);
b->ins[lo_idx].arg[0] =
getcon(ctx, static_cast(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