diff options
Diffstat (limited to 'src')
48 files changed, 19991 insertions, 0 deletions
diff --git a/src/backend/emit.cc b/src/backend/emit.cc new file mode 100644 index 0000000..6e1f971 --- /dev/null +++ b/src/backend/emit.cc @@ -0,0 +1,304 @@ +/* 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 backend/emit.cc +/// @brief Target-independent assembly/data/debug section output tools. + +#include "rcc/all.h" + +#include <cinttypes> +#include <cstring> +#include <vector> + +namespace rcc { + +// Section types. + +namespace { + +enum Sec : int { kSecText = 0, kSecData = 1, kSecBss = 2 }; + +// Given (sec, thread), return the name/instruction string after .section. +// When thread=true, SecText is unreachable. +const char* section_name(int sec, bool thread) { + if (!thread) { + switch (sec) { + case kSecText: + return ".text"; + case kSecData: + return ".data"; + case kSecBss: + return ".bss"; + } + } else { + switch (sec) { + case kSecText: + return ".abort \"unreachable\""; + case kSecData: + return ".section .tdata,\"awT\""; + case kSecBss: + return ".section .tbss,\"awT\""; + } + } + return nullptr; // unreachable +} + +// Write ".globl / label / section switch" header. Apple branch has TLV trampolines. +void write_lnk(char* n, Lnk* l, int sec_kind, FILE* f) { + const char* pfx = (n[0] == '"') ? "" : T.assym; + const char* sfx = ""; + + if (T.apple && l->thread) { + l->sec = const_cast<char*>("__DATA"); + l->secf = const_cast<char*>("__thread_data,thread_local_regular"); + sfx = "$tlv$init"; + fputs(".section __DATA,__thread_vars,thread_local_variables\n", f); + fprintf(f, "%s%s:\n", pfx, n); + fprintf(f, + "\t.quad __tlv_bootstrap\n" + "\t.quad 0\n" + "\t.quad %s%s%s\n\n", + pfx, n, sfx); + } + + if (l->sec) { + fprintf(f, ".section %s", l->sec); + if (l->secf) + fprintf(f, ",%s", l->secf); + } else { + fputs(section_name(sec_kind, l->thread != 0), f); + } + fputc('\n', f); + + if (l->align) + fprintf(f, ".balign %d\n", l->align); + if (l->export_) + fprintf(f, ".globl %s%s\n", pfx, n); + fprintf(f, "%s%s%s:\n", pfx, n, sfx); +} + +} // namespace + +void emitfnlnk(char* n, Lnk* l, FILE* f) { + write_lnk(n, l, kSecText, f); +} + +// Data sections. + +namespace { +int64_t g_data_zero = 0; + +struct DataDecl { + const char* decl; + int64_t mask; +}; + +constexpr DataDecl kDataDecl[] = { + /* DStart */ {nullptr, 0}, + /* DEnd */ {nullptr, 0}, + /* DB */ {"\t.byte", 0xffLL}, + /* DH */ {"\t.short", 0xffffLL}, + /* DW */ {"\t.int", 0xffffffffLL}, + /* DL */ {"\t.quad", -1LL}, + /* DZ */ {nullptr, 0}, +}; + +} // namespace + +void emitdat(Dat* d, FILE* f) { + switch (d->type) { + case Dat::DStart: + g_data_zero = 0; + return; + + case Dat::DEnd: + if (d->lnk->common) { + if (g_data_zero == -1) + die("invalid common data definition"); + const char* p = (d->name[0] == '"') ? "" : T.assym; + fprintf(f, ".comm %s%s,%" PRId64, p, d->name, g_data_zero); + if (d->lnk->align) + fprintf(f, ",%d", d->lnk->align); + fputc('\n', f); + } else if (g_data_zero != -1) { + write_lnk(d->name, d->lnk, kSecBss, f); + fprintf(f, "\t.fill %" PRId64 ",1,0\n", g_data_zero); + } + return; + + case Dat::DZ: + if (g_data_zero != -1) + g_data_zero += d->u.num; + else + fprintf(f, "\t.fill %" PRId64 ",1,0\n", d->u.num); + return; + + case Dat::DB: + case Dat::DH: + case Dat::DW: + case Dat::DL: + break; + } + + // Switch to .data and flush accumulated zeros on first non-zero content. + if (g_data_zero != -1) { + write_lnk(d->name, d->lnk, kSecData, f); + if (g_data_zero > 0) + fprintf(f, "\t.fill %" PRId64 ",1,0\n", g_data_zero); + g_data_zero = -1; + } + + const DataDecl& dd = kDataDecl[d->type]; + if (d->isstr) { + if (d->type != Dat::DB) + err(const_cast<char*>("strings only supported for 'b' currently")); + fprintf(f, "\t.ascii %s\n", d->u.str); + } else if (d->isref) { + const char* p = (d->u.ref.name[0] == '"') ? "" : T.assym; + fprintf(f, "%s %s%s%+" PRId64 "\n", dd.decl, p, d->u.ref.name, d->u.ref.off); + } else { + fprintf(f, "%s %" PRId64 "\n", dd.decl, d->u.num & dd.mask); + } +} + +// FP constant pool (stashbits + emitfin). + +struct Asmbits { + bits n; + int size; + Asmbits* link; +}; + +int stashbits(Ctx* ctx, bits n, int size) { + assert(size == 4 || size == 8 || size == 16); + Asmbits** pb; + Asmbits* b; + int i = 0; + for (pb = &ctx->emit_stash; (b = *pb); pb = &b->link, ++i) { + if (size <= b->size && b->n == n) + return i; + } + b = static_cast<Asmbits*>(emalloc(sizeof(Asmbits))); + b->n = n; + b->size = size; + b->link = nullptr; + *pb = b; + return i; +} + +namespace { + +// Layout by .quad / .quad 0 (16B) / .quad (8B) / .int (4B). +void emit_fp_pool(Ctx* ctx, FILE* f, const char* sec[3]) { + if (!ctx->emit_stash) + return; + fprintf(f, "/* floating point constants */\n"); + + union { + int32_t i; + float f; + } u; + + // Three passes from largest to smallest (16, 8, 4). + for (int lg = 4; lg >= 2; --lg) { + int width = 1 << lg; + int i = 0; + for (Asmbits* b = ctx->emit_stash; b; b = b->link, ++i) { + if (b->size != width) + continue; + fprintf(f, + ".section %s\n" + ".p2align %d\n" + "%sfp%d:", + sec[lg - 2], lg, T.asloc, i); + if (lg == 4) { + fprintf(f, "\n\t.quad %" PRId64 "\n\t.quad 0\n\n", static_cast<int64_t>(b->n)); + } else if (lg == 3) { + double dv; + memcpy(&dv, &b->n, sizeof(dv)); + fprintf(f, "\n\t.quad %" PRId64 " /* %f */\n\n", static_cast<int64_t>(b->n), dv); + } else /* lg == 2 */ { + u.i = static_cast<int32_t>(b->n); + fprintf(f, "\n\t.int %" PRId32 " /* %f */\n\n", u.i, static_cast<double>(u.f)); + } + } + } + + // Free linked list. + while (Asmbits* b = ctx->emit_stash) { + ctx->emit_stash = b->link; + free(b); + } +} + +} // namespace + +void elf_emitfin(Ctx* ctx, FILE* f) { + static const char* sec[3] = {".rodata", ".rodata", ".rodata"}; + emit_fp_pool(ctx, f, sec); + fprintf(f, ".section .note.GNU-stack,\"\",@progbits\n"); +} + +void elf_emitfnfin(char* fn, FILE* f) { + fprintf(f, ".type %s, @function\n", fn); + fprintf(f, ".size %s, .-%s\n", fn, fn); +} + +void macho_emitfin(Ctx* ctx, FILE* f) { + static const char* sec[3] = { + "__TEXT,__literal4,4byte_literals", + "__TEXT,__literal8,8byte_literals", + "__TEXT,__literal16,16byte_literals", + }; + emit_fp_pool(ctx, f, sec); +} + +void pe_emitfin(Ctx* ctx, FILE* f) { + static const char* sec[3] = {".rodata", ".rodata", ".rodata"}; + emit_fp_pool(ctx, f, sec); +} + +// DWARF .file / .loc. + +namespace { +std::vector<uint32_t> g_dbg_files; // intern id sequence. +uint32_t g_dbg_curfile = 0; // 1-based (gas requires > 0). +} // namespace + +void emitdbgfile(char* fn, FILE* f) { + uint32_t id = intern(fn); + for (size_t i = 0; i < g_dbg_files.size(); ++i) { + if (g_dbg_files[i] == id) { + g_dbg_curfile = static_cast<uint32_t>(i + 1); + return; + } + } + g_dbg_files.push_back(id); + g_dbg_curfile = static_cast<uint32_t>(g_dbg_files.size()); + fprintf(f, ".file %u %s\n", g_dbg_curfile, fn); +} + +void emitdbgloc(uint line, uint col, FILE* f) { + if (col != 0) + fprintf(f, "\t.loc %u %u %u\n", g_dbg_curfile, line, col); + else + fprintf(f, "\t.loc %u %u\n", g_dbg_curfile, line); +} + +} // namespace rcc diff --git a/src/backend/peephole.cc b/src/backend/peephole.cc new file mode 100644 index 0000000..0c5e95f --- /dev/null +++ b/src/backend/peephole.cc @@ -0,0 +1,146 @@ +/* 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 peephole.cc +/// @brief Backend sliding-window peephole pass. + +#include "rcc/all.h" + +namespace rcc { + +namespace { + +// Store op to byte width. +int store_size(int op) { + switch (op) { + case Ostoreb: + return 1; + case Ostoreh: + return 2; + case Ostorew: + case Ostores: + return 4; + case Ostorel: + case Ostored: + return 8; + default: + return 0; + } +} + +// Check if store and load are compatible for forwarding. +bool store_load_compatible(int sop, int lcls) { + switch (sop) { + case Ostorew: + return lcls == Kw; + case Ostorel: + return lcls == Kl; + case Ostores: + return lcls == Ks; + case Ostored: + return lcls == Kd; + default: + return false; + } +} + +void peep_blk(Blk* b) { + uint n = b->nins; + if (n == 0) + return; + + // Self Ocopy elimination. + for (uint k = 0; k < n; k++) { + Ins* i = &b->ins[k]; + if (i->op == Ocopy && req(i->to, i->arg[0]) && rtype(i->to) != -1) + i->op = Onop; + } + + // Window 2 mode. + auto next_live = [&](uint from) -> int { + for (uint k = from; k < n; k++) + if (b->ins[k].op != Onop) + return static_cast<int>(k); + return -1; + }; + + int p = next_live(0); + while (p >= 0) { + int q = next_live(static_cast<uint>(p) + 1); + if (q < 0) + break; + Ins* pi = &b->ins[p]; + Ins* qi = &b->ins[q]; + + // Store + Load to same slot: keep store, change load to copy. + if (isstore(pi->op) && qi->op == Oload && rtype(pi->arg[1]) == RSlot && + rtype(qi->arg[0]) == RSlot && req(pi->arg[1], qi->arg[0]) && + store_load_compatible(pi->op, qi->cls) && rtype(pi->arg[0]) != RCon) { + // Skip forwarding if value is a constant. + qi->op = Ocopy; + qi->arg[0] = pi->arg[0]; + qi->arg[1] = Ref::none(); + // Keep cls and to. + p = q; + continue; + } + + // Consecutive stores to same slot: drop first. + if (isstore(pi->op) && pi->op == qi->op && rtype(pi->arg[1]) == RSlot && + rtype(qi->arg[1]) == RSlot && req(pi->arg[1], qi->arg[1]) && store_size(pi->op) > 0) { + pi->op = Onop; + p = q; + continue; + } + + p = q; + } + + // Redundant Oextsw elimination. + for (uint pidx = 0; pidx < n; pidx++) { + Ins* pi = &b->ins[pidx]; + if (pi->op != Oextsw) + continue; + if (!isreg(pi->to) || !isreg(pi->arg[0])) + continue; + for (uint qidx = pidx + 1; qidx < n; qidx++) { + Ins* qi = &b->ins[qidx]; + if (qi->op == Onop) + continue; + // Identical Oextsw. + if (qi->op == pi->op && req(qi->to, pi->to) && req(qi->arg[0], pi->arg[0]) && + req(qi->arg[1], pi->arg[1])) { + qi->op = Onop; + continue; + } + // Anchor invalidated. + if (isreg(qi->to) && (req(qi->to, pi->to) || req(qi->to, pi->arg[0]))) + break; + } + } +} + +} // namespace + +void peephole(Ctx* /*ctx*/, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + peep_blk(b); +} + +} // namespace rcc diff --git a/src/backend/rega.cc b/src/backend/rega.cc new file mode 100644 index 0000000..e4f9da1 --- /dev/null +++ b/src/backend/rega.cc @@ -0,0 +1,684 @@ +/* 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 backend/rega.cc +/// @brief Register allocation pass. + +#include "rcc/all.h" + +#include <algorithm> +#include <climits> +#include <cstdio> +#include <cstring> + +namespace rcc { + +namespace { + +#ifdef TEST_PMOV +#undef assert +#define assert(x) assert_test(#x, x) +#endif + +struct RMap { + int t[Tmp0]; + int r[Tmp0]; + int w[Tmp0]; /* wait list, for unmatched hints */ + BSet b[1]; + int n; +}; + +int* hint(Ctx* ctx, int t) { + return &ctx->rega_tmp[phicls(t, ctx->rega_tmp)].hint.r; +} + +void sethint(Ctx* ctx, int t, int r) { + Tmp* p; + + p = &ctx->rega_tmp[phicls(t, ctx->rega_tmp)]; + if (p->hint.r == -1 || p->hint.w > ctx->rega_loop) { + p->hint.r = r; + p->hint.w = ctx->rega_loop; + ctx->rega_tmp[t].visit = -1; + } +} + +void rcopy(RMap* ma, RMap* mb) { + memcpy(ma->t, mb->t, sizeof ma->t); + memcpy(ma->r, mb->r, sizeof ma->r); + memcpy(ma->w, mb->w, sizeof ma->w); + bscopy(ma->b, mb->b); + ma->n = mb->n; +} + +int rfind(RMap* m, int t) { + int i; + + for (i = 0; i < m->n; i++) + if (m->t[i] == t) + return m->r[i]; + return -1; +} + +Ref rref(Ctx* ctx, RMap* m, int t) { + int r, s; + + r = rfind(m, t); + if (r == -1) { + s = ctx->rega_tmp[t].slot; + assert(s != -1 && "should have spilled"); + return Ref::slot(s); + } else + return Ref::tmp(static_cast<uint>(r)); +} + +void radd(Ctx* ctx, RMap* m, int t, int r) { + assert((t >= Tmp0 || t == r) && "invalid temporary"); + assert(((T.gpr0 <= r && r < T.gpr0 + T.ngpr) || (T.fpr0 <= r && r < T.fpr0 + T.nfpr)) && + "invalid register"); + assert(!bshas(m->b, static_cast<uint>(t)) && "temporary has mapping"); + assert(!bshas(m->b, static_cast<uint>(r)) && "register already allocated"); + assert(m->n <= T.ngpr + T.nfpr && "too many mappings"); + bsset(m->b, static_cast<uint>(t)); + bsset(m->b, static_cast<uint>(r)); + m->t[m->n] = t; + m->r[m->n] = r; + m->n++; + ctx->rega_regu |= BIT(r); +} + +Ref ralloctry(Ctx* ctx, RMap* m, int t, int try_) { + bits regs; + int h, r, r0, r1; + + if (t < Tmp0) { + assert(bshas(m->b, static_cast<uint>(t))); + return Ref::tmp(static_cast<uint>(t)); + } + if (bshas(m->b, static_cast<uint>(t))) { + r = rfind(m, t); + assert(r != -1); + return Ref::tmp(static_cast<uint>(r)); + } + r = ctx->rega_tmp[t].visit; + if (r == -1 || bshas(m->b, static_cast<uint>(r))) + r = *hint(ctx, t); + if (r == -1 || bshas(m->b, static_cast<uint>(r))) { + if (try_) + return Ref::none(); + regs = ctx->rega_tmp[phicls(t, ctx->rega_tmp)].hint.m; + regs |= m->b->t[0]; + if (KBASE(ctx->rega_tmp[t].cls) == 0) { + r0 = T.gpr0; + r1 = r0 + T.ngpr; + } else { + r0 = T.fpr0; + r1 = r0 + T.nfpr; + } + for (r = r0; r < r1; r++) + if (!(regs & BIT(r))) + goto Found; + for (r = r0; r < r1; r++) + if (!bshas(m->b, static_cast<uint>(r))) + goto Found; + fprintf(stderr, "DBG no-more-regs: t=%d cls=%d range=[R%d,R%d) m->n=%d\n", t, + ctx->rega_tmp[t].cls, r0, r1, m->n); + fprintf(stderr, " m->t/r entries:\n"); + for (int idx = 0; idx < m->n; ++idx) + fprintf(stderr, " [%d] t=%d -> r=R%d\n", idx, m->t[idx], m->r[idx]); + fprintf(stderr, " m->b set bits:"); + for (int rr = 0; rr < 64; ++rr) + if (bshas(m->b, static_cast<uint>(rr))) + fprintf(stderr, " %d", rr); + fprintf(stderr, "\n"); + err(const_cast<char*>("no more regs")); + } +Found: + radd(ctx, m, t, r); + sethint(ctx, t, r); + ctx->rega_tmp[t].visit = r; + h = *hint(ctx, t); + if (h != -1 && h != r) + m->w[h] = t; + return Ref::tmp(static_cast<uint>(r)); +} + +inline Ref ralloc(Ctx* ctx, RMap* m, int t) { + return ralloctry(ctx, m, t, 0); +} + +int rfree(RMap* m, int t) { + int i, r; + + assert(t >= Tmp0 || !(BIT(t) & T.rglob)); + if (!bshas(m->b, static_cast<uint>(t))) + return -1; + for (i = 0; m->t[i] != t; i++) + assert(i + 1 < m->n); + r = m->r[i]; + bsclr(m->b, static_cast<uint>(t)); + bsclr(m->b, static_cast<uint>(r)); + m->n--; + memmove(&m->t[i], &m->t[i + 1], static_cast<size_t>(m->n - i) * sizeof m->t[0]); + memmove(&m->r[i], &m->r[i + 1], static_cast<size_t>(m->n - i) * sizeof m->r[0]); + assert(t >= Tmp0 || t == r); + return r; +} + +void mdump(Ctx* ctx, RMap* m) { + int i; + + for (i = 0; i < m->n; i++) + if (m->t[i] >= Tmp0) + fprintf(ctx->dbg_buf, " (%s, R%d)", ctx->rega_tmp[m->t[i]].name, m->r[i]); + fprintf(ctx->dbg_buf, "\n"); +} + +void pmadd(Ctx* ctx, Ref src, Ref dst, int k) { + if (ctx->rega_npm == NPm) + err(const_cast<char*>("no more pm slots")); + ctx->rega_pm[ctx->rega_npm].src = src; + ctx->rega_pm[ctx->rega_npm].dst = dst; + ctx->rega_pm[ctx->rega_npm].cls = k; + ctx->rega_npm++; +} + +enum PMStat { ToMove, Moving, Moved }; + +int pmrec(Ctx* ctx, enum PMStat* status, int i, int* k) { + int j, c; + + /* note, this routine might emit + * too many large instructions + */ + if (req(ctx->rega_pm[i].src, ctx->rega_pm[i].dst)) { + status[i] = Moved; + return -1; + } + assert(KBASE(ctx->rega_pm[i].cls) == KBASE(*k)); + assert((Kw | Kl) == Kl && (Ks | Kd) == Kd); + *k |= ctx->rega_pm[i].cls; + for (j = 0; j < ctx->rega_npm; j++) + if (req(ctx->rega_pm[j].dst, ctx->rega_pm[i].src)) + break; + switch (j == ctx->rega_npm ? Moved : status[j]) { + case Moving: + c = j; /* start of cycle */ + emit(ctx, Oswap, *k, Ref::none(), ctx->rega_pm[i].src, ctx->rega_pm[i].dst); + break; + case ToMove: + status[i] = Moving; + c = pmrec(ctx, status, j, k); + if (c == i) { + c = -1; /* end of cycle */ + break; + } + if (c != -1) { + emit(ctx, Oswap, *k, Ref::none(), ctx->rega_pm[i].src, ctx->rega_pm[i].dst); + break; + } + [[fallthrough]]; + case Moved: + c = -1; + emit(ctx, Ocopy, ctx->rega_pm[i].cls, ctx->rega_pm[i].dst, ctx->rega_pm[i].src, + Ref::none()); + break; + default: + die("unreachable"); + } + status[i] = Moved; + return c; +} + +void pmgen(Ctx* ctx) { + int i; + enum PMStat* status; + + status = static_cast<enum PMStat*>( + alloc(ctx, static_cast<size_t>(ctx->rega_npm) * sizeof status[0])); + assert(!ctx->rega_npm || status[ctx->rega_npm - 1] == ToMove); + for (i = 0; i < ctx->rega_npm; i++) + if (status[i] == ToMove) { + int k = ctx->rega_pm[i].cls; + pmrec(ctx, status, i, &k); + } +} + +void move(Ctx* ctx, int r, Ref to, RMap* m) { + int n, t, r1; + + r1 = req(to, Ref::none()) ? -1 : rfree(m, static_cast<int>(to.val)); + if (bshas(m->b, static_cast<uint>(r))) { + /* r is used and not by to */ + assert(r1 != r); + for (n = 0; m->r[n] != r; n++) + assert(n + 1 < m->n); + t = m->t[n]; + rfree(m, t); + bsset(m->b, static_cast<uint>(r)); + ralloc(ctx, m, t); + bsclr(m->b, static_cast<uint>(r)); + } + t = req(to, Ref::none()) ? r : static_cast<int>(to.val); + radd(ctx, m, t, r); +} + +int regcpy(Ins* i) { + return i->op == Ocopy && isreg(i->arg[0]); +} + +Ins* dopm(Ctx* ctx, Blk* b, Ins* i, RMap* m) { + RMap m0; + int n, r, r1, t, s; + Ins *i1, *ip; + bits def; + + m0 = *m; /* okay since we don't use m0.b */ + m0.b->t = 0; + i1 = ++i; + do { + i--; + move(ctx, static_cast<int>(i->arg[0].val), i->to, m); + } while (i != b->ins && regcpy(i - 1)); + assert(m0.n <= m->n); + if (i != b->ins && (i - 1)->op == Ocall) { + def = T.retregs((i - 1)->arg[1], 0) | T.rglob; + for (r = 0; T.rsave[r] >= 0; r++) + if (!(BIT(T.rsave[r]) & def)) + move(ctx, T.rsave[r], Ref::none(), m); + } + for (ctx->rega_npm = 0, n = 0; n < m->n; n++) { + t = m->t[n]; + s = ctx->rega_tmp[t].slot; + r1 = m->r[n]; + r = rfind(&m0, t); + if (r != -1) + pmadd(ctx, Ref::tmp(static_cast<uint>(r1)), Ref::tmp(static_cast<uint>(r)), + ctx->rega_tmp[t].cls); + else if (s != -1) + pmadd(ctx, Ref::tmp(static_cast<uint>(r1)), Ref::slot(s), ctx->rega_tmp[t].cls); + } + for (ip = i; ip < i1; ip++) { + if (!req(ip->to, Ref::none())) + rfree(m, static_cast<int>(ip->to.val)); + r = static_cast<int>(ip->arg[0].val); + if (rfind(m, r) == -1) + radd(ctx, m, r, r); + } + pmgen(ctx); + return i; +} + +int prio1(Ctx* ctx, Ref r1, Ref r2) { + /* trivial heuristic to begin with, + * later we can use the distance to + * the definition instruction + */ + (void)r2; + return *hint(ctx, static_cast<int>(r1.val)) != -1; +} + +void insert(Ctx* ctx, Ref* r, Ref** rs, int p) { + int i; + + rs[i = p] = r; + while (i-- > 0 && prio1(ctx, *r, *rs[i])) { + rs[i + 1] = rs[i]; + rs[i] = r; + } +} + +void doblk(Ctx* ctx, Blk* b, RMap* cur) { + int t, x, r, rf, rt, nr; + bits rs; + Ins *i, *i1; + Mem* m; + Ref* ra[4]; + + if (rtype(b->jmp.arg) == RTmp) + b->jmp.arg = ralloc(ctx, cur, static_cast<int>(b->jmp.arg.val)); + if (rtype(b->jmp.arg2) == RTmp) + b->jmp.arg2 = ralloc(ctx, cur, static_cast<int>(b->jmp.arg2.val)); + ctx->curi = &ctx->insb[NIns]; + for (i1 = &b->ins[b->nins]; i1 != b->ins;) { + emiti(ctx, *--i1); + i = ctx->curi; + rf = -1; + switch (i->op) { + case Ocall: + rs = T.argregs(i->arg[1], 0) | T.rglob; + for (r = 0; T.rsave[r] >= 0; r++) + if (!(BIT(T.rsave[r]) & rs)) + rfree(cur, T.rsave[r]); + break; + case Ocopy: + if (regcpy(i)) { + ctx->curi++; + i1 = dopm(ctx, b, i1, cur); + ctx->rega_stmov += static_cast<uint>(i + 1 - ctx->curi); + continue; + } + if (isreg(i->to)) + if (rtype(i->arg[0]) == RTmp) + sethint(ctx, static_cast<int>(i->arg[0].val), static_cast<int>(i->to.val)); + [[fallthrough]]; + default: + if (!req(i->to, Ref::none())) { + assert(rtype(i->to) == RTmp); + r = static_cast<int>(i->to.val); + if (r < Tmp0 && (BIT(r) & T.rglob)) + break; + rf = rfree(cur, r); + if (rf == -1) { + assert(!isreg(i->to)); + ctx->curi++; + continue; + } + i->to = Ref::tmp(static_cast<uint>(rf)); + } + break; + } + for (x = 0, nr = 0; x < 2; x++) + switch (rtype(i->arg[x])) { + case RMem: + m = &ctx->rega_mem[i->arg[x].val]; + if (rtype(m->base) == RTmp) + insert(ctx, &m->base, ra, nr++); + if (rtype(m->index) == RTmp) + insert(ctx, &m->index, ra, nr++); + break; + case RTmp: + insert(ctx, &i->arg[x], ra, nr++); + break; + } + for (r = 0; r < nr; r++) + *ra[r] = ralloc(ctx, cur, static_cast<int>(ra[r]->val)); + if (i->op == Ocopy && req(i->to, i->arg[0])) + ctx->curi++; + + /* try to change the register of a hinted + * temporary if rf is available */ + if (rf != -1 && (t = cur->w[rf]) != 0) + if (!bshas(cur->b, static_cast<uint>(rf)) && *hint(ctx, t) == rf && + (rt = rfree(cur, t)) != -1) { + ctx->rega_tmp[t].visit = -1; + ralloc(ctx, cur, t); + assert(bshas(cur->b, static_cast<uint>(rf))); + emit(ctx, Ocopy, ctx->rega_tmp[t].cls, Ref::tmp(static_cast<uint>(rt)), + Ref::tmp(static_cast<uint>(rf)), Ref::none()); + ctx->rega_stmov += 1; + cur->w[rf] = 0; + for (r = 0; r < nr; r++) + if (req(*ra[r], Ref::tmp(static_cast<uint>(rt)))) + *ra[r] = Ref::tmp(static_cast<uint>(rf)); + /* one could iterate this logic with + * the newly freed rt, but in this case + * the above loop must be changed */ + } + } + idup(ctx, b, ctx->curi, static_cast<ulong>(&ctx->insb[NIns] - ctx->curi)); +} + +/* comparison function to order temporaries + * for allocation at the end of blocks */ +int prio2(Ctx* ctx, int t1, int t2) { + if ((ctx->rega_tmp[t1].visit ^ ctx->rega_tmp[t2].visit) < 0) /* != signs */ + return ctx->rega_tmp[t1].visit != -1 ? +1 : -1; + if ((*hint(ctx, t1) ^ *hint(ctx, t2)) < 0) + return *hint(ctx, t1) != -1 ? +1 : -1; + return static_cast<int>(ctx->rega_tmp[t1].cost) - static_cast<int>(ctx->rega_tmp[t2].cost); +} + +} // namespace + +/* register allocation + * depends on rpo, phi, cost, (and obviously spill) + */ +void rega(Ctx* ctx, Fn* fn) { + int j, t, r, x, rl[Tmp0]; + Blk *b, *b1, *s, ***ps, *blist, **blk, **bp; + RMap *end, *beg, cur, old, *m; + Ins* i; + Phi* p; + uint u, n; + Ref src, dst; + + /* setup */ + ctx->rega_stmov = 0; + ctx->rega_stblk = 0; + ctx->rega_regu = 0; + ctx->rega_tmp = fn->tmp; + ctx->rega_mem = fn->mem; + blk = static_cast<Blk**>(alloc(ctx, fn->nblk * sizeof blk[0])); + end = static_cast<RMap*>(alloc(ctx, fn->nblk * sizeof end[0])); + beg = static_cast<RMap*>(alloc(ctx, fn->nblk * sizeof beg[0])); + for (n = 0; n < fn->nblk; n++) { + bsinit(ctx, end[n].b, static_cast<uint>(fn->ntmp)); + bsinit(ctx, beg[n].b, static_cast<uint>(fn->ntmp)); + } + bsinit(ctx, cur.b, static_cast<uint>(fn->ntmp)); + bsinit(ctx, old.b, static_cast<uint>(fn->ntmp)); + (void)old; /* declares but doesn't use this, suppress -Wunused here */ + + ctx->rega_loop = INT_MAX; + for (t = 0; t < fn->ntmp; t++) { + ctx->rega_tmp[t].hint.r = t < Tmp0 ? t : -1; + ctx->rega_tmp[t].hint.w = ctx->rega_loop; + ctx->rega_tmp[t].visit = -1; + } + for (bp = blk, b = fn->start; b; b = b->link) + *bp++ = b; + /* qsort() comparison function to peel + * loop nests from inside out */ + std::sort(blk, blk + fn->nblk, [](Blk* ba, Blk* bb) { + /* todo, evaluate if this order is really + * better than the simple postorder */ + if (ba->loop == bb->loop) + return ba->id > bb->id; + return ba->loop > bb->loop; + }); + for (b = fn->start, i = b->ins; i < &b->ins[b->nins]; i++) + if (i->op != Ocopy || !isreg(i->arg[0])) + break; + else { + assert(rtype(i->to) == RTmp); + sethint(ctx, static_cast<int>(i->to.val), static_cast<int>(i->arg[0].val)); + } + + /* assign registers */ + for (bp = blk; bp < &blk[fn->nblk]; bp++) { + b = *bp; + n = b->id; + ctx->rega_loop = b->loop; + cur.n = 0; + bszero(cur.b); + memset(cur.w, 0, sizeof cur.w); + for (x = 0, t = Tmp0; bsiter(b->out, &t); t++) { + j = x++; + rl[j] = t; + while (j-- > 0 && prio2(ctx, t, rl[j]) > 0) { + rl[j + 1] = rl[j]; + rl[j] = t; + } + } + for (r = 0; bsiter(b->out, &r) && r < Tmp0; r++) + radd(ctx, &cur, r, r); + for (j = 0; j < x; j++) + ralloctry(ctx, &cur, rl[j], 1); + for (j = 0; j < x; j++) + ralloc(ctx, &cur, rl[j]); + rcopy(&end[n], &cur); + doblk(ctx, b, &cur); + bscopy(b->in, cur.b); + for (p = b->phi; p; p = p->link) + if (rtype(p->to) == RTmp) + bsclr(b->in, p->to.val); + rcopy(&beg[n], &cur); + } + + /* emit copies shared by multiple edges + * to the same block */ + for (s = fn->start; s; s = s->link) { + if (s->npred <= 1) + continue; + m = &beg[s->id]; + + /* rl maps a register that is live at the + * beginning of s to the one used in all + * predecessors (if any, -1 otherwise) */ + memset(rl, 0, sizeof rl); + + /* to find the register of a phi in a + * predecessor, we have to find the + * corresponding argument */ + for (p = s->phi; p; p = p->link) { + if (rtype(p->to) != RTmp || (r = rfind(m, static_cast<int>(p->to.val))) == -1) + continue; + for (u = 0; u < p->narg; u++) { + b = p->blk[u]; + src = p->arg[u]; + if (rtype(src) != RTmp) + continue; + x = rfind(&end[b->id], static_cast<int>(src.val)); + if (x == -1) /* spilled */ + continue; + rl[r] = (!rl[r] || rl[r] == x) ? x : -1; + } + if (rl[r] == 0) + rl[r] = -1; + } + + /* process non-phis temporaries */ + for (j = 0; j < m->n; j++) { + t = m->t[j]; + r = m->r[j]; + if (rl[r] || t < Tmp0 /* todo, remove this */) + continue; + for (bp = s->pred; bp < &s->pred[s->npred]; bp++) { + x = rfind(&end[(*bp)->id], t); + if (x == -1) /* spilled */ + continue; + rl[r] = (!rl[r] || rl[r] == x) ? x : -1; + } + if (rl[r] == 0) + rl[r] = -1; + } + + ctx->rega_npm = 0; + for (j = 0; j < m->n; j++) { + t = m->t[j]; + r = m->r[j]; + x = rl[r]; + assert(x != 0 || t < Tmp0 /* todo, ditto */); + if (x > 0 && !bshas(m->b, static_cast<uint>(x))) { + pmadd(ctx, Ref::tmp(static_cast<uint>(x)), Ref::tmp(static_cast<uint>(r)), + ctx->rega_tmp[t].cls); + m->r[j] = x; + bsset(m->b, static_cast<uint>(x)); + } + } + ctx->curi = &ctx->insb[NIns]; + pmgen(ctx); + j = static_cast<int>(&ctx->insb[NIns] - ctx->curi); + if (j == 0) + continue; + ctx->rega_stmov += static_cast<uint>(j); + s->nins += static_cast<uint>(j); + i = static_cast<Ins*>(alloc(ctx, s->nins * sizeof(Ins))); + icpy(icpy(i, ctx->curi, static_cast<ulong>(j)), s->ins, s->nins - static_cast<uint>(j)); + s->ins = i; + } + + if (debug['R']) { + fprintf(ctx->dbg_buf, "\n> Register mappings:\n"); + for (n = 0; n < fn->nblk; n++) { + b = fn->rpo[n]; + fprintf(ctx->dbg_buf, "\t%-10s beg", b->name); + mdump(ctx, &beg[n]); + fprintf(ctx->dbg_buf, "\t end"); + mdump(ctx, &end[n]); + } + fprintf(ctx->dbg_buf, "\n"); + } + + /* emit remaining copies in new blocks */ + blist = 0; + for (b = fn->start;; b = b->link) { + Blk* null_blk = nullptr; + Blk** ps_arr[3] = {&b->s1, &b->s2, &null_blk}; + ps = ps_arr; + for (; (s = **ps); ps++) { + ctx->rega_npm = 0; + for (p = s->phi; p; p = p->link) { + dst = p->to; + assert(rtype(dst) == RSlot || rtype(dst) == RTmp); + if (rtype(dst) == RTmp) { + r = rfind(&beg[s->id], static_cast<int>(dst.val)); + if (r == -1) + continue; + dst = Ref::tmp(static_cast<uint>(r)); + } + for (u = 0; p->blk[u] != b; u++) + assert(u + 1 < p->narg); + src = p->arg[u]; + if (rtype(src) == RTmp) + src = rref(ctx, &end[b->id], static_cast<int>(src.val)); + pmadd(ctx, src, dst, p->cls); + } + for (t = Tmp0; bsiter(s->in, &t); t++) { + src = rref(ctx, &end[b->id], t); + dst = rref(ctx, &beg[s->id], t); + pmadd(ctx, src, dst, ctx->rega_tmp[t].cls); + } + ctx->curi = &ctx->insb[NIns]; + pmgen(ctx); + if (ctx->curi == &ctx->insb[NIns]) + continue; + b1 = newblk(ctx); + b1->loop = (b->loop + s->loop) / 2; + b1->link = blist; + blist = b1; + fn->nblk++; + b1->name = strf(ctx, PFn, "%s_%s", b->name, s->name); + ctx->rega_stmov += static_cast<uint>(&ctx->insb[NIns] - ctx->curi); + ctx->rega_stblk += 1; + idup(ctx, b1, ctx->curi, static_cast<ulong>(&ctx->insb[NIns] - ctx->curi)); + b1->jmp.type = Jjmp; + b1->s1 = s; + **ps = b1; + } + if (!b->link) { + b->link = blist; + break; + } + } + for (b = fn->start; b; b = b->link) + b->phi = 0; + fn->reg = ctx->rega_regu; + + if (debug['R']) { + fprintf(ctx->dbg_buf, "\n> Register allocation statistics:\n"); + fprintf(ctx->dbg_buf, "\tnew moves: %d\n", ctx->rega_stmov); + fprintf(ctx->dbg_buf, "\tnew blocks: %d\n", ctx->rega_stblk); + fprintf(ctx->dbg_buf, "\n> After register allocation:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +} // namespace rcc diff --git a/src/backend/rv64/abi.cc b/src/backend/rv64/abi.cc new file mode 100644 index 0000000..6456c21 --- /dev/null +++ b/src/backend/rv64/abi.cc @@ -0,0 +1,690 @@ +/* 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 abi.cc +/// @brief RISC-V lp64d ABI lowering. + +#include "rcc/rv64.h" + +namespace rcc { + +namespace { + +// ABI classification flags (OR-able). +enum : int { + kCptr = 1, // Pointer to large struct. + kCstk1 = 2, // First XLEN on stack. + kCstk2 = 4, // Second XLEN on stack. + kCstk = kCstk1 | kCstk2, + kCfpint = 8, // Float in GP reg. +}; + +// Argument class. +struct ArgClass { + int flags; // OR of kCptr / kCstk1 / kCstk2 / kCfpint + Typ* type; + int reg[2]; + int cls[2]; + int off[2]; + int ngp; // Written by typclass + int nfp; + int nreg; +}; + +struct Insl { + Insl* link; + Ins i; +}; + +struct Params { + int ngp; + int nfp; + int stk; +}; + +// Registers. Sentinels of 0 are used to prevent out-of-bounds reads. +constexpr int kGpReg[10] = {A0, A1, A2, A3, A4, A5, A6, A7, 0, 0}; +constexpr int kFpReg[10] = {FA0, FA1, FA2, FA3, FA4, FA5, FA6, FA7, 0, 0}; + +} // namespace + +// RCall encoding. + +bits rv64_retregs(Ref r, int p[2]) { + assert(rtype(r) == RCall); + int ngp = static_cast<int>(r.val & 3); + int nfp = static_cast<int>((r.val >> 2) & 3); + if (p) { + p[0] = ngp; + p[1] = nfp; + } + bits b = 0; + for (int i = 0; i < ngp; ++i) + b |= BIT(A0 + i); + for (int i = 0; i < nfp; ++i) + b |= BIT(FA0 + i); + return b; +} + +bits rv64_argregs(Ref r, int p[2]) { + assert(rtype(r) == RCall); + int ngp = static_cast<int>((r.val >> 4) & 15); + int nfp = static_cast<int>((r.val >> 8) & 15); + int t5 = static_cast<int>((r.val >> 12) & 1); + if (p) { + p[0] = ngp + t5; + p[1] = nfp; + } + bits b = 0; + for (int i = 0; i < ngp; ++i) + b |= BIT(A0 + i); + for (int i = 0; i < nfp; ++i) + b |= BIT(FA0 + i); + if (t5) + b |= BIT(T5); + return b; +} + +namespace { + +// HFA-style floating-point struct classification. + +int classify_fpstruct(Typ* t, int off, ArgClass* c) { + if (t->isunion) + return -1; + + for (Field* f = *t->fields; f->type != Field::FEnd; ++f) { + if (f->type == Field::FPad) { + off += static_cast<int>(f->len); + continue; + } + if (f->type == Field::FTyp) { + if (classify_fpstruct(&typ[f->len], off, c) == -1) + return -1; + continue; + } + + int slot = c->nfp + c->ngp; + if (slot == 2) + return -1; + + switch (f->type) { + case Field::Fb: + case Field::Fh: + case Field::Fw: + c->cls[slot] = Kw; + ++c->ngp; + break; + case Field::Fl: + c->cls[slot] = Kl; + ++c->ngp; + break; + case Field::Fs: + c->cls[slot] = Ks; + ++c->nfp; + break; + case Field::Fd: + c->cls[slot] = Kd; + ++c->nfp; + break; + default: + die("classify_fpstruct: unexpected field type"); + } + c->off[slot] = off; + off += static_cast<int>(f->len); + } + return c->nfp; +} + +// Determine ABI class for struct t. +void typclass(ArgClass* c, Typ* t, int fpabi, const int* gp, const int* fp) { + c->type = t; + c->flags = 0; + c->ngp = 0; + c->nfp = 0; + + if (t->align > 4) + err(const_cast<char*>("alignments larger than 16 are not supported")); + + if (t->isdark || t->size > 16 || t->size == 0) { + // Large or dark: pass by pointer. + c->flags |= kCptr; + c->cls[0] = Kl; + c->off[0] = 0; + c->ngp = 1; + } else if (!fpabi || classify_fpstruct(t, 0, c) <= 0) { + // GP-only word packing. + uint n = 0; + while (8 * n < t->size) { + c->cls[n] = Kl; + c->off[n] = static_cast<int>(8 * n); + ++n; + } + c->nfp = 0; + c->ngp = static_cast<int>(n); + } + + c->nreg = c->nfp + c->ngp; + for (int i = 0; i < c->nreg; ++i) { + if (KBASE(c->cls[i]) == 0) + c->reg[i] = *gp++; + else + c->reg[i] = *fp++; + } +} + +// Store tmp to mem+off. +void sttmps(Ctx* ctx, Ref tmp[], int ntmp, ArgClass* c, Ref mem, Fn* fn) { + static constexpr auto kStOp = [] { + std::array<int, 4> a{}; + a[Kw] = Ostorew; + a[Kl] = Ostorel; + a[Ks] = Ostores; + a[Kd] = Ostored; + return a; + }(); + assert(ntmp > 0 && ntmp <= 2); + for (int i = 0; i < ntmp; ++i) { + tmp[i] = newtmp(ctx, "abi", c->cls[i], fn); + Ref addr = newtmp(ctx, "abi", Kl, fn); + emit(ctx, kStOp[static_cast<size_t>(c->cls[i])], 0, Ref::none(), tmp[i], addr); + emit(ctx, Oadd, Kl, addr, mem, getcon(ctx, c->off[i], fn)); + } +} + +// Load mem+off to regs. +void ldregs(Ctx* ctx, ArgClass* c, Ref mem, Fn* fn) { + for (int i = 0; i < c->nreg; ++i) { + Ref addr = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Oload, c->cls[i], Ref::tmp(static_cast<uint>(c->reg[i])), addr, Ref::none()); + emit(ctx, Oadd, Kl, addr, mem, getcon(ctx, c->off[i], fn)); + } +} + +// Return value lowering. + +void selret(Ctx* ctx, Blk* b, Fn* fn) { + int j = b->jmp.type; + if (!isret(j) || j == Jret0) + return; + + Ref r = b->jmp.arg; + b->jmp.type = Jret0; + + int cty; + if (j == Jretc) { + ArgClass cr{}; + typclass(&cr, &typ[fn->retty], 1, kGpReg, kFpReg); + if (cr.flags & kCptr) { + assert(rtype(fn->retr) == RTmp); + emit(ctx, Oblit1, 0, Ref::none(), Ref::integer(static_cast<int>(cr.type->size)), + Ref::none()); + emit(ctx, Oblit0, 0, Ref::none(), r, fn->retr); + cty = 0; + } else { + ldregs(ctx, &cr, r, fn); + cty = (cr.nfp << 2) | cr.ngp; + } + } else { + int k = j - Jretw; + if (KBASE(k) == 0) { + emit(ctx, Ocopy, k, Ref::tmp(A0), r, Ref::none()); + cty = 1; + } else { + emit(ctx, Ocopy, k, Ref::tmp(FA0), r, Ref::none()); + cty = 1 << 2; + } + } + + b->jmp.arg = Ref::call(static_cast<uint>(cty)); +} + +// Arg/par classification. + +int argsclass(Ins* i0, Ins* i1, ArgClass* carg, int retptr) { + const int* gp = kGpReg; + const int* fp = kFpReg; + int ngp = 8, nfp = 8; + bool vararg = false, envc = false; + + if (retptr) { + ++gp; + --ngp; + } + + ArgClass* c = carg; + for (Ins* i = i0; i < i1; ++i, ++c) { + switch (i->op) { + case Opar: + case Oarg: + c->cls[0] = static_cast<int>(i->cls); + if (!vararg && KBASE(i->cls) == 1 && nfp > 0) { + --nfp; + c->reg[0] = *fp++; + } else if (ngp > 0) { + if (KBASE(i->cls) == 1) + c->flags |= kCfpint; + --ngp; + c->reg[0] = *gp++; + } else { + c->flags |= kCstk1; + } + break; + + case Oargv: + vararg = true; + break; + + case Oparc: + case Oargc: { + Typ* t = &typ[i->arg[0].val]; + typclass(c, t, 1, gp, fp); + if (c->nfp > 0 && (c->nfp >= nfp || c->ngp >= ngp)) { + // Downgrade to GP-only. + typclass(c, t, 0, gp, fp); + } + assert(c->nfp <= nfp); + if (c->ngp <= ngp) { + ngp -= c->ngp; + nfp -= c->nfp; + gp += c->ngp; + fp += c->nfp; + } else if (ngp > 0) { + // First word in GP, second on stack. + assert(c->ngp == 2); + assert(c->flags == 0); + c->flags |= kCstk2; + c->nreg = 1; + --ngp; + ++gp; + } else { + c->flags |= kCstk1; + if (c->nreg > 1) + c->flags |= kCstk2; + c->nreg = 0; + } + break; + } + + case Opare: + case Oarge: + c->reg[0] = T5; + c->cls[0] = Kl; + envc = true; + break; + } + } + + int passgp = static_cast<int>(gp - kGpReg); + int passfp = static_cast<int>(fp - kFpReg); + return ((envc ? 1 : 0) << 12) | (passgp << 4) | (passfp << 8); +} + +// Prepend Oalloc to start block. +void stkblob(Ctx* ctx, Ref r, Typ* t, Fn* fn, Insl** ilp) { + Insl* il = static_cast<Insl*>(alloc(ctx, sizeof(*il))); + int al = t->align - 2; // alignment logic. + if (al < 0) + al = 0; + uint64_t sz = (t->size + 7) & ~static_cast<uint64_t>(7); + + Ins ins = {}; + ins.op = static_cast<uint>(Oalloc + al); + ins.cls = Kl; + ins.to = r; + ins.arg[0] = getcon(ctx, static_cast<int64_t>(sz), fn); + il->i = ins; + il->link = *ilp; + *ilp = il; +} + +// Call lowering. + +void selcall(Ctx* ctx, Fn* fn, Ins* i0, Ins* i1, Insl** ilp) { + ArgClass* ca = + static_cast<ArgClass*>(alloc(ctx, static_cast<size_t>(i1 - i0) * sizeof(ArgClass))); + for (Ins* z = i0; z < i1; ++z) + ca[z - i0] = {}; + ArgClass cr{}; + + if (!req(i1->arg[1], Ref::none())) { + typclass(&cr, &typ[i1->arg[1].val], 1, kGpReg, kFpReg); + } + + int cty = argsclass(i0, i1, ca, cr.flags & kCptr); + + // compute stk and replace large struct args with pointer. + uint64_t stk = 0; + { + ArgClass* c = ca; + for (Ins* i = i0; i < i1; ++i, ++c) { + if (i->op == Oargv) + continue; + if (c->flags & kCptr) { + i->arg[0] = newtmp(ctx, "abi", Kl, fn); + stkblob(ctx, i->arg[0], c->type, fn, ilp); + i->op = Oarg; + } + if (c->flags & kCstk1) + stk += 8; + if (c->flags & kCstk2) + stk += 8; + } + stk += stk & 15; // 16B align tail + } + if (stk) + emit(ctx, Osalloc, Kl, Ref::none(), getcon(ctx, -static_cast<int64_t>(stk), fn), + Ref::none()); + + // Emit instructions backwards. + Ref tmp[2]; + if (!req(i1->arg[1], Ref::none())) { + stkblob(ctx, i1->to, cr.type, fn, ilp); + cty |= (cr.nfp << 2) | cr.ngp; + if (cr.flags & kCptr) { + // Dummy Ocopy. + emit(ctx, Ocopy, Kw, Ref::none(), Ref::tmp(A0), Ref::none()); + } else { + sttmps(ctx, tmp, cr.nreg, &cr, i1->to, fn); + for (int j = 0; j < cr.nreg; ++j) { + Ref rreg = Ref::tmp(static_cast<uint>(cr.reg[j])); + emit(ctx, Ocopy, cr.cls[j], tmp[j], rreg, Ref::none()); + } + } + } else if (KBASE(i1->cls) == 0) { + emit(ctx, Ocopy, i1->cls, i1->to, Ref::tmp(A0), Ref::none()); + cty |= 1; + } else { + emit(ctx, Ocopy, i1->cls, i1->to, Ref::tmp(FA0), Ref::none()); + cty |= 1 << 2; + } + + emit(ctx, Ocall, 0, Ref::none(), i1->arg[0], Ref::call(static_cast<uint>(cty))); + + if (cr.flags & kCptr) { + // Cptr return logic. + emit(ctx, Ocopy, Kl, Ref::tmp(A0), i1->to, Ref::none()); + } + + // move args to regs. + { + ArgClass* c = ca; + for (Ins* i = i0; i < i1; ++i, ++c) { + if (i->op == Oargv || (c->flags & kCstk1)) + continue; + if (i->op == Oargc) { + ldregs(ctx, c, i->arg[1], fn); + } else if (c->flags & kCfpint) { + int k = KWIDE(c->cls[0]) ? Kl : Kw; + Ref r = newtmp(ctx, "abi", k, fn); + emit(ctx, Ocopy, k, Ref::tmp(static_cast<uint>(c->reg[0])), r, Ref::none()); + c->reg[0] = static_cast<int>(r.val); + } else { + emit(ctx, Ocopy, c->cls[0], Ref::tmp(static_cast<uint>(c->reg[0])), i->arg[0], + Ref::none()); + } + } + } + + // casts and blits. + { + ArgClass* c = ca; + for (Ins* i = i0; i < i1; ++i, ++c) { + if (c->flags & kCfpint) { + int k = KWIDE(c->cls[0]) ? Kl : Kw; + emit(ctx, Ocast, k, Ref::tmp(static_cast<uint>(c->reg[0])), i->arg[0], Ref::none()); + } + if (c->flags & kCptr) { + emit(ctx, Oblit1, 0, Ref::none(), Ref::integer(static_cast<int>(c->type->size)), + Ref::none()); + emit(ctx, Oblit0, 0, Ref::none(), i->arg[1], i->arg[0]); + } + } + } + + if (!stk) + return; + + // move stack args to slot. + uint64_t off = 0; + Ref sp = newtmp(ctx, "abi", Kl, fn); + { + ArgClass* c = ca; + for (Ins* i = i0; i < i1; ++i, ++c) { + if (i->op == Oargv || !(c->flags & kCstk)) + continue; + if (i->op == Oarg) { + Ref addr = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Ostorew + i->cls, Kw, Ref::none(), i->arg[0], addr); + if (i->cls == Kw) { + // TODO: check if sext is needed. + ctx->curi->op = Ostorel; + ctx->curi->arg[0] = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Oextsw, Kl, ctx->curi->arg[0], i->arg[0], Ref::none()); + } + emit(ctx, Oadd, Kl, addr, sp, getcon(ctx, static_cast<int64_t>(off), fn)); + off += 8; + } + if (i->op == Oargc) { + if (c->flags & kCstk1) { + Ref addr = newtmp(ctx, "abi", Kl, fn); + Ref val = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Ostorel, 0, Ref::none(), val, addr); + emit(ctx, Oadd, Kl, addr, sp, getcon(ctx, static_cast<int64_t>(off), fn)); + emit(ctx, Oload, Kl, val, i->arg[1], Ref::none()); + off += 8; + } + if (c->flags & kCstk2) { + Ref addr = newtmp(ctx, "abi", Kl, fn); + Ref val = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Ostorel, 0, Ref::none(), val, addr); + emit(ctx, Oadd, Kl, addr, sp, getcon(ctx, static_cast<int64_t>(off), fn)); + Ref addr2 = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Oload, Kl, val, addr2, Ref::none()); + emit(ctx, Oadd, Kl, addr2, i->arg[1], getcon(ctx, 8, fn)); + off += 8; + } + } + } + } + emit(ctx, Osalloc, Kl, sp, getcon(ctx, static_cast<int64_t>(stk), fn), Ref::none()); +} + +// Par lowering. + +Params selpar(Ctx* ctx, Fn* fn, Ins* i0, Ins* i1) { + ArgClass* ca = + static_cast<ArgClass*>(alloc(ctx, static_cast<size_t>(i1 - i0) * sizeof(ArgClass))); + for (Ins* z = i0; z < i1; ++z) + ca[z - i0] = {}; + ArgClass cr{}; + ctx->curi = &ctx->insb[NIns]; + + if (fn->retty >= 0) { + typclass(&cr, &typ[fn->retty], 1, kGpReg, kFpReg); + if (cr.flags & kCptr) { + fn->retr = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Ocopy, Kl, fn->retr, Ref::tmp(A0), Ref::none()); + } + } + + int cty = argsclass(i0, i1, ca, cr.flags & kCptr); + fn->reg = rv64_argregs(Ref::call(static_cast<uint>(cty)), nullptr); + + // slot allocations and Ocast. + Insl* il = nullptr; + Ref tmp[17]; + Ref* t = tmp; + { + ArgClass* c = ca; + for (Ins* i = i0; i < i1; ++i, ++c) { + if (c->flags & kCfpint) { + Ref to_orig = i->to; + int k = c->cls[0]; + c->cls[0] = KWIDE(k) ? Kl : Kw; + i->to = newtmp(ctx, "abi", k, fn); + emit(ctx, Ocast, k, to_orig, i->to, Ref::none()); + } + if (i->op == Oparc && !(c->flags & kCptr) && c->nreg != 0) { + int nt = c->nreg; + if (c->flags & kCstk2) { + c->cls[1] = Kl; + c->off[1] = 8; + assert(nt == 1); + nt = 2; + } + sttmps(ctx, t, nt, c, i->to, fn); + stkblob(ctx, i->to, c->type, fn, &il); + t += nt; + } + } + } + for (; il; il = il->link) + emiti(ctx, il->i); + + // copy from regs or load from slot. + t = tmp; + int s = 2 + 8 * fn->vararg; + { + ArgClass* c = ca; + for (Ins* i = i0; i < i1; ++i, ++c) { + if (i->op == Oparc && !(c->flags & kCptr)) { + if (c->nreg == 0) { + fn->tmp[i->to.val].slot = -s; + s += (c->flags & kCstk2) ? 2 : 1; + continue; + } + for (int j = 0; j < c->nreg; ++j) { + Ref rreg = Ref::tmp(static_cast<uint>(c->reg[j])); + emit(ctx, Ocopy, c->cls[j], *t++, rreg, Ref::none()); + } + if (c->flags & kCstk2) { + emit(ctx, Oload, Kl, *t, Ref::slot(-s), Ref::none()); + ++t; + ++s; + } + } else if (c->flags & kCstk1) { + emit(ctx, Oload, c->cls[0], i->to, Ref::slot(-s), Ref::none()); + ++s; + } else { + emit(ctx, Ocopy, c->cls[0], i->to, Ref::tmp(static_cast<uint>(c->reg[0])), + Ref::none()); + } + } + } + + Params p; + p.stk = s; + p.ngp = (cty >> 4) & 15; + p.nfp = (cty >> 8) & 15; + return p; +} + +// Varargs. + +void selvaarg(Ctx* ctx, Fn* fn, Ins* i) { + Ref loc = newtmp(ctx, "abi", Kl, fn); + Ref newloc = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Ostorel, Kw, Ref::none(), newloc, i->arg[0]); + emit(ctx, Oadd, Kl, newloc, loc, getcon(ctx, 8, fn)); + emit(ctx, Oload, i->cls, i->to, loc, Ref::none()); + emit(ctx, Oload, Kl, loc, i->arg[0], Ref::none()); +} + +void selvastart(Ctx* ctx, Fn* fn, Params p, Ref ap) { + Ref rsave = newtmp(ctx, "abi", Kl, fn); + emit(ctx, Ostorel, Kw, Ref::none(), rsave, ap); + int s = p.stk > 2 + 8 * fn->vararg ? p.stk : 2 + p.ngp; + emit(ctx, Oaddr, Kl, rsave, Ref::slot(-s), Ref::none()); +} + +} // namespace + +// Entry point. + +void rv64_abi(Ctx* ctx, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + b->visit = 0; + + // Lower parameters. + Blk* b = fn->start; + Ins* end = &b->ins[b->nins]; + Ins* i = b->ins; + while (i < end && ispar(i->op)) + ++i; + Params p = selpar(ctx, fn, b->ins, i); + + int n0 = static_cast<int>(&ctx->insb[NIns] - ctx->curi); + int ioff = static_cast<int>(i - b->ins); + int n1 = static_cast<int>(b->nins) - ioff; + vgrow(ctx, &b->ins, static_cast<ulong>(n0 + n1)); + icpy(b->ins + n0, b->ins + ioff, static_cast<ulong>(n1)); + icpy(b->ins, ctx->curi, static_cast<ulong>(n0)); + b->nins = static_cast<uint>(n0 + n1); + + // Lower calls/returns/varargs. + Insl* il = nullptr; + b = fn->start; + do { + b = b->link ? b->link : fn->start; + if (b->visit) + continue; + ctx->curi = &ctx->insb[NIns]; + selret(ctx, b, fn); + + for (Ins* it = &b->ins[b->nins]; it != b->ins;) { + --it; + switch (it->op) { + default: + emiti(ctx, *it); + break; + case Ocall: { + Ins* lo = it; + while (lo > b->ins && isarg((lo - 1)->op)) + --lo; + selcall(ctx, fn, lo, it, &il); + it = lo; + break; + } + case Ovastart: + selvastart(ctx, fn, p, it->arg[0]); + break; + case Ovaarg: + selvaarg(ctx, fn, it); + break; + case Oarg: + case Oargc: + die("rv64_abi: Oarg/Oargc unreachable"); + } + } + + if (b == fn->start) { + for (; il; il = il->link) + emiti(ctx, il->i); + } + idup(ctx, b, ctx->curi, static_cast<ulong>(&ctx->insb[NIns] - ctx->curi)); + } while (b != fn->start); + + if (debug['A']) { + fprintf(ctx->dbg_buf, "\n> After ABI lowering:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +} // namespace rcc diff --git a/src/backend/rv64/emit.cc b/src/backend/rv64/emit.cc new file mode 100644 index 0000000..4a0c87e --- /dev/null +++ b/src/backend/rv64/emit.cc @@ -0,0 +1,949 @@ +/* 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 backend/rv64/emit.cc +/// @brief RISC-V 64 (lp64d) assembly emission. + +#include "rcc/rv64.h" + +#include <cinttypes> +#include <cstdio> + +namespace rcc { + +// Pseudo K values for omap wildcards. + +namespace { + +enum : short { + Ki = -1, // Matches Kw / Kl + Ka = -2, // Matches all cls +}; + +// Single instruction format templates. +struct AsmFmt { + short op; + short cls; + const char* fmt; +}; + +constexpr AsmFmt kAsmFmt[] = { + {Oadd, Ki, "add%k %=, %0, %1"}, + {Oadd, Ka, "fadd.%k %=, %0, %1"}, + {Osub, Ki, "sub%k %=, %0, %1"}, + {Osub, Ka, "fsub.%k %=, %0, %1"}, + {Oneg, Ki, "neg%k %=, %0"}, + {Oneg, Ka, "fneg.%k %=, %0"}, + {Odiv, Ki, "div%k %=, %0, %1"}, + {Odiv, Ka, "fdiv.%k %=, %0, %1"}, + {Orem, Ki, "rem%k %=, %0, %1"}, + {Orem, Kl, "rem %=, %0, %1"}, + {Oudiv, Ki, "divu%k %=, %0, %1"}, + {Ourem, Ki, "remu%k %=, %0, %1"}, + {Omul, Ki, "mul%k %=, %0, %1"}, + {Omul, Ka, "fmul.%k %=, %0, %1"}, + {Oand, Ki, "and %=, %0, %1"}, + {Oor, Ki, "or %=, %0, %1"}, + {Oxor, Ki, "xor %=, %0, %1"}, + {Osar, Ki, "sra%k %=, %0, %1"}, + {Oshr, Ki, "srl%k %=, %0, %1"}, + {Oshl, Ki, "sll%k %=, %0, %1"}, + {Ocsltl, Ki, "slt %=, %0, %1"}, + {Ocultl, Ki, "sltu %=, %0, %1"}, + {Oceqs, Ki, "feq.s %=, %0, %1"}, + {Ocges, Ki, "fge.s %=, %0, %1"}, + {Ocgts, Ki, "fgt.s %=, %0, %1"}, + {Ocles, Ki, "fle.s %=, %0, %1"}, + {Oclts, Ki, "flt.s %=, %0, %1"}, + {Oceqd, Ki, "feq.d %=, %0, %1"}, + {Ocged, Ki, "fge.d %=, %0, %1"}, + {Ocgtd, Ki, "fgt.d %=, %0, %1"}, + {Ocled, Ki, "fle.d %=, %0, %1"}, + {Ocltd, Ki, "flt.d %=, %0, %1"}, + {Ostoreb, Kw, "sb %0, %M1"}, + {Ostoreh, Kw, "sh %0, %M1"}, + {Ostorew, Kw, "sw %0, %M1"}, + {Ostorel, Ki, "sd %0, %M1"}, + {Ostores, Kw, "fsw %0, %M1"}, + {Ostored, Kw, "fsd %0, %M1"}, + {Oloadsb, Ki, "lb %=, %M0"}, + {Oloadub, Ki, "lbu %=, %M0"}, + {Oloadsh, Ki, "lh %=, %M0"}, + {Oloaduh, Ki, "lhu %=, %M0"}, + {Oloadsw, Ki, "lw %=, %M0"}, + // rv64 sign-extends 32-bit values into 64-bit registers. + {Oloaduw, Kw, "lw %=, %M0"}, + {Oloaduw, Kl, "lwu %=, %M0"}, + {Oload, Kw, "lw %=, %M0"}, + {Oload, Kl, "ld %=, %M0"}, + {Oload, Ks, "flw %=, %M0"}, + {Oload, Kd, "fld %=, %M0"}, + {Oextsb, Ki, "sext.b %=, %0"}, + {Oextub, Ki, "zext.b %=, %0"}, + {Oextsh, Ki, "sext.h %=, %0"}, + {Oextuh, Ki, "zext.h %=, %0"}, + {Oextsw, Kl, "sext.w %=, %0"}, + {Oextuw, Kl, "zext.w %=, %0"}, + {Otruncd, Ks, "fcvt.s.d %=, %0"}, + {Oexts, Kd, "fcvt.d.s %=, %0"}, + {Ostosi, Kw, "fcvt.w.s %=, %0, rtz"}, + {Ostosi, Kl, "fcvt.l.s %=, %0, rtz"}, + {Ostoui, Kw, "fcvt.wu.s %=, %0, rtz"}, + {Ostoui, Kl, "fcvt.lu.s %=, %0, rtz"}, + {Odtosi, Kw, "fcvt.w.d %=, %0, rtz"}, + {Odtosi, Kl, "fcvt.l.d %=, %0, rtz"}, + {Odtoui, Kw, "fcvt.wu.d %=, %0, rtz"}, + {Odtoui, Kl, "fcvt.lu.d %=, %0, rtz"}, + {Oswtof, Ka, "fcvt.%k.w %=, %0"}, + {Ouwtof, Ka, "fcvt.%k.wu %=, %0"}, + {Osltof, Ka, "fcvt.%k.l %=, %0"}, + {Oultof, Ka, "fcvt.%k.lu %=, %0"}, + {Ocast, Kw, "fmv.x.w %=, %0"}, + {Ocast, Kl, "fmv.x.d %=, %0"}, + {Ocast, Ks, "fmv.w.x %=, %0"}, + {Ocast, Kd, "fmv.d.x %=, %0"}, + {Ocopy, Ki, "mv %=, %0"}, + {Ocopy, Ka, "fmv.%k %=, %0"}, + {Oswap, Ki, "mv %?, %0\n\tmv %0, %1\n\tmv %1, %?"}, + {Oswap, Ka, "fmv.%k %?, %0\n\tfmv.%k %0, %1\n\tfmv.%k %1, %?"}, + {Oreqz, Ki, "seqz %=, %0"}, + {Ornez, Ki, "snez %=, %0"}, + {Ocall, Kw, "jalr %0"}, + {NOp, 0, nullptr}, +}; + +// Register names. + +constexpr auto kRegName = [] { + std::array<const char*, FT11 + 1> a{}; + a[T0] = "t0"; + a[T1] = "t1"; + a[T2] = "t2"; + a[T3] = "t3"; + a[T4] = "t4"; + a[T5] = "t5"; + a[A0] = "a0"; + a[A1] = "a1"; + a[A2] = "a2"; + a[A3] = "a3"; + a[A4] = "a4"; + a[A5] = "a5"; + a[A6] = "a6"; + a[A7] = "a7"; + a[S1] = "s1"; + a[S2] = "s2"; + a[S3] = "s3"; + a[S4] = "s4"; + a[S5] = "s5"; + a[S6] = "s6"; + a[S7] = "s7"; + a[S8] = "s8"; + a[S9] = "s9"; + a[S10] = "s10"; + a[S11] = "s11"; + a[FP] = "fp"; + a[SP] = "sp"; + a[GP] = "gp"; + a[TP] = "tp"; + a[RA] = "ra"; + a[FT0] = "ft0"; + a[FT1] = "ft1"; + a[FT2] = "ft2"; + a[FT3] = "ft3"; + a[FT4] = "ft4"; + a[FT5] = "ft5"; + a[FT6] = "ft6"; + a[FT7] = "ft7"; + a[FT8] = "ft8"; + a[FT9] = "ft9"; + a[FT10] = "ft10"; + a[FA0] = "fa0"; + a[FA1] = "fa1"; + a[FA2] = "fa2"; + a[FA3] = "fa3"; + a[FA4] = "fa4"; + a[FA5] = "fa5"; + a[FA6] = "fa6"; + a[FA7] = "fa7"; + a[FS0] = "fs0"; + a[FS1] = "fs1"; + a[FS2] = "fs2"; + a[FS3] = "fs3"; + a[FS4] = "fs4"; + a[FS5] = "fs5"; + a[FS6] = "fs6"; + a[FS7] = "fs7"; + a[FS8] = "fs8"; + a[FS9] = "fs9"; + a[FS10] = "fs10"; + a[FS11] = "fs11"; + a[T6] = "t6"; + a[FT11] = "ft11"; + return a; +}(); + +// Register name access helper. +static inline const char* regname(int r) { + return kRegName[static_cast<size_t>(r)]; +} + +constexpr char kClsChar[] = {'w', 'l', 's', 'd'}; + +// Slot offsets. +int64_t slot_offset(Ref r, Fn* fn) { + int s = rsval(r); + assert(s <= fn->slot); + if (s < 0) + return 8 * -s; + return -4 * (fn->slot - s); +} + +// CAddr symbol literals. + +void emit_addr_sym(Con* c, FILE* f) { + assert((c->sym.type & ~Sym::SExt) == Sym::SGlo); + fputs(str(c->sym.id), f); + if (c->bits.i) + fprintf(f, "+%" PRIi64, c->bits.i); +} + +// Load into specific register. + +void load_sym_into(Con* c, const char* rn, FILE* f) { + char off[32]; + switch (c->sym.type) { + case Sym::SGlo: + case Sym::SExt: + // medany compatibility. + fprintf(f, "\tla %s, ", rn); + emit_addr_sym(c, f); + fputc('\n', f); + return; + case Sym::SThr: + if (c->bits.i) + snprintf(off, sizeof(off), "+%" PRIi64, c->bits.i); + else + off[0] = 0; + fprintf(f, "\tlui %s, %%tprel_hi(%s)%s\n", rn, str(c->sym.id), off); + fprintf(f, "\tadd %s, %s, tp, %%tprel_add(%s)%s\n", rn, rn, str(c->sym.id), off); + fprintf(f, "\taddi %s, %s, %%tprel_lo(%s)%s\n", rn, rn, str(c->sym.id), off); + return; + case Sym::SExtThr: + die("extern thread unavailable on rv64"); + } +} + +void load_con_into(Con* c, int r, int k, FILE* f) { + const char* rn = regname(r); + switch (c->type) { + case Con::CAddr: + load_sym_into(c, rn, f); + return; + case Con::CBits: { + int64_t n = c->bits.i; + if (!KWIDE(k)) + n = static_cast<int32_t>(n); + fprintf(f, "\tli %s, %" PRIi64 "\n", rn, n); + return; + } + default: + die("invalid constant"); + } +} + +// Materialize memory operand into t6. + +void materialize_mem(Ref* pr, Fn* fn, FILE* f) { + Ref r = *pr; + if (rtype(r) == RCon) { + Con* c = &fn->con[r.val]; + if (c->type == Con::CAddr && c->sym.type != Sym::SGlo) { + load_con_into(c, T6, Kl, f); + *pr = Ref::tmp(static_cast<uint>(T6)); + } + } + if (rtype(r) == RSlot) { + int64_t s = slot_offset(r, fn); + if (s < -2048 || s > 2047) { + fprintf(f, "\tli t6, %" PRId64 "\n", s); + fputs("\tadd t6, fp, t6\n", f); + *pr = Ref::tmp(static_cast<uint>(T6)); + } + } +} + +// Format string expansion. + +void emit_reg_arg(Ref r, FILE* f) { + assert(isreg(r)); + fputs(kRegName[r.val], f); +} + +void emit_imm_or_reg(Ref r, Ins* /*i*/, Fn* fn, FILE* f) { + switch (rtype(r)) { + case RTmp: + emit_reg_arg(r, f); + return; + case RCon: { + Con* pc = &fn->con[r.val]; + assert(pc->type == Con::CBits); + assert(pc->bits.i >= -2048 && pc->bits.i < 2048); + fprintf(f, "%d", static_cast<int>(pc->bits.i)); + return; + } + default: + die("invalid second argument"); + } +} + +void emit_mem_arg(Ref r, Ins* i, Fn* fn, FILE* f) { + switch (rtype(r)) { + case RTmp: + fprintf(f, "0(%s)", kRegName[r.val]); + return; + case RCon: { + Con* pc = &fn->con[r.val]; + assert(pc->type == Con::CAddr); + emit_addr_sym(pc, f); + // Store / float load pseudo-instructions need a scratch register. + if (isstore(i->op) || (isload(i->op) && KBASE(i->cls) == 1)) + fputs(", t6", f); + return; + } + case RSlot: { + int64_t offset = slot_offset(r, fn); + assert(offset >= -2048 && offset <= 2047); + fprintf(f, "%d(fp)", static_cast<int>(offset)); + return; + } + case RMem: { + // Base+const addressing from isel. + Mem* m = &fn->mem[r.val]; + assert(isreg(m->base)); + assert(req(m->index, Ref::none())); + int64_t off = m->offset.type == Con::CBits ? m->offset.bits.i : 0; + assert(off >= -2048 && off <= 2047); + fprintf(f, "%d(%s)", static_cast<int>(off), kRegName[m->base.val]); + return; + } + default: + die("invalid address argument"); + } +} + +void emit_fmt(const char* s, Ins* i, Fn* fn, FILE* f) { + int k = i->cls; + fputc('\t', f); + for (;;) { + // Process characters or escape sequences. + char c; + while ((c = *s++) != '%') { + if (!c) { + fputc('\n', f); + return; + } + fputc(c, f); + } + char e = *s++; + switch (e) { + case '?': + fputs(KBASE(k) == 0 ? "t6" : "ft11", f); + break; + case 'k': + if (k != Kl) + fputc(kClsChar[k], f); + break; + case '=': + emit_reg_arg(i->to, f); + break; + case '0': + emit_reg_arg(i->arg[0], f); + break; + case '1': + emit_imm_or_reg(i->arg[1], i, fn, f); + break; + case 'M': { + char sel = *s++; + assert(sel == '0' || sel == '1'); + emit_mem_arg(i->arg[sel - '0'], i, fn, f); + break; + } + default: + die("invalid escape"); + } + } +} + +// Table-driven emit dispatch. + +void emit_from_table(Ins* i, Fn* fn, FILE* f) { + for (int o = 0;; ++o) { + if (kAsmFmt[o].op == NOp) + die("no match for %s(%c) to=t%d/v%u a0=t%d/v%u a1=t%d/v%u || %s", optab[i->op].name, + "wlsd"[i->cls], rtype(i->to), i -> to.val, rtype(i->arg[0]), i->arg[0].val, + rtype(i->arg[1]), i->arg[1].val, die_ctx_str(nullptr, i)); + if (kAsmFmt[o].op != static_cast<short>(i->op)) + continue; + short tc = kAsmFmt[o].cls; + if (tc == static_cast<short>(i->cls) || tc == Ka || (tc == Ki && KBASE(i->cls) == 0)) { + emit_fmt(kAsmFmt[o].fmt, i, fn, f); + return; + } + } +} + +// Single instruction dispatch. + +void emit_ins(Ins* i, Fn* fn, FILE* f) { + // Store with constant 0. + if (INRANGE(i->op, Ostoreb, Ostorel) && rtype(i->arg[0]) == RCon) { + Con* c = &fn->con[i->arg[0].val]; + if (c->type == Con::CBits && c->bits.i == 0) { + materialize_mem(&i->arg[1], fn, f); + const char* mn; + switch (i->op) { + case Ostoreb: + mn = "sb"; + break; + case Ostoreh: + mn = "sh"; + break; + case Ostorew: + mn = "sw"; + break; + case Ostorel: + mn = "sd"; + break; + default: + die("unreachable"); + } + fprintf(f, "\t%s zero, ", mn); + emit_mem_arg(i->arg[1], i, fn, f); + fputc('\n', f); + return; + } + } + + switch (i->op) { + default: + if (isload(i->op)) + materialize_mem(&i->arg[0], fn, f); + else if (isstore(i->op)) + materialize_mem(&i->arg[1], fn, f); + emit_from_table(i, fn, f); + return; + + case Ocopy: { + if (req(i->to, i->arg[0])) + return; + + if (rtype(i->to) == RSlot) { + int rt = rtype(i->arg[0]); + if (rt == RSlot) { + // SLOT -> SLOT bridge. + int64_t off_src = slot_offset(i->arg[0], fn); + int64_t off_dst = slot_offset(i->to, fn); + const char* scratch = KBASE(i->cls) ? "ft11" : "t6"; + const char* ld; + const char* st; + switch (i->cls) { + case Kw: + ld = "lw"; + st = "sw"; + break; + case Kl: + ld = "ld"; + st = "sd"; + break; + case Ks: + ld = "flw"; + st = "fsw"; + break; + case Kd: + ld = "fld"; + st = "fsd"; + break; + default: + die("Ocopy SLOT->SLOT illegal cls=%d", i->cls); + } + // Check for large offsets. + if (off_src < -2048 || off_src > 2047 || off_dst < -2048 || off_dst > 2047) + die("Ocopy SLOT->SLOT large offset NYI off_src=%" PRId64 " off_dst=%" PRId64 + " || %s", + off_src, off_dst, die_ctx_str(nullptr, i)); + fprintf(f, "\t%s %s, %" PRId64 "(fp)\n", ld, scratch, off_src); + fprintf(f, "\t%s %s, %" PRId64 "(fp)\n", st, scratch, off_dst); + return; + } + if (rt == RCon) + die("unimplemented Ocopy SLOT-dst cls=%c to=t%d/v%u a0=t%d/v%u || %s", + "wlsd"[i->cls], rtype(i->to), i -> to.val, rtype(i->arg[0]), i->arg[0].val, + die_ctx_str(nullptr, i)); + assert(isreg(i->arg[0])); + Ins ii = *i; + ii.arg[1] = ii.to; + ii.to = Ref::none(); + switch (ii.cls) { + case Kw: + ii.op = Ostorew; + break; + case Kl: + ii.op = Ostorel; + break; + case Ks: + ii.op = Ostores; + break; + case Kd: + ii.op = Ostored; + break; + } + // Revert dopm Ks/Kd to Kw for Ostore*. + ii.cls = Kw; + materialize_mem(&ii.arg[1], fn, f); + emit_from_table(&ii, fn, f); + return; + } + + assert(isreg(i->to)); + switch (rtype(i->arg[0])) { + case RCon: + load_con_into(&fn->con[i->arg[0].val], static_cast<int>(i->to.val), + static_cast<int>(i->cls), f); + return; + case RSlot: { + Ins ii = *i; + ii.op = Oload; + materialize_mem(&ii.arg[0], fn, f); + emit_from_table(&ii, fn, f); + return; + } + default: + assert(isreg(i->arg[0])); + emit_from_table(i, fn, f); + return; + } + } + + case Onop: + return; + + case Oaddr: { + const char* rn = kRegName[i->to.val]; + switch (rtype(i->arg[0])) { + case RSlot: { + int64_t s = slot_offset(i->arg[0], fn); + if (-s < 2048) { + fprintf(f, "\tadd %s, fp, %" PRId64 "\n", rn, s); + } else { + fprintf(f, "\tli %s, %" PRId64 "\n", rn, s); + fprintf(f, "\tadd %s, fp, %s\n", rn, rn); + } + return; + } + case RCon: { + // addr $sym generated from IRGen. + Con* c = &fn->con[i->arg[0].val]; + assert(c->type == Con::CAddr); + load_sym_into(c, rn, f); + return; + } + default: + die("invalid Oaddr argument"); + } + } + + case Ocall: + switch (rtype(i->arg[0])) { + case RCon: { + Con* con = &fn->con[i->arg[0].val]; + if (con->type != Con::CAddr || (con->sym.type & Sym::SThr) || con->bits.i) + die("invalid call argument"); + fprintf(f, "\tcall %s\n", str(con->sym.id)); + return; + } + case RTmp: + emit_fmt("jalr %0", i, fn, f); + return; + default: + die("invalid call argument"); + } + + case Osalloc: + emit_fmt("sub sp, sp, %0", i, fn, f); + if (!req(i->to, Ref::none())) + emit_fmt("mv %=, sp", i, fn, f); + return; + + case Odbgloc: + emitdbgloc(i->arg[0].val, i->arg[1].val, f); + return; + } +} + +// Function prologue / epilogue helpers. + +void save_clobbers(Fn* fn, FILE* f) { + int off = 0; + for (int* pr = rv64_rclob; *pr >= 0; ++pr) { + if (!(fn->reg & BIT(*pr))) + continue; + fprintf(f, "\t%s %s, %d(sp)\n", *pr < FT0 ? "sd" : "fsd", regname(*pr), off); + off += 8; + } +} + +void restore_clobbers(Fn* fn, FILE* f) { + int off = 0; + for (int* pr = rv64_rclob; *pr >= 0; ++pr) { + if (!(fn->reg & BIT(*pr))) + continue; + fprintf(f, "\t%s %s, %d(sp)\n", *pr < FT0 ? "ld" : "fld", regname(*pr), off); + off += 8; + } +} + +// Compute total stack frame size. +int compute_frame_size(Fn* fn) { + int frame = (16 + 4 * fn->slot + 15) & ~15; + for (int* pr = rv64_rclob; *pr >= 0; ++pr) { + if (fn->reg & BIT(*pr)) + frame += 8; + } + return (frame + 15) & ~15; +} + +// Adjust sp by delta. +void emit_sp_adjust(const char* op, const char* base, int delta, FILE* f) { + if (delta <= 2048) { + fprintf(f, "\t%s sp, %s, -%d\n", op, base, delta); + } else { + fprintf(f, "\tli t6, -%d\n\t%s sp, %s, t6\n", delta, op, base); + } +} + +// Fused cmp+jnz -> single conditional branch. + +struct BrCfg { + const char* mn; // rv64 branch mnemonic. + bool swap; // Swap operands. +}; + +// Branch mnemonic for each condition code. +constexpr BrCfg kBrCfg[NCmpI] = { + /* Cieq */ {"beq", false}, /* Cine */ {"bne", false}, + /* Cisge */ {"bge", false}, /* Cisgt */ {"blt", true}, + /* Cisle */ {"bge", true}, /* Cislt */ {"blt", false}, + /* Ciuge */ {"bgeu", false}, /* Ciugt */ {"bltu", true}, + /* Ciule */ {"bgeu", true}, /* Ciult */ {"bltu", false}, +}; + +// Logical negation of condition codes. +constexpr int kCmpNeg[NCmpI] = { + /* Cieq */ Cine, /* Cine */ Cieq, /* Cisge */ Cislt, /* Cisgt */ Cisle, + /* Cisle */ Cisgt, /* Cislt */ Cisge, /* Ciuge */ Ciult, /* Ciugt */ Ciule, + /* Ciule */ Ciugt, /* Ciult */ Ciuge, +}; + +// Resolve fused branch operand. +const char* branch_operand(Ref r, int ck, Fn* fn, FILE* f, bool* slot_used, Blk* b) { + if (rtype(r) == RSlot) { + if (*slot_used) + die("fused branch: both operands spilled @%d", static_cast<int>(b->id)); + Ins ii = {}; + ii.arg[0] = r; + emit_fmt(KWIDE(ck) ? "ld t6, %M0" : "lw t6, %M0", &ii, fn, f); + *slot_used = true; + return kRegName[T6]; + } + assert(isreg(r)); + return kRegName[r.val]; +} + +} // namespace + +// rv64_emitfn: Function entry point. + +namespace { +// Global block ID base. +int g_blkid_base = 0; +} // namespace + +void rv64_emitfn(Fn* fn, FILE* f) { + emitfnlnk(fn->name, &fn->lnk, f); + + // Prologue. + if (fn->vararg) { + // TODO: only allocate for a* unused by named arguments. + fputs("\tadd sp, sp, -64\n", f); + for (int r = A0; r <= A7; ++r) + fprintf(f, "\tsd %s, %d(sp)\n", regname(r), 8 * (r - A0)); + } + fputs("\tsd fp, -16(sp)\n", f); + fputs("\tsd ra, -8(sp)\n", f); + fputs("\tadd fp, sp, -16\n", f); + + int frame = compute_frame_size(fn); + emit_sp_adjust("add", "sp", frame, f); + save_clobbers(fn, f); + + // Body. + bool need_label = false; + for (Blk* b = fn->start; b; b = b->link) { + if (need_label || b->npred > 1) + fprintf(f, ".L%d:\n", g_blkid_base + static_cast<int>(b->id)); + for (Ins* i = b->ins; i != b->ins + b->nins; ++i) + emit_ins(i, fn, f); + need_label = true; + + switch (b->jmp.type) { + case Jhlt: + fputs("\tebreak\n", f); + break; + + case Jret0: + if (fn->dynalloc) + emit_sp_adjust("add", "fp", frame - 16, f); + restore_clobbers(fn, f); + fprintf(f, + "\tadd sp, fp, %d\n" + "\tld ra, 8(fp)\n" + "\tld fp, 0(fp)\n" + "\tret\n", + 16 + fn->vararg * 64); + break; + + case Jjmp: + emit_jmp: + if (b->s1 != b->link) + fprintf(f, "\tj .L%d\n", g_blkid_base + static_cast<int>(b->s1->id)); + else + need_label = false; + break; + + case Jjnz: { + // Ordering optimization for fall-through. + bool neg = false; + if (b->link == b->s2) { + Blk* s = b->s1; + b->s1 = b->s2; + b->s2 = s; + neg = true; + } + if (rtype(b->jmp.arg) == RSlot) { + Ins ii = {}; + ii.arg[0] = b->jmp.arg; + emit_fmt("lw t6, %M0", &ii, fn, f); + b->jmp.arg = Ref::tmp(static_cast<uint>(T6)); + } + assert(isreg(b->jmp.arg)); + fprintf(f, "\tb%sz %s, .L%d\n", neg ? "ne" : "eq", kRegName[b->jmp.arg.val], + g_blkid_base + static_cast<int>(b->s2->id)); + goto emit_jmp; + } + + default: + // Fused integer cmp+jnz. + if (b->jmp.type >= Jjfieq && b->jmp.type <= Jjfiult) { + int cc = static_cast<int>(b->jmp.type) - Jjfieq; + int ck = b->jmp.cls; + + bool neg = false; + if (b->link == b->s2) { + Blk* s = b->s1; + b->s1 = b->s2; + b->s2 = s; + neg = true; + } + BrCfg cfg = kBrCfg[neg ? cc : kCmpNeg[cc]]; + + bool slot_used = false; + const char* r0 = branch_operand(b->jmp.arg, ck, fn, f, &slot_used, b); + const char* r1 = branch_operand(b->jmp.arg2, ck, fn, f, &slot_used, b); + if (cfg.swap) { + const char* t = r0; + r0 = r1; + r1 = t; + } + fprintf(f, "\t%s %s, %s, .L%d\n", cfg.mn, r0, r1, + g_blkid_base + static_cast<int>(b->s2->id)); + goto emit_jmp; + } + break; + } + } + + g_blkid_base += static_cast<int>(fn->nblk); + elf_emitfnfin(fn->name, f); +} + +// Built-in runtime. + +namespace { + +void emit_runtime_memset(FILE* f) { + fprintf(f, + ".globl __rcc_memset\n" + "__rcc_memset:\n" + "\tandi a1, a1, 0xff\n" + "\tslli t0, a1, 8\n" + "\tor a1, a1, t0\n" + "\tslli t0, a1, 16\n" + "\tor a1, a1, t0\n" + "\tslli t0, a1, 32\n" + "\tor a1, a1, t0\n" + "\tmv t1, a0\n" + "\tli t2, 64\n" + ".Lrcc_ms64:\n" + "\tblt a2, t2, .Lrcc_ms8\n" + "\tsd a1, 0(t1)\n" + "\tsd a1, 8(t1)\n" + "\tsd a1, 16(t1)\n" + "\tsd a1, 24(t1)\n" + "\tsd a1, 32(t1)\n" + "\tsd a1, 40(t1)\n" + "\tsd a1, 48(t1)\n" + "\tsd a1, 56(t1)\n" + "\taddi t1, t1, 64\n" + "\taddi a2, a2, -64\n" + "\tj .Lrcc_ms64\n" + ".Lrcc_ms8:\n" + "\tli t2, 8\n" + ".Lrcc_ms8l:\n" + "\tblt a2, t2, .Lrcc_ms1\n" + "\tsd a1, 0(t1)\n" + "\taddi t1, t1, 8\n" + "\taddi a2, a2, -8\n" + "\tj .Lrcc_ms8l\n" + ".Lrcc_ms1:\n" + "\tbeqz a2, .Lrcc_msd\n" + "\tsb a1, 0(t1)\n" + "\taddi t1, t1, 1\n" + "\taddi a2, a2, -1\n" + "\tj .Lrcc_ms1\n" + ".Lrcc_msd:\n" + "\tret\n" + ".type __rcc_memset, @function\n" + ".size __rcc_memset, .-__rcc_memset\n\n"); +} + +void emit_runtime_memcpy(FILE* f) { + fprintf(f, + ".globl __rcc_memcpy\n" + "__rcc_memcpy:\n" + "\tmv t1, a0\n" + ".Lrcc_mc32:\n" + "\tli t2, 32\n" + "\tblt a2, t2, .Lrcc_mc8\n" + "\tld t3, 0(a1)\n" + "\tld t4, 8(a1)\n" + "\tld t5, 16(a1)\n" + "\tld t6, 24(a1)\n" + "\tsd t3, 0(t1)\n" + "\tsd t4, 8(t1)\n" + "\tsd t5, 16(t1)\n" + "\tsd t6, 24(t1)\n" + "\taddi a1, a1, 32\n" + "\taddi t1, t1, 32\n" + "\taddi a2, a2, -32\n" + "\tj .Lrcc_mc32\n" + ".Lrcc_mc8:\n" + "\tli t2, 8\n" + ".Lrcc_mc8l:\n" + "\tblt a2, t2, .Lrcc_mc1\n" + "\tld t3, 0(a1)\n" + "\tsd t3, 0(t1)\n" + "\taddi a1, a1, 8\n" + "\taddi t1, t1, 8\n" + "\taddi a2, a2, -8\n" + "\tj .Lrcc_mc8l\n" + ".Lrcc_mc1:\n" + "\tbeqz a2, .Lrcc_mcd\n" + "\tlb t3, 0(a1)\n" + "\tsb t3, 0(t1)\n" + "\taddi a1, a1, 1\n" + "\taddi t1, t1, 1\n" + "\taddi a2, a2, -1\n" + "\tj .Lrcc_mc1\n" + ".Lrcc_mcd:\n" + "\tret\n" + ".type __rcc_memcpy, @function\n" + ".size __rcc_memcpy, .-__rcc_memcpy\n\n"); +} + +void emit_runtime_memmove(FILE* f) { + fprintf(f, + ".globl __rcc_memmove\n" + "__rcc_memmove:\n" + "\tbgeu a0, a1, .Lrcc_mm_chk\n" + "\tj __rcc_memcpy\n" + ".Lrcc_mm_chk:\n" + "\tadd t0, a1, a2\n" + "\tbgeu a0, t0, __rcc_memcpy\n" + "\tadd t1, a0, a2\n" + "\tadd a1, a1, a2\n" + ".Lrcc_mmb32:\n" + "\tli t2, 32\n" + "\tblt a2, t2, .Lrcc_mmb8\n" + "\taddi t1, t1, -32\n" + "\taddi a1, a1, -32\n" + "\tld t3, 0(a1)\n" + "\tld t4, 8(a1)\n" + "\tld t5, 16(a1)\n" + "\tld t6, 24(a1)\n" + "\tsd t3, 0(t1)\n" + "\tsd t4, 8(t1)\n" + "\tsd t5, 16(t1)\n" + "\tsd t6, 24(t1)\n" + "\taddi a2, a2, -32\n" + "\tj .Lrcc_mmb32\n" + ".Lrcc_mmb8:\n" + "\tli t2, 8\n" + ".Lrcc_mmb8l:\n" + "\tblt a2, t2, .Lrcc_mmb1\n" + "\taddi t1, t1, -8\n" + "\taddi a1, a1, -8\n" + "\tld t3, 0(a1)\n" + "\tsd t3, 0(t1)\n" + "\taddi a2, a2, -8\n" + "\tj .Lrcc_mmb8l\n" + ".Lrcc_mmb1:\n" + "\tbeqz a2, .Lrcc_mmd\n" + "\taddi t1, t1, -1\n" + "\taddi a1, a1, -1\n" + "\tlb t3, 0(a1)\n" + "\tsb t3, 0(t1)\n" + "\taddi a2, a2, -1\n" + "\tj .Lrcc_mmb1\n" + ".Lrcc_mmd:\n" + "\tret\n" + ".type __rcc_memmove, @function\n" + ".size __rcc_memmove, .-__rcc_memmove\n\n"); +} + +} // namespace + +void rv64_emitfin(Ctx* ctx, FILE* f) { + fprintf(f, ".text\n"); + emit_runtime_memset(f); + emit_runtime_memcpy(f); + emit_runtime_memmove(f); + elf_emitfin(ctx, f); +} + +} // namespace rcc diff --git a/src/backend/rv64/isel.cc b/src/backend/rv64/isel.cc new file mode 100644 index 0000000..c2ded02 --- /dev/null +++ b/src/backend/rv64/isel.cc @@ -0,0 +1,518 @@ +/* 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 isel.cc +/// @brief RISC-V 64 instruction selection. + +#include "rcc/rv64.h" + +#include <cinttypes> +#include <climits> +#include <cstdio> + +namespace rcc { +namespace { + +// Operand role classification. + +// is_mem_arg: Does the arg slot pointed to by r have "memory operand" semantics? +bool is_mem_arg(Ref* r, int op, Ins* i) { + if (isload(op) || op == Ocall) + return r == &i->arg[0]; + if (isstore(op)) + return r == &i->arg[1]; + return false; +} + +// is_imm_arg: Is r the immediate slot for this op? +bool is_imm_arg(Ref* r, int op, Ins* i) { + return rv64_op[op].imm && r == &i->arg[1]; +} + +// Compare op -> (sign, swap, neg) tuple. + +struct CmpCfg { + uint8_t sign; // 1=signed (Ocsltl), 0=unsigned (Ocultl) + uint8_t swap; // 1=swap arg0/arg1 + uint8_t neg; // 1=negate i.to +}; + +constexpr CmpCfg kIntOrderCmp[8] = { + /* Cisge */ {1, 0, 1}, // a >= b ⇔ !(a < b) + /* Cisgt */ {1, 1, 0}, // a > b ⇔ (b < a) + /* Cisle */ {1, 1, 1}, // a <= b ⇔ !(b < a) + /* Cislt */ {1, 0, 0}, // a < b + /* Ciuge */ {0, 0, 1}, + /* Ciugt */ {0, 1, 0}, + /* Ciule */ {0, 1, 1}, + /* Ciult */ {0, 0, 0}, +}; + +// Utilities. + +// boolean negate: emit `xor pr, src, 1` +void negate_to(Ctx* ctx, Ref* pr, Fn* fn) { + Ref src = newtmp(ctx, "isel", Kw, fn); + emit(ctx, Oxor, Kw, *pr, src, getcon(ctx, 1, fn)); + *pr = src; +} + +// fixarg: operand fixup. + +// Fix constant operand. +void fix_con_arg(Ctx* ctx, Ref* r, int k, Ins* i, int op, Fn* fn) { + Con* c = &fn->con[r->val]; + + // CAddr as mem operand: no materialization needed. + if (c->type == Con::CAddr && is_mem_arg(r, op, i)) + return; + + // Integer immediate within I-form range. + if (KBASE(k) == 0 && c->type == Con::CBits && is_imm_arg(r, op, i)) { + if (-2048 <= c->bits.i && c->bits.i < 2048) + return; + } + + // Integer store value of 0: emit `s* zero, ...` without materialization. + if (i && c->type == Con::CBits && c->bits.i == 0 && r == &i->arg[0] && + INRANGE(op, Ostoreb, Ostorel)) { + return; + } + + Ref dst = newtmp(ctx, "isel", k, fn); + + if (KBASE(k) == 1) { + // Floating-point constants must be stashed to ro data section. + assert(c->type == Con::CBits); + int slot_id = stashbits(ctx, static_cast<bits>(c->bits.i), KWIDE(k) ? 8 : 4); + + vgrow(ctx, &fn->con, static_cast<ulong>(++fn->ncon)); + Con* nc = &fn->con[fn->ncon - 1]; + *nc = {}; + nc->type = Con::CAddr; + + char buf[32]; + snprintf(buf, sizeof(buf), "\"%sfp%d\"", T.asloc, slot_id); + nc->sym.id = intern(buf); + + emit(ctx, Oload, k, dst, Ref::con(static_cast<uint>(nc - fn->con)), Ref::none()); + } else { + // Integer constants out of imm range use Ocopy. + emit(ctx, Ocopy, k, dst, *r, Ref::none()); + } + + *r = dst; +} + +// Operations that do not require sext.w when Kw is input. +bool skip_sext_for_kw(int op) { + switch (op) { + // *w-form arithmetic. + case Oadd: + case Osub: + case Omul: + case Odiv: + case Orem: + case Oudiv: + case Ourem: + case Osar: + case Oshr: + case Oshl: + // Narrow stores. + case Ostoreb: + case Ostoreh: + case Ostorew: + // Extensions. + case Oextsb: + case Oextub: + case Oextsh: + case Oextuh: + case Oextsw: + case Oextuw: + // int->float conversions. + case Oswtof: + case Ouwtof: + return true; + default: + return false; + } +} + +// Fix temporary operand. +void fix_tmp_arg(Ctx* ctx, Ref* r, int k, Ins* i, int op, Fn* fn) { + if (isreg(*r)) + return; + + int s = fn->tmp[r->val].slot; + if (s != -1) { + // Tmp with slot allocated. + if (is_mem_arg(r, op, i)) { + *r = Ref::slot(s); + } else { + Ref dst = newtmp(ctx, "isel", k, fn); + emit(ctx, Oaddr, k, dst, Ref::slot(s), Ref::none()); + *r = dst; + } + return; + } + + // Tmp is Kl but used as Kw. + if (k == Kw && fn->tmp[r->val].cls == Kl) { + if (!skip_sext_for_kw(op)) { + Ref dst = newtmp(ctx, "isel", k, fn); + emit(ctx, Oextsw, Kl, dst, *r, Ref::none()); + *r = dst; + } + // Skip sext.w. + return; + } + + assert(k == fn->tmp[r->val].cls); +} + +void fixarg(Ctx* ctx, Ref* r, int k, Ins* i, Fn* fn) { + int op = i ? static_cast<int>(i->op) : Ocopy; + switch (rtype(*r)) { + case RCon: + fix_con_arg(ctx, r, k, i, op, fn); + break; + case RTmp: + fix_tmp_arg(ctx, r, k, i, op, fn); + break; + default: + // RSlot/RType/RCall/RMem/RInt/RNone don't need fixing. + break; + } +} + +// Fixarg both operands of the instruction. +void fixarg2(Ctx* ctx, int k, Fn* fn) { + Ins* h = ctx->curi; + fixarg(ctx, &h->arg[0], k, h, fn); + fixarg(ctx, &h->arg[1], k, h, fn); +} + +// Compare selection. + +void selcmp(Ctx* ctx, Ins i, int k, int op, Fn* fn) { + assert(op >= 0 && op < NCmp); + + // (1) Integer eq/ne. + if (op == Cieq || op == Cine) { + Ref r = newtmp(ctx, "isel", k, fn); + emit(ctx, op == Cieq ? Oreqz : Ornez, static_cast<int>(i.cls), i.to, r, Ref::none()); + emit(ctx, Oxor, k, r, i.arg[0], i.arg[1]); + fixarg2(ctx, k, fn); + return; + } + + // (2) Integer ordered comparisons. + if (op < NCmpI) { + const CmpCfg cfg = kIntOrderCmp[op - Cisge]; + i.op = static_cast<uint>(cfg.sign ? Ocsltl : Ocultl); + if (cfg.swap) { + Ref t = i.arg[0]; + i.arg[0] = i.arg[1]; + i.arg[1] = t; + } + if (cfg.neg) + negate_to(ctx, &i.to, fn); + emiti(ctx, i); + fixarg2(ctx, k, fn); + return; + } + + // (3) Floating point comparisons. + int fop = op - NCmpI; + + // Cfo (ordered) / Cfuo (unordered). + if (fop == Cfo || fop == Cfuo) { + if (fop == Cfuo) + negate_to(ctx, &i.to, fn); + Ref r0 = newtmp(ctx, "isel", static_cast<int>(i.cls), fn); + Ref r1 = newtmp(ctx, "isel", static_cast<int>(i.cls), fn); + int ceq_op = KWIDE(k) ? Oceqd : Oceqs; + emit(ctx, Oand, static_cast<int>(i.cls), i.to, r0, r1); + emit(ctx, ceq_op, static_cast<int>(i.cls), r0, i.arg[0], i.arg[0]); + fixarg2(ctx, k, fn); + emit(ctx, ceq_op, static_cast<int>(i.cls), r1, i.arg[1], i.arg[1]); + fixarg2(ctx, k, fn); + return; + } + + // Cfne: a != b -> !(a == b). + if (fop == Cfne) { + i.op = static_cast<uint>(KWIDE(k) ? Oceqd : Oceqs); + negate_to(ctx, &i.to, fn); + } + // Remaining Cfeq/Cfge/Cfgt/Cfle/Cflt. + + emiti(ctx, i); + fixarg2(ctx, k, fn); +} + +// Load/store constant offset folding. + +Ref new_offset_mem(Ctx* ctx, Fn* fn, Ref base, int64_t off) { + vensure(ctx, &fn->mem, static_cast<ulong>(fn->nmem + 1), sizeof(Mem), PFn); + Mem* m = &fn->mem[fn->nmem]; + *m = {}; + m->offset.type = Con::CBits; + m->offset.bits.i = off; + m->base = base; + m->index = Ref::none(); + m->scale = 1; + return Ref::mem(static_cast<uint>(fn->nmem++)); +} + +// Reverse fold Oaddr -> CAddr. +[[maybe_unused]] void fold_addr_into_mem(Ctx* ctx, Ref* paddr, uint bid, Fn* fn) { + if (rtype(*paddr) == RTmp) { + Tmp* t = &fn->tmp[paddr->val]; + Ins* def = t->def; + if (!def || static_cast<int>(def->op) != Oaddr) + return; + if (t->nuse != 1 || t->bid != bid) + return; + if (rtype(def->arg[0]) != RCon) + return; + Con* c = &fn->con[def->arg[0].val]; + if (c->type != Con::CAddr) + return; + *paddr = def->arg[0]; + def->op = Onop; + return; + } + if (rtype(*paddr) == RMem) { + Mem* m = &fn->mem[paddr->val]; + if (rtype(m->base) != RTmp) + return; + if (!req(m->index, Ref::none())) + return; + if (m->offset.type != Con::CBits) + return; + Tmp* t = &fn->tmp[m->base.val]; + Ins* def = t->def; + if (!def || static_cast<int>(def->op) != Oaddr) + return; + if (t->nuse != 1 || t->bid != bid) + return; + if (rtype(def->arg[0]) != RCon) + return; + Con* c = &fn->con[def->arg[0].val]; + if (c->type != Con::CAddr) + return; + // Create new CAddr combining bits. + vgrow(ctx, &fn->con, static_cast<ulong>(fn->ncon + 1)); + Con* nc = &fn->con[fn->ncon]; + *nc = *c; + nc->bits.i = c->bits.i + m->offset.bits.i; + uint cidx = static_cast<uint>(fn->ncon++); + *paddr = Ref::con(cidx); + def->op = Onop; + return; + } +} + +void fold_mem_offset(Ctx* ctx, Ref* paddr, uint bid, Fn* fn) { + if (rtype(*paddr) != RTmp) + return; + Tmp* t = &fn->tmp[paddr->val]; + Ins* def = t->def; + if (!def || static_cast<int>(def->op) != Oadd || static_cast<int>(def->cls) != Kl) + return; + if (t->nuse != 1 || t->bid != bid) + return; + + // Oadd is commutative. + Ref base, koff; + if (rtype(def->arg[0]) == RTmp && rtype(def->arg[1]) == RCon) { + base = def->arg[0]; + koff = def->arg[1]; + } else if (rtype(def->arg[0]) == RCon && rtype(def->arg[1]) == RTmp) { + base = def->arg[1]; + koff = def->arg[0]; + } else { + return; + } + + Con* c = &fn->con[koff.val]; + if (c->type != Con::CBits) + return; + int64_t off = c->bits.i; + if (off < -2048 || off > 2047) + return; + + // base must be a normal tmp (value is address). + if (fn->tmp[base.val].slot != -1) + return; + + *paddr = new_offset_mem(ctx, fn, base, off); + def->op = Onop; +} + +// Single instruction selection. + +void sel(Ctx* ctx, Ins i, Blk* b, Fn* fn) { + // Oalloc{4,8,16} non-constant sizes. + if (INRANGE(i.op, Oalloc, Oalloc1)) { + Ins* head = ctx->curi - 1; + salloc(ctx, i.to, i.arg[0], fn); + fixarg(ctx, &head->arg[0], Kl, head, fn); + return; + } + + int ck, cc; + if (iscmp(static_cast<int>(i.op), &ck, &cc)) { + selcmp(ctx, i, ck, cc, fn); + return; + } + + // Consume Onop. + if (i.op == Onop) + return; + + // Oaddr from IRGen. + if (i.op == Oaddr) { + emiti(ctx, i); + return; + } + + // Load/store constant offset folding. + int op = static_cast<int>(i.op); + Ref* paddr = nullptr; + if (isload(op)) + paddr = &i.arg[0]; + else if (isstore(op)) + paddr = &i.arg[1]; + if (paddr) { + fold_mem_offset(ctx, paddr, b->id, fn); + } + + emiti(ctx, i); + Ins* h = ctx->curi; // capture head. + fixarg(ctx, &h->arg[0], argcls(&i, 0), h, fn); + fixarg(ctx, &h->arg[1], argcls(&i, 1), h, fn); +} + +// Jump selection. + +// cmp+jnz fusion. +inline short cmp_to_jjfi(int cc) { + assert(cc >= 0 && cc < NCmpI); + return static_cast<short>(Jjfieq + cc); +} + +void seljmp(Ctx* ctx, Blk* b, Fn* fn) { + if (b->jmp.type != Jjnz) + return; + + if (rtype(b->jmp.arg) == RTmp) { + Tmp* t = &fn->tmp[b->jmp.arg.val]; + Ins* def = t->def; + if (t->nuse == 1 && def && t->bid == b->id) { + int ck, cc; + if (iscmp(static_cast<int>(def->op), &ck, &cc) && cc < NCmpI) { + b->jmp.type = cmp_to_jjfi(cc); + b->jmp.cls = static_cast<short>(ck); + b->jmp.arg = def->arg[0]; + b->jmp.arg2 = def->arg[1]; + def->op = Onop; + fixarg(ctx, &b->jmp.arg, ck, nullptr, fn); + fixarg(ctx, &b->jmp.arg2, ck, nullptr, fn); + return; + } + } + } + fixarg(ctx, &b->jmp.arg, Kw, nullptr, fn); +} + +// Pass A: fast alloc -> slot. + +void assign_fast_alloc_slots(Fn* fn) { + Blk* b = fn->start; + + int align_bytes = 4; + for (int op = Oalloc; op <= Oalloc1; ++op, align_bytes *= 2) { + Ins* ie = b->ins + b->nins; + for (Ins* i = b->ins; i < ie; ++i) { + if (static_cast<int>(i->op) != op) + continue; + if (rtype(i->arg[0]) != RCon) + continue; + + int64_t sz = fn->con[i->arg[0].val].bits.i; + if (sz < 0 || sz >= INT_MAX - 15) + err(const_cast<char*>("invalid alloc size %" PRId64), sz); + + sz = (sz + align_bytes - 1) & -align_bytes; + sz /= 4; + if (sz > INT_MAX - fn->slot) + die("alloc too large"); + + fn->tmp[i->to.val].slot = fn->slot; + fn->slot += static_cast<int>(sz); + + *i = Ins{}; + i->op = Onop; + } + } +} + +} // namespace + +// rv64_isel: Main entry point. + +void rv64_isel(Ctx* ctx, Fn* fn) { + assign_fast_alloc_slots(fn); + + for (Blk* b = fn->start; b; b = b->link) { + ctx->curi = ctx->insb + NIns; + + // Phi nodes of successors. + Blk* succ[3] = {b->s1, b->s2, nullptr}; + for (Blk** ps = succ; *ps; ++ps) { + for (Phi* p = (*ps)->phi; p; p = p->link) { + uint n = 0; + while (p->blk[n] != b) { + ++n; + assert(n < p->narg); + } + fixarg(ctx, &p->arg[n], static_cast<int>(p->cls), nullptr, fn); + } + } + + seljmp(ctx, b, fn); + + // Reverse iteration for emit prepending. + for (Ins* i = b->ins + b->nins; i != b->ins;) { + --i; + sel(ctx, *i, b, fn); + } + + idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi)); + } + + if (debug['I']) { + fprintf(ctx->dbg_buf, "\n> After instruction selection:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +} // namespace rcc diff --git a/src/backend/rv64/targ.cc b/src/backend/rv64/targ.cc new file mode 100644 index 0000000..4d0ab8c --- /dev/null +++ b/src/backend/rv64/targ.cc @@ -0,0 +1,142 @@ +/* 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 targ.cc +/// @brief RISC-V 64 lp64d target descriptor. + +#include "rcc/rv64.h" + +namespace rcc { + +// Per-op attribute table. +Rv64Op rv64_op[NOp]; + +namespace { +struct Rv64OpInit { + Rv64OpInit() { + for (size_t i = 0; i < NOp; ++i) + rv64_op[i].imm = kops[i].imm; + } +} rv64_op_init_; +} // namespace + +// Caller-save list for rega/spill scanning. + +int rv64_rsave[] = { + // GP segment (NGPS = 14): args + temps + A0, + A1, + A2, + A3, + A4, + A5, + A6, + A7, + T0, + T1, + T2, + T3, + T4, + T5, + + // FP segment (NFPS = 19): temps + args + FT0, + FT1, + FT2, + FT3, + FT4, + FT5, + FT6, + FT7, + FT8, + FT9, + FT10, + FA0, + FA1, + FA2, + FA3, + FA4, + FA5, + FA6, + FA7, + + -1, +}; + +// Callee-save list for prologue/epilogue generation. + +int rv64_rclob[] = { + S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11, FS0, + FS1, FS2, FS3, FS4, FS5, FS6, FS7, FS8, FS9, FS10, FS11, -1, +}; + +// Global live registers bitmask. + +namespace { +constexpr bits kRv64GlobLive = BIT(FP) | BIT(SP) | BIT(GP) | BIT(TP) | BIT(RA); + +// RV64 ISA has no memory operand instructions. +int memargs_none(int /*op*/) { + return 0; +} +} // namespace + +// Target descriptor. + +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++20-designator" +#endif + +Target T_rv64 = { + .name = "rv64", + .apple = 0, + .windows = 0, + .gpr0 = T0, + .ngpr = NGPR, + .fpr0 = FT0, + .nfpr = NFPR, + .rglob = kRv64GlobLive, + .nrglob = 5, + .rsave = rv64_rsave, + .nrsave = {NGPS, NFPS}, + .retregs = rv64_retregs, + .argregs = rv64_argregs, + .memargs = memargs_none, + .abi0 = elimsb, + .abi1 = rv64_abi, + .isel = rv64_isel, + .emitfn = rv64_emitfn, + .emitfin = rv64_emitfin, + .asloc = ".L", + .assym = "", + .cansel = 0, +}; + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +// Table size consistency checks. + +MAKESURE(rv64_rsave_size_ok, + sizeof(rv64_rsave) / sizeof(int) == static_cast<size_t>(NGPS + NFPS + 1)); +MAKESURE(rv64_rclob_size_ok, sizeof(rv64_rclob) / sizeof(int) == static_cast<size_t>(NCLR + 1)); + +} // namespace rcc diff --git a/src/backend/simpl.cc b/src/backend/simpl.cc new file mode 100644 index 0000000..0ec2082 --- /dev/null +++ b/src/backend/simpl.cc @@ -0,0 +1,176 @@ +/* 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 backend/simpl.cc +/// @brief Frontend simplification pass. + +#include "rcc/all.h" + +#include <cinttypes> +#include <climits> +#include <cstdlib> + +namespace rcc { + +// Memory block copy expansion: Oblit pair -> explicit load/store chain. +namespace { + +struct BlitTier { + int store_op; + int load_op; + int load_cls; + int width; +}; + +constexpr BlitTier kBlitTiers[] = { + {Ostorel, Oload, Kl, 8}, + {Ostorew, Oload, Kw, 4}, + {Ostoreh, Oloaduh, Kw, 2}, + {Ostoreb, Oloadub, Kw, 1}, +}; + +void blit(Ctx* ctx, Ref sd[2], int signed_sz, Fn* fn) { + bool fwd = signed_sz >= 0; + int sz = std::abs(signed_sz); + int off = fwd ? sz : 0; + + for (const BlitTier& t : kBlitTiers) { + while (sz >= t.width) { + if (fwd) + off -= t.width; + + Ref val = newtmp(ctx, "blt", Kl, fn); + Ref dst_addr = newtmp(ctx, "blt", Kl, fn); + Ref off_con = getcon(ctx, off, fn); + + // Reverse emit: store <- dst_addr <- load <- src_addr. + emit(ctx, t.store_op, 0, Ref::none(), val, dst_addr); + emit(ctx, Oadd, Kl, dst_addr, sd[1], off_con); + + Ref src_addr = newtmp(ctx, "blt", Kl, fn); + emit(ctx, t.load_op, t.load_cls, val, src_addr, Ref::none()); + emit(ctx, Oadd, Kl, src_addr, sd[0], off_con); + + if (!fwd) + off += t.width; + sz -= t.width; + } + } +} + +inline bool is_pow2(uint64_t v) { + return v != 0 && (v & (v - 1)) == 0; +} + +// Single instruction simplification: Oblit1 / Oudiv / Ourem. +void simplify_one(Ctx* ctx, Ins** pi, bool* rebuilt, Blk* b, Fn* fn) { + Ins* i = *pi; + + switch (i->op) { + case Oblit1: { + assert(i > b->ins); + assert((i - 1)->op == Oblit0); + + // First rebuild trigger: move unprocessed tail to insb. + if (!*rebuilt) { + ctx->curi = ctx->insb + NIns; + ulong tail = static_cast<ulong>((b->ins + b->nins) - (i + 1)); + ctx->curi -= tail; + icpy(ctx->curi, i + 1, tail); + *rebuilt = true; + } + + blit(ctx, (i - 1)->arg, rsval(i->arg[0]), fn); + *pi = i - 1; // Retreat pointer to Oblit0. + return; + } + + case Oudiv: + case Ourem: { + // Unsigned integer + constant divisor + pow2. + if (KBASE(i->cls) != 0) + break; + if (rtype(i->arg[1]) != RCon) + break; + Con* c = &fn->con[i->arg[1].val]; + if (c->type != Con::CBits) + break; + if (!is_pow2(static_cast<uint64_t>(c->bits.i))) + break; + + int n = lowest_bit(static_cast<bits>(c->bits.i)); + if (i->op == Ourem) { + i->op = Oand; + i->arg[1] = getcon(ctx, (1LL << n) - 1, fn); + } else { + i->op = Oshr; + i->arg[1] = getcon(ctx, n, fn); + } + break; + } + + default: + break; + } + + if (*rebuilt) + emiti(ctx, *i); +} + +} // namespace + +void simpl(Ctx* ctx, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) { + bool rebuilt = false; + for (Ins* i = b->ins + b->nins; i != b->ins;) { + --i; + simplify_one(ctx, &i, &rebuilt, b, fn); + } + if (rebuilt) + idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi)); + } +} + +// salloc: Dynamic alloca expanded on stack. Called by rv64_isel. + +void salloc(Ctx* ctx, Ref rt, Ref rs, Fn* fn) { + fn->dynalloc = 1; + + if (rtype(rs) == RCon) { + int64_t sz = fn->con[rs.val].bits.i; + if (sz < 0 || sz >= INT_MAX - 15) + err(const_cast<char*>("invalid alloc size %" PRId64), sz); + sz = (sz + 15) & -16; + emit(ctx, Osalloc, Kl, rt, getcon(ctx, sz, fn), Ref::none()); + return; + } + + // Non-constant size path. + if (rtype(rs) == RTmp && fn->tmp[rs.val].slot != -1) + err(const_cast<char*>("unlikely alloc argument %%%s for %%%s"), fn->tmp[rs.val].name, + fn->tmp[rt.val].name); + + Ref r0 = newtmp(ctx, "isel", Kl, fn); + Ref r1 = newtmp(ctx, "isel", Kl, fn); + emit(ctx, Osalloc, Kl, rt, r0, Ref::none()); + emit(ctx, Oand, Kl, r0, r1, getcon(ctx, -16, fn)); + emit(ctx, Oadd, Kl, r1, rs, getcon(ctx, 15, fn)); +} + +} // namespace rcc diff --git a/src/backend/spill.cc b/src/backend/spill.cc new file mode 100644 index 0000000..360dcb3 --- /dev/null +++ b/src/backend/spill.cc @@ -0,0 +1,600 @@ +/* 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 backend/spill.cc +/// @brief Register spilling pass. + +#include "rcc/all.h" + +#include <algorithm> +#include <climits> +#include <cstdio> +#include <vector> + +namespace rcc { + +// fillcost + +namespace { + +// loopiter callback: aggregate gen/nlive of loop body to loop header. +void aggreg(Blk* hd, Blk* b) { + bsunion(hd->gen, b->gen); + for (int k = 0; k < 2; ++k) + if (b->nlive[k] > hd->nlive[k]) + hd->nlive[k] = b->nlive[k]; +} + +// Single Ref use accumulation. +void tmpuse(Ref r, int use, int loop, Fn* fn) { + if (rtype(r) == RMem) { + Mem* m = &fn->mem[r.val]; + tmpuse(m->base, 1, loop, fn); + tmpuse(m->index, 1, loop, fn); + return; + } + if (rtype(r) == RTmp && r.val >= Tmp0) { + Tmp* t = &fn->tmp[r.val]; + t->nuse += static_cast<uint>(use); + t->ndef += static_cast<uint>(!use); + t->cost += static_cast<uint>(loop); + } +} + +void accumulate_phi_costs(Blk* b, Fn* fn) { + for (Phi* p = b->phi; p; p = p->link) { + Tmp* t = &fn->tmp[p->to.val]; + tmpuse(p->to, 0, 0, fn); + for (uint a = 0; a < p->narg; ++a) { + int n = p->blk[a]->loop; + t->cost += static_cast<uint>(n); + tmpuse(p->arg[a], 1, n, fn); + } + } +} + +void accumulate_ins_costs(Blk* b, Fn* fn) { + int n = b->loop; + for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) { + tmpuse(i->to, 0, n, fn); + tmpuse(i->arg[0], 1, n, fn); + tmpuse(i->arg[1], 1, n, fn); + } + tmpuse(b->jmp.arg, 1, n, fn); + tmpuse(b->jmp.arg2, 1, n, fn); // Second operand for Jjfi/Jjff. +} + +void dump_loop_info(Ctx* ctx, Fn* fn) { + FILE* f = ctx->dbg_buf; + fprintf(f, "\n> Loop information:\n"); + for (Blk* b = fn->start; b; b = b->link) { + uint a; + for (a = 0; a < b->npred; ++a) + if (b->id <= b->pred[a]->id) + break; + if (a != b->npred) { + fprintf(f, "\t%-10s", b->name); + fprintf(f, " (% 3d ", b->nlive[0]); + fprintf(f, "% 3d) ", b->nlive[1]); + dumpts(b->gen, fn->tmp, f); + } + } +} + +} // namespace + +void fillcost(Ctx* ctx, Fn* fn) { + loopiter(fn, aggreg); + + if (debug['S']) + dump_loop_info(ctx, fn); + + // Register cost = UINT_MAX (never spill); normal tmp accumulates from 0. + for (Tmp* t = fn->tmp; t - fn->tmp < fn->ntmp; ++t) { + t->cost = (t - fn->tmp < Tmp0) ? UINT_MAX : 0; + t->nuse = 0; + t->ndef = 0; + } + + for (Blk* b = fn->start; b; b = b->link) { + accumulate_phi_costs(b, fn); + accumulate_ins_costs(b, fn); + } + + if (debug['S']) { + FILE* f = ctx->dbg_buf; + fprintf(f, "\n> Spill costs:\n"); + for (int n = Tmp0; n < fn->ntmp; ++n) + fprintf(f, "\t%-10s %d\n", fn->tmp[n].name, fn->tmp[n].cost); + fprintf(f, "\n"); + } +} + +// spill + +namespace { + +// Per-call state. +struct SpillCtx { + Ctx* ctx; // Back reference to ctx. + Tmp* tmp; // Cache of fn->tmp. + int ntmp; + int locs; // fn->slot entrance snapshot. + int slot4; // Next 4B slot. + int slot8; // Next 8B slot. + BSet mask[2]; // GPR and FPR bitsets. + BSet b2_scratch; // Scratch buffer for limit2. + std::vector<int> tarr; // Scratch buffer for limit. +}; + +// Allocate stack slot on demand. +Ref alloc_spill_slot(SpillCtx& s, int t) { + assert(t >= Tmp0 && "cannot spill register"); + int existing = s.tmp[t].slot; + if (existing != -1) + return Ref::slot(existing); + + int pos; + if (KWIDE(s.tmp[t].cls)) { + pos = s.slot8; + if (s.slot4 == s.slot8) + s.slot4 += 2; + s.slot8 += 2; + } else { + pos = s.slot4; + if (s.slot4 == s.slot8) { + s.slot8 += 2; + s.slot4 += 1; + } else { + // 4B hole filled, jump to slot8. + s.slot4 = s.slot8; + } + } + pos += s.locs; + s.tmp[t].slot = pos; + return Ref::slot(pos); +} + +// Limit bitset to at most k elements. +void limit_one(SpillCtx& s, BSet* b, int k, BSet* f) { + int nt = static_cast<int>(bscount(b)); + if (nt <= k) + return; + + s.tarr.resize(static_cast<size_t>(nt)); + + // Pull elements from b to tarr, clear b. + int collected = 0; + int t = 0; + while (bsiter(b, &t)) { + bsclr(b, static_cast<uint>(t)); + s.tarr[static_cast<size_t>(collected++)] = t; + ++t; + } + assert(collected == nt); + + if (nt > 1) { + Tmp* tmp = s.tmp; + if (f) { + BSet* fst = f; + std::stable_sort(s.tarr.begin(), s.tarr.begin() + nt, [fst, tmp](int x, int y) { + int fx = bshas(fst, static_cast<uint>(x)); + int fy = bshas(fst, static_cast<uint>(y)); + if (fx != fy) + return fx > fy; + return tmp[x].cost > tmp[y].cost; + }); + } else { + std::stable_sort(s.tarr.begin(), s.tarr.begin() + nt, + [tmp](int x, int y) { return tmp[x].cost > tmp[y].cost; }); + } + } + + int i = 0; + for (; i < k && i < nt; ++i) + bsset(b, static_cast<uint>(s.tarr[static_cast<size_t>(i)])); + for (; i < nt; ++i) + alloc_spill_slot(s, s.tarr[static_cast<size_t>(i)]); +} + +// Limit GPR/FPR separately. +void limit_two(SpillCtx& s, BSet* b1, int k1, int k2, BSet* f) { + BSet* b2 = &s.b2_scratch; + bscopy(b2, b1); + bsinter(b1, &s.mask[0]); + bsinter(b2, &s.mask[1]); + limit_one(s, b1, T.ngpr - k1, f); + { + int cnt = static_cast<int>(bscount(b2)); + if (cnt > s.ctx->spill_fp_live_peak) + s.ctx->spill_fp_live_peak = cnt; + } + limit_one(s, b2, T.nfpr - k2, f); + bsunion(b1, b2); +} + +// Set register hint for tmp in u. +void sethint(SpillCtx& s, BSet* u, bits r) { + int t = Tmp0; + while (bsiter(u, &t)) { + s.tmp[phicls(t, s.tmp)].hint.m |= r; + ++t; + } +} + +// Emit Oload for tmp in u \ v. +void reloads(Ctx* ctx, SpillCtx& s, BSet* u, BSet* v) { + int t = Tmp0; + while (bsiter(u, &t)) { + if (!bshas(v, static_cast<uint>(t))) + emit(ctx, Oload, s.tmp[t].cls, Ref::tmp(static_cast<uint>(t)), alloc_spill_slot(s, t), + Ref::none()); + ++t; + } +} + +// Emit Ostore to SLOT(s) if s != -1. +void store_to_slot(Ctx* ctx, SpillCtx& spc, Ref r, int slot_id) { + if (slot_id == -1) + return; + int cls = spc.tmp[r.val].cls; + emit(ctx, Ostorew + cls, 0, Ref::none(), r, Ref::slot(slot_id)); +} + +inline bool is_regcopy(Ins* i) { + return i->op == Ocopy && isreg(i->arg[0]); +} + +// Process parallel moves (contiguous reg->tmp copies). +Ins* spill_par_moves(Ctx* ctx, SpillCtx& s, Blk* b, Ins* i, BSet* v) { + BSet u[1]; + bsinit(ctx, u, static_cast<uint>(s.ntmp)); + + // Backward scan of contiguous regcpy segment. + Ins* i1 = ++i; + do { + --i; + int t = i->to.val; + if (!req(i->to, Ref::none())) + if (bshas(v, static_cast<uint>(t))) { + bsclr(v, static_cast<uint>(t)); + store_to_slot(ctx, s, i->to, s.tmp[t].slot); + } + bsset(v, i->arg[0].val); + } while (i != b->ins && is_regcopy(i - 1)); + + bscopy(u, v); + + bits r; + if (i != b->ins && (i - 1)->op == Ocall) { + // Call site: return registers are dead, argregs must be preserved. + Ins* call = i - 1; + v->t[0] &= ~T.retregs(call->arg[1], nullptr); + limit_two(s, v, T.nrsave[0], T.nrsave[1], nullptr); + r = 0; + for (int n = 0; T.rsave[n] >= 0; ++n) + r |= BIT(T.rsave[n]); + v->t[0] |= T.argregs(call->arg[1], nullptr); + } else { + limit_two(s, v, 0, 0, nullptr); + r = v->t[0]; + } + sethint(s, v, r); + reloads(ctx, s, u, v); + + // Re-emit original regcpy in the segment. + do + emiti(ctx, *--i1); + while (i1 != i); + return i; +} + +// Loop header path merge. +void merge_pred(SpillCtx& s, BSet* u, Blk* bu, BSet* v, Blk* bv) { + if (bu->loop <= bv->loop) { + bsunion(u, v); + return; + } + int t = 0; + while (bsiter(v, &t)) { + if (s.tmp[t].slot == -1) + bsset(u, static_cast<uint>(t)); + ++t; + } +} + +// Compute exit set v. +void compute_exit_set(SpillCtx& s, Blk* b, BSet* u, BSet* v, BSet* w) { + Blk* s1 = b->s1; + Blk* s2 = b->s2; + Blk* hd = nullptr; + if (s1 && s1->id <= b->id) + hd = s1; + if (s2 && s2->id <= b->id) + if (!hd || s2->id >= hd->id) + hd = s2; + + if (hd) { + // Back-edge handling. + bszero(v); + hd->gen->t[0] |= T.rglob; // Global registers never spill. + for (int k = 0; k < 2; ++k) { + uint n = (k == 0) ? static_cast<uint>(T.ngpr) : static_cast<uint>(T.nfpr); + bscopy(u, b->out); + bsinter(u, &s.mask[k]); + bscopy(w, u); + bsinter(u, hd->gen); + bsdiff(w, hd->gen); + if (bscount(u) < n) { + int j = static_cast<int>(bscount(w)); // Live-through. + int l = hd->nlive[k]; + if (k == 1 && static_cast<int>(bscount(w)) > s.ctx->spill_fp_live_peak) + s.ctx->spill_fp_live_peak = static_cast<int>(bscount(w)); + limit_one(s, w, static_cast<int>(n) - (l - j), nullptr); + bsunion(u, w); + } else { + if (k == 1 && static_cast<int>(bscount(u)) > s.ctx->spill_fp_live_peak) + s.ctx->spill_fp_live_peak = static_cast<int>(bscount(u)); + limit_one(s, u, static_cast<int>(n), nullptr); + } + bsunion(v, u); + } + } else if (s1) { + // Forward edge handling. + bszero(v); + liveon(w, b, s1); + merge_pred(s, v, b, w, s1); + if (s2) { + liveon(u, b, s2); + merge_pred(s, v, b, u, s2); + bsinter(w, u); + } + limit_two(s, v, 0, 0, w); + } else { + bscopy(v, b->out); + if (rtype(b->jmp.arg) == RCall) + v->t[0] |= T.retregs(b->jmp.arg, nullptr); + } +} + +// Single instruction processing in main loop. + +// Process i->to. +void process_def(Ins* i, BSet* v, BSet* w) { + if (req(i->to, Ref::none())) + return; + assert(rtype(i->to) == RTmp); + uint t = i->to.val; + if (bshas(v, t)) { + bsclr(v, t); + } else { + // Dead def. + assert(t >= Tmp0 && "dead reg"); + bsset(v, t); + bsset(w, t); + } +} + +// Process args. +void process_args(Ins* i, BSet* v, BSet* w, int lvarg[2], Fn* fn) { + int memquota = T.memargs(static_cast<int>(i->op)); + for (int n = 0; n < 2; ++n) + if (rtype(i->arg[n]) == RMem) + --memquota; + + for (int n = 0; n < 2; ++n) { + switch (rtype(i->arg[n])) { + case RMem: { + uint t = i->arg[n].val; + Mem* m = &fn->mem[t]; + if (rtype(m->base) == RTmp) { + bsset(v, m->base.val); + bsset(w, m->base.val); + } + if (rtype(m->index) == RTmp) { + bsset(v, m->index.val); + bsset(w, m->index.val); + } + break; + } + case RTmp: { + uint t = i->arg[n].val; + lvarg[n] = bshas(v, t); + bsset(v, t); + if (memquota-- <= 0) + bsset(w, t); + break; + } + default: + break; + } + } +} + +// Rewrite evicted args after limit2. +void rewrite_evicted_args(SpillCtx& s, Ins* i, BSet* u, BSet* v, const int lvarg[2]) { + for (int n = 0; n < 2; ++n) { + if (rtype(i->arg[n]) != RTmp) + continue; + uint t = i->arg[n].val; + if (!bshas(v, t)) { + if (!lvarg[n]) + bsclr(u, t); + i->arg[n] = alloc_spill_slot(s, static_cast<int>(t)); + } + } +} + +// Process jmp args. +void process_jmp_arg(SpillCtx& s, Blk* b, BSet* v) { + bool has1 = rtype(b->jmp.arg) == RTmp; + bool has2 = rtype(b->jmp.arg2) == RTmp; + if (!has1 && !has2) + return; + int t1 = has1 ? static_cast<int>(b->jmp.arg.val) : -1; + int t2 = has2 ? static_cast<int>(b->jmp.arg2.val) : -1; + if (has1) { + assert(KBASE(s.tmp[t1].cls) == 0); + bsset(v, static_cast<uint>(t1)); + } + if (has2) { + assert(KBASE(s.tmp[t2].cls) == 0); + bsset(v, static_cast<uint>(t2)); + } + limit_two(s, v, 0, 0, nullptr); + if (has1 && !bshas(v, static_cast<uint>(t1))) + b->jmp.arg = alloc_spill_slot(s, t1); + if (has2 && !bshas(v, static_cast<uint>(t2))) + b->jmp.arg2 = alloc_spill_slot(s, t2); +} + +// Slot unselected outs. +void slot_unselected_outs(SpillCtx& s, Blk* b, BSet* v) { + int t = Tmp0; + while (bsiter(b->out, &t)) { + if (!bshas(v, static_cast<uint>(t))) + alloc_spill_slot(s, t); + ++t; + } +} + +// Process block phis. +void process_block_phis(Ctx* ctx, SpillCtx& s, Blk* b, BSet* v) { + for (Phi* p = b->phi; p; p = p->link) { + assert(rtype(p->to) == RTmp); + uint t = p->to.val; + if (bshas(v, t)) { + bsclr(v, t); + store_to_slot(ctx, s, p->to, s.tmp[t].slot); + } else if (bshas(b->in, t)) { + p->to = alloc_spill_slot(s, static_cast<int>(t)); + } + } +} + +void dump_spill_result(Ctx* ctx, Fn* fn) { + FILE* f = ctx->dbg_buf; + fprintf(f, "\n> Block information:\n"); + for (Blk* b = fn->start; b; b = b->link) { + fprintf(f, "\t%-10s (% 5d) ", b->name, b->loop); + dumpts(b->out, fn->tmp, f); + } + fprintf(f, "\n> After spilling:\n"); + printfn(fn, f); +} + +} // namespace + +// spill main entry. + +void spill(Ctx* ctx, Fn* fn) { + SpillCtx s{}; + s.ctx = ctx; + s.tmp = fn->tmp; + s.ntmp = fn->ntmp; + s.locs = fn->slot; + s.slot4 = 0; + s.slot8 = 0; + + uint n = static_cast<uint>(s.ntmp); + bsinit(ctx, &s.mask[0], n); + bsinit(ctx, &s.mask[1], n); + bsinit(ctx, &s.b2_scratch, n); + + BSet u[1], v[1], w[1]; + bsinit(ctx, u, n); + bsinit(ctx, v, n); + bsinit(ctx, w, n); + + // Mask initialization. + for (int t = 0; t < s.ntmp; ++t) { + int k = 0; + if (t >= T.fpr0 && t < T.fpr0 + T.nfpr) + k = 1; + if (t >= Tmp0) + k = KBASE(s.tmp[t].cls); + bsset(&s.mask[k], static_cast<uint>(t)); + } + + // RPO reverse order iteration. + for (Blk** bp = &fn->rpo[fn->nblk]; bp != fn->rpo;) { + Blk* b = *--bp; + + // Determine exit set v. + ctx->curi = nullptr; // emit/reloads disabled here. + compute_exit_set(s, b, u, v, w); + process_jmp_arg(s, b, v); + slot_unselected_outs(s, b, v); + bscopy(b->out, v); + + // Reverse process instructions, fill insb. + ctx->curi = ctx->insb + NIns; + for (Ins* i = &b->ins[b->nins]; i != b->ins;) { + --i; + if (is_regcopy(i)) { + i = spill_par_moves(ctx, s, b, i, v); + continue; + } + + bszero(w); + int lvarg[2] = {0, 0}; + process_def(i, v, w); + process_args(i, v, w, lvarg, fn); + + bscopy(u, v); + limit_two(s, v, 0, 0, w); + rewrite_evicted_args(s, i, u, v, lvarg); + reloads(ctx, s, u, v); + + if (!req(i->to, Ref::none())) { + uint t = i->to.val; + store_to_slot(ctx, s, i->to, s.tmp[t].slot); + if (t >= Tmp0) + bsclr(v, t); + } + emiti(ctx, *i); + + bits r = v->t[0]; // Low 64 bits are physical registers. + if (r) + sethint(s, v, r); + } + + // Entry handling. + if (b == fn->start) { + assert(v->t[0] == (T.rglob | fn->reg)); + } else { + assert(v->t[0] == T.rglob); + } + + // Block entry phis. + process_block_phis(ctx, s, b, v); + bscopy(b->in, v); + + idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi)); + } + + // 16-byte alignment. + s.slot8 += s.slot8 & 3; + fn->slot += s.slot8; + + if (debug['S']) + dump_spill_result(ctx, fn); +} + +} // namespace rcc diff --git a/src/frontend/ast.cc b/src/frontend/ast.cc new file mode 100644 index 0000000..c82272a --- /dev/null +++ b/src/frontend/ast.cc @@ -0,0 +1,214 @@ +/* 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 ast.cc +/// @brief AST debug printing and built-in function registration. + +#include "rcc/ast.h" + +#include "rcc/symtab.h" + +#include <cstdio> + +namespace rcc { + +// AST debug printing + +static void indent(int n) { + for (int i = 0; i < n; i++) + fprintf(stderr, " "); +} + +void ast_dump(const ASTNode* node, int depth) { + if (!node) { + indent(depth); + fprintf(stderr, "(null)\n"); + return; + } + + indent(depth); + switch (node->kind) { + case NodeKind::CompUnit: + fprintf(stderr, "CompUnit\n"); + for (auto& c : static_cast<const CompUnitNode*>(node)->decls_and_funcs) + ast_dump(c.get(), depth + 1); + break; + + case NodeKind::FuncDef: { + auto* f = static_cast<const FuncDefNode*>(node); + fprintf(stderr, "FuncDef '%s' -> %s\n", f->name.c_str(), btype_str(f->ret_type)); + for (auto& p : f->params) + ast_dump(p.get(), depth + 1); + ast_dump(f->body.get(), depth + 1); + break; + } + + case NodeKind::FuncFParam: { + auto* p = static_cast<const FuncFParamNode*>(node); + fprintf(stderr, "Param '%s' : %s%s\n", p->name.c_str(), btype_str(p->btype), + p->is_array ? "[]" : ""); + break; + } + + case NodeKind::VarDecl: { + auto* d = static_cast<const VarDeclNode*>(node); + fprintf(stderr, "%s %s\n", d->is_const ? "ConstDecl" : "VarDecl", btype_str(d->btype)); + for (auto& def : d->defs) + ast_dump(def.get(), depth + 1); + break; + } + + case NodeKind::VarDef: { + auto* d = static_cast<const VarDefNode*>(node); + fprintf(stderr, "VarDef '%s'", d->name.c_str()); + if (!d->dims.empty()) + fprintf(stderr, " (array, %zu dims)", d->dims.size()); + fprintf(stderr, "\n"); + if (d->init_val) + ast_dump(d->init_val.get(), depth + 1); + break; + } + + case NodeKind::InitList: + fprintf(stderr, "InitList\n"); + for (auto& i : static_cast<const InitListNode*>(node)->items) + ast_dump(i.get(), depth + 1); + break; + + case NodeKind::Block: + fprintf(stderr, "Block\n"); + for (auto& i : static_cast<const BlockNode*>(node)->items) + ast_dump(i.get(), depth + 1); + break; + + case NodeKind::AssignStmt: + fprintf(stderr, "Assign\n"); + ast_dump(static_cast<const AssignStmtNode*>(node)->lval.get(), depth + 1); + ast_dump(static_cast<const AssignStmtNode*>(node)->expr.get(), depth + 1); + break; + + case NodeKind::ExprStmt: + fprintf(stderr, "ExprStmt\n"); + if (static_cast<const ExprStmtNode*>(node)->expr) + ast_dump(static_cast<const ExprStmtNode*>(node)->expr.get(), depth + 1); + break; + + case NodeKind::IfStmt: { + auto* s = static_cast<const IfStmtNode*>(node); + fprintf(stderr, "If\n"); + ast_dump(s->cond.get(), depth + 1); + ast_dump(s->then_stmt.get(), depth + 1); + if (s->else_stmt) + ast_dump(s->else_stmt.get(), depth + 1); + break; + } + + case NodeKind::WhileStmt: + fprintf(stderr, "While\n"); + ast_dump(static_cast<const WhileStmtNode*>(node)->cond.get(), depth + 1); + ast_dump(static_cast<const WhileStmtNode*>(node)->body.get(), depth + 1); + break; + + case NodeKind::BreakStmt: + fprintf(stderr, "Break\n"); + break; + case NodeKind::ContinueStmt: + fprintf(stderr, "Continue\n"); + break; + + case NodeKind::ReturnStmt: + fprintf(stderr, "Return\n"); + if (static_cast<const ReturnStmtNode*>(node)->expr) + ast_dump(static_cast<const ReturnStmtNode*>(node)->expr.get(), depth + 1); + break; + + case NodeKind::IntLiteral: + fprintf(stderr, "IntLit %d\n", static_cast<const IntLiteralNode*>(node)->value); + break; + + case NodeKind::FloatLiteral: + fprintf(stderr, "FloatLit %f\n", + static_cast<double>(static_cast<const FloatLiteralNode*>(node)->value)); + break; + + case NodeKind::LVal: { + auto* l = static_cast<const LValNode*>(node); + fprintf(stderr, "LVal '%s'\n", l->name.c_str()); + for (auto& idx : l->indices) + ast_dump(idx.get(), depth + 1); + break; + } + + case NodeKind::FuncCall: { + auto* c = static_cast<const FuncCallNode*>(node); + fprintf(stderr, "Call '%s'\n", c->name.c_str()); + for (auto& a : c->args) + ast_dump(a.get(), depth + 1); + break; + } + + case NodeKind::BinaryExpr: { + auto* e = static_cast<const BinaryExprNode*>(node); + const char* ops[] = {"Add", "Sub", "Mul", "Div", "Mod", "Lt", "Gt", + "Le", "Ge", "Eq", "Ne", "And", "Or"}; + fprintf(stderr, "BinExpr %s\n", ops[static_cast<int>(e->op)]); + ast_dump(e->lhs.get(), depth + 1); + ast_dump(e->rhs.get(), depth + 1); + break; + } + + case NodeKind::UnaryExpr: { + auto* e = static_cast<const UnaryExprNode*>(node); + const char* ops[] = {"Pos", "Neg", "Not"}; + fprintf(stderr, "UnaryExpr %s\n", ops[static_cast<int>(e->op)]); + ast_dump(e->operand.get(), depth + 1); + break; + } + + case NodeKind::ImplicitCast: { + auto* c = static_cast<const ImplicitCastNode*>(node); + fprintf(stderr, "ImplicitCast -> %s\n", btype_str(c->target_type)); + ast_dump(c->operand.get(), depth + 1); + break; + } + } +} + +// built-in functions + +void SymbolTable::register_builtins() { + // I/O functions + register_func({"getint", BType::Int, {}}); + register_func({"getch", BType::Int, {}}); + register_func({"getfloat", BType::Float, {}}); + register_func({"getarray", BType::Int, {{BType::Int, true}}}); + register_func({"getfarray", BType::Int, {{BType::Float, true}}}); + register_func({"putint", BType::Void, {{BType::Int, false}}}); + register_func({"putch", BType::Void, {{BType::Int, false}}}); + register_func({"putfloat", BType::Void, {{BType::Float, false}}}); + register_func({"putarray", BType::Void, {{BType::Int, false}, {BType::Int, true}}}); + register_func({"putfarray", BType::Void, {{BType::Int, false}, {BType::Float, true}}}); + register_func({"putf", BType::Void, {}}); // variable parameters + + // timing functions (actually call _sysy_starttime / _sysy_stoptime) + register_func({"starttime", BType::Void, {}}); + register_func({"stoptime", BType::Void, {}}); +} + +} // namespace rcc diff --git a/src/frontend/lexer.l b/src/frontend/lexer.l new file mode 100644 index 0000000..33359ef --- /dev/null +++ b/src/frontend/lexer.l @@ -0,0 +1,95 @@ +/* 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/>. */ + +%{ +/* lexer.l - SysY2022 Lexer */ +#include "rcc/ast.h" +#include "parser.tab.h" + +#include <cstdlib> +#include <cstring> + +// Line number tracking +int yylineno_val = 1; +#define YY_USER_ACTION yylloc.first_line = yylineno_val; + +using namespace rcc; +%} + +%option noyywrap nounput noinput + +/* Exclusive start condition: multi-line comments */ +%x COMMENT + +%% + + /* ─── Whitespace and Newlines ─── */ +[ \t\r]+ { /* skip whitespace */ } +\n { yylineno_val++; } + + /* ─── Comments ─── */ +"//"[^\n]* { /* single-line comment */ } +"/*" { BEGIN(COMMENT); } +<COMMENT>"*/" { BEGIN(INITIAL); } +<COMMENT>\n { yylineno_val++; } +<COMMENT>. { /* eat anything inside comment */ } + + /* ─── Keywords ─── */ +"int" { return T_INT; } +"float" { return T_FLOAT; } +"void" { return T_VOID; } +"const" { return T_CONST; } +"if" { return T_IF; } +"else" { return T_ELSE; } +"while" { return T_WHILE; } +"break" { return T_BREAK; } +"continue" { return T_CONTINUE; } +"return" { return T_RETURN; } + + /* ─── Integer Constants (Hex > Octal > Decimal, order is important) ─── */ +0[xX][0-9a-fA-F]+ { yylval.int_val = static_cast<int32_t>(strtoul(yytext, nullptr, 16)); return INT_CONST; } +0[0-7]+ { yylval.int_val = static_cast<int32_t>(strtoul(yytext, nullptr, 8)); return INT_CONST; } +[0-9]+ { yylval.int_val = static_cast<int32_t>(strtoul(yytext, nullptr, 10)); return INT_CONST; } + + /* ─── Floating Point Constants (Hex float + Decimal float) ─── */ +0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+([pP][+-]?[0-9]+)? { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; } +0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*([pP][+-]?[0-9]+)? { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; } +0[xX][0-9a-fA-F]+[pP][+-]?[0-9]+ { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; } +[0-9]*\.[0-9]+([eE][+-]?[0-9]+)? { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; } +[0-9]+\.[0-9]*([eE][+-]?[0-9]+)? { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; } +[0-9]+[eE][+-]?[0-9]+ { yylval.float_val = strtof(yytext, nullptr); return FLOAT_CONST; } + + /* ─── Identifiers ─── */ +[a-zA-Z_][a-zA-Z0-9_]* { yylval.str_val = strdup(yytext); return IDENT; } + + /* ─── Multi-character Operators ─── */ +"&&" { return T_AND; } +"||" { return T_OR; } +"<=" { return T_LE; } +">=" { return T_GE; } +"==" { return T_EQ; } +"!=" { return T_NE; } + + /* ─── Format Strings (for putf only) ─── */ +\"[^\"]*\" { yylval.str_val = strdup(yytext); return STRING_CONST; } + + /* ─── Single-character Operators and Delimiters, return ASCII directly ─── */ +. { return yytext[0]; } + +%% diff --git a/src/frontend/parser.y b/src/frontend/parser.y new file mode 100644 index 0000000..b2d5506 --- /dev/null +++ b/src/frontend/parser.y @@ -0,0 +1,686 @@ +/* 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/>. */ +%{ +/* parser.y - SysY2022 Parser */ +#include "rcc/ast.h" + +#include <cstdio> +#include <cstdlib> +#include <memory> +#include <string> +#include <vector> + +using namespace rcc; + +// Global AST root node +extern CompUnitNode* g_ast_root; + +extern int yylex(); +extern int yylineno_val; +void yyerror(const char* msg); +extern FILE* yyin; +%} + +/* ─── Bison Configuration ─── */ +%locations +%define parse.error verbose + +/* ─── Value Types ─── */ +%union { + int32_t int_val; + float float_val; + char* str_val; + rcc::ASTNode* node; + rcc::ASTList* node_list; + rcc::BType btype; +} + +/* ─── Terminals ─── */ +%token <int_val> INT_CONST +%token <float_val> FLOAT_CONST +%token <str_val> IDENT STRING_CONST + +%token T_INT T_FLOAT T_VOID T_CONST +%token T_IF T_ELSE T_WHILE T_BREAK T_CONTINUE T_RETURN +%token T_AND T_OR T_LE T_GE T_EQ T_NE + +/* ─── Non-terminal Types ─── */ +%type <node> CompUnit +%type <node> GlobalItem FuncDef_Void +%type <node> FuncFParam +%type <node> Block BlockItem Stmt +%type <node> Decl ConstDecl VarDecl ConstDef VarDef +%type <node> InitVal ConstInitVal +%type <node> Exp Cond LVal PrimaryExp UnaryExp Number +%type <node> MulExp AddExp RelExp EqExp LAndExp LOrExp ConstExp +%type <node_list> ConstDefList VarDefList +%type <node_list> FuncFParams FuncRParams +%type <node_list> BlockItemList +%type <node_list> ArrayDims ArrayIndices +%type <node_list> InitValList ConstInitValList +%type <btype> BType + +/* ─── Resolve dangling else ─── */ +%nonassoc LOWER_THAN_ELSE +%nonassoc T_ELSE + +/* ─── Start Symbol ─── */ +%start CompUnit + +%% + +/* Compilation Unit */ + +CompUnit + : CompUnit GlobalItem { + auto* unit = static_cast<CompUnitNode*>($1); + unit->decls_and_funcs.push_back(ASTPtr($2)); + $$ = unit; + } + | CompUnit ConstDecl { + auto* unit = static_cast<CompUnitNode*>($1); + unit->decls_and_funcs.push_back(ASTPtr($2)); + $$ = unit; + } + | CompUnit FuncDef_Void { + auto* unit = static_cast<CompUnitNode*>($1); + unit->decls_and_funcs.push_back(ASTPtr($2)); + $$ = unit; + } + | GlobalItem { + auto* unit = new CompUnitNode(); + unit->decls_and_funcs.push_back(ASTPtr($1)); + $$ = unit; + g_ast_root = unit; + } + | ConstDecl { + auto* unit = new CompUnitNode(); + unit->decls_and_funcs.push_back(ASTPtr($1)); + $$ = unit; + g_ast_root = unit; + } + | FuncDef_Void { + auto* unit = new CompUnitNode(); + unit->decls_and_funcs.push_back(ASTPtr($1)); + $$ = unit; + g_ast_root = unit; + } + ; + +/* GlobalItem */ +GlobalItem + : BType IDENT '(' ')' Block { + /* FuncDef: int/float foo() { ... } */ + auto* func = new FuncDefNode(); + func->ret_type = $1; + func->name = $2; free($2); + func->body = ASTPtr($5); + func->lineno = @1.first_line; + $$ = func; + } + | BType IDENT '(' FuncFParams ')' Block { + /* FuncDef: int/float foo(params) { ... } */ + auto* func = new FuncDefNode(); + func->ret_type = $1; + func->name = $2; free($2); + func->params = std::move(*$4); delete $4; + func->body = ASTPtr($6); + func->lineno = @1.first_line; + $$ = func; + } + | BType IDENT ArrayDims ';' { + /* VarDecl: int x; / int x[10]; */ + auto* decl = new VarDeclNode(); + decl->is_const = false; + decl->btype = $1; + auto* def = new VarDefNode(); + def->name = $2; free($2); + def->dims = std::move(*$3); delete $3; + def->lineno = @2.first_line; + decl->defs.push_back(ASTPtr(def)); + $$ = decl; + } + | BType IDENT ArrayDims '=' InitVal ';' { + /* VarDecl: int x = 5; / int x[10] = {1,2}; */ + auto* decl = new VarDeclNode(); + decl->is_const = false; + decl->btype = $1; + auto* def = new VarDefNode(); + def->name = $2; free($2); + def->dims = std::move(*$3); delete $3; + def->init_val = ASTPtr($5); + def->lineno = @2.first_line; + decl->defs.push_back(ASTPtr(def)); + $$ = decl; + } + | BType IDENT ArrayDims ',' VarDefList ';' { + /* VarDecl: int x, y, z; */ + auto* decl = new VarDeclNode(); + decl->is_const = false; + decl->btype = $1; + auto* def = new VarDefNode(); + def->name = $2; free($2); + def->dims = std::move(*$3); delete $3; + def->lineno = @2.first_line; + decl->defs.push_back(ASTPtr(def)); + for (auto& d : *$5) decl->defs.push_back(std::move(d)); + delete $5; + $$ = decl; + } + | BType IDENT ArrayDims '=' InitVal ',' VarDefList ';' { + /* VarDecl: int x = 1, y, z; */ + auto* decl = new VarDeclNode(); + decl->is_const = false; + decl->btype = $1; + auto* def = new VarDefNode(); + def->name = $2; free($2); + def->dims = std::move(*$3); delete $3; + def->init_val = ASTPtr($5); + def->lineno = @2.first_line; + decl->defs.push_back(ASTPtr(def)); + for (auto& d : *$7) decl->defs.push_back(std::move(d)); + delete $7; + $$ = decl; + } + ; + +/* void functions */ +FuncDef_Void + : T_VOID IDENT '(' ')' Block { + auto* func = new FuncDefNode(); + func->ret_type = BType::Void; + func->name = $2; free($2); + func->body = ASTPtr($5); + func->lineno = @1.first_line; + $$ = func; + } + | T_VOID IDENT '(' FuncFParams ')' Block { + auto* func = new FuncDefNode(); + func->ret_type = BType::Void; + func->name = $2; free($2); + func->params = std::move(*$4); delete $4; + func->body = ASTPtr($6); + func->lineno = @1.first_line; + $$ = func; + } + ; + +/* Types */ + +BType : T_INT { $$ = BType::Int; } + | T_FLOAT { $$ = BType::Float; } + ; + +/* Declarations */ + +/* Decl */ +Decl : ConstDecl { $$ = $1; } + | VarDecl { $$ = $1; } + ; + +ConstDecl + : T_CONST BType ConstDefList ';' { + auto* decl = new VarDeclNode(); + decl->is_const = true; + decl->btype = $2; + decl->defs = std::move(*$3); + delete $3; + $$ = decl; + } + ; + +ConstDefList + : ConstDef { + $$ = new ASTList(); + $$->push_back(ASTPtr($1)); + } + | ConstDefList ',' ConstDef { + $1->push_back(ASTPtr($3)); + $$ = $1; + } + ; + +ConstDef + : IDENT ArrayDims '=' ConstInitVal { + auto* def = new VarDefNode(); + def->name = $1; free($1); + def->dims = std::move(*$2); delete $2; + def->init_val = ASTPtr($4); + def->lineno = @1.first_line; + $$ = def; + } + ; + +ConstInitVal + : ConstExp { $$ = $1; } + | '{' '}' { + $$ = new InitListNode(); + } + | '{' ConstInitValList '}' { + auto* list = new InitListNode(); + list->items = std::move(*$2); delete $2; + $$ = list; + } + ; + +ConstInitValList + : ConstInitVal { + $$ = new ASTList(); + $$->push_back(ASTPtr($1)); + } + | ConstInitValList ',' ConstInitVal { + $1->push_back(ASTPtr($3)); + $$ = $1; + } + ; + +/* Local VarDecl */ +VarDecl + : BType VarDefList ';' { + auto* decl = new VarDeclNode(); + decl->is_const = false; + decl->btype = $1; + decl->defs = std::move(*$2); + delete $2; + $$ = decl; + } + ; + +VarDefList + : VarDef { + $$ = new ASTList(); + $$->push_back(ASTPtr($1)); + } + | VarDefList ',' VarDef { + $1->push_back(ASTPtr($3)); + $$ = $1; + } + ; + +VarDef + : IDENT ArrayDims { + auto* def = new VarDefNode(); + def->name = $1; free($1); + def->dims = std::move(*$2); delete $2; + def->lineno = @1.first_line; + $$ = def; + } + | IDENT ArrayDims '=' InitVal { + auto* def = new VarDefNode(); + def->name = $1; free($1); + def->dims = std::move(*$2); delete $2; + def->init_val = ASTPtr($4); + def->lineno = @1.first_line; + $$ = def; + } + ; + +InitVal + : Exp { $$ = $1; } + | '{' '}' { + $$ = new InitListNode(); + } + | '{' InitValList '}' { + auto* list = new InitListNode(); + list->items = std::move(*$2); delete $2; + $$ = list; + } + ; + +InitValList + : InitVal { + $$ = new ASTList(); + $$->push_back(ASTPtr($1)); + } + | InitValList ',' InitVal { + $1->push_back(ASTPtr($3)); + $$ = $1; + } + ; + +/* Array Dimensions */ +ArrayDims + : /* empty */ { $$ = new ASTList(); } + | ArrayDims '[' ConstExp ']' { + $1->push_back(ASTPtr($3)); + $$ = $1; + } + ; + +/* Function Parameters */ + +FuncFParams + : FuncFParam { + $$ = new ASTList(); + $$->push_back(ASTPtr($1)); + } + | FuncFParams ',' FuncFParam { + $1->push_back(ASTPtr($3)); + $$ = $1; + } + ; + +FuncFParam + : BType IDENT { + auto* p = new FuncFParamNode(); + p->btype = $1; + p->name = $2; free($2); + p->lineno = @1.first_line; + $$ = p; + } + | BType IDENT '[' ']' ArrayDims { + /* Array parameter */ + auto* p = new FuncFParamNode(); + p->btype = $1; + p->name = $2; free($2); + p->is_array = true; + p->dims = std::move(*$5); delete $5; + p->lineno = @1.first_line; + $$ = p; + } + ; + +/* Blocks and Statements */ + +Block + : '{' BlockItemList '}' { + auto* block = new BlockNode(); + block->items = std::move(*$2); delete $2; + $$ = block; + } + ; + +BlockItemList + : /* empty */ { $$ = new ASTList(); } + | BlockItemList BlockItem { + $1->push_back(ASTPtr($2)); + $$ = $1; + } + ; + +BlockItem + : Decl { $$ = $1; } + | Stmt { $$ = $1; } + ; + +Stmt + : LVal '=' Exp ';' { + auto* s = new AssignStmtNode(); + s->lval = ASTPtr($1); + s->expr = ASTPtr($3); + s->lineno = @1.first_line; + $$ = s; + } + | Exp ';' { + auto* s = new ExprStmtNode(); + s->expr = ASTPtr($1); + $$ = s; + } + | ';' { + $$ = new ExprStmtNode(); + } + | Block { $$ = $1; } + | T_IF '(' Cond ')' Stmt %prec LOWER_THAN_ELSE { + auto* s = new IfStmtNode(); + s->cond = ASTPtr($3); + s->then_stmt = ASTPtr($5); + s->lineno = @1.first_line; + $$ = s; + } + | T_IF '(' Cond ')' Stmt T_ELSE Stmt { + auto* s = new IfStmtNode(); + s->cond = ASTPtr($3); + s->then_stmt = ASTPtr($5); + s->else_stmt = ASTPtr($7); + s->lineno = @1.first_line; + $$ = s; + } + | T_WHILE '(' Cond ')' Stmt { + auto* s = new WhileStmtNode(); + s->cond = ASTPtr($3); + s->body = ASTPtr($5); + s->lineno = @1.first_line; + $$ = s; + } + | T_BREAK ';' { + $$ = new BreakStmtNode(); + $$->lineno = @1.first_line; + } + | T_CONTINUE ';' { + $$ = new ContinueStmtNode(); + $$->lineno = @1.first_line; + } + | T_RETURN ';' { + $$ = new ReturnStmtNode(); + $$->lineno = @1.first_line; + } + | T_RETURN Exp ';' { + auto* s = new ReturnStmtNode(); + s->expr = ASTPtr($2); + s->lineno = @1.first_line; + $$ = s; + } + ; + +/* Expressions */ + +/* Exp relaxed to LOrExp */ +Exp : LOrExp { $$ = $1; } ; + +Cond : LOrExp { $$ = $1; } ; + +ConstExp : AddExp { $$ = $1; } ; + +LVal + : IDENT ArrayIndices { + auto* lv = new LValNode(); + lv->name = $1; free($1); + lv->indices = std::move(*$2); delete $2; + lv->lineno = @1.first_line; + $$ = lv; + } + ; + +ArrayIndices + : /* empty */ { $$ = new ASTList(); } + | ArrayIndices '[' Exp ']' { + $1->push_back(ASTPtr($3)); + $$ = $1; + } + ; + +PrimaryExp + : '(' Exp ')' { $$ = $2; } + | LVal { $$ = $1; } + | Number { $$ = $1; } + ; + +Number + : INT_CONST { + auto* n = new IntLiteralNode(); + n->value = $1; + n->lineno = @1.first_line; + $$ = n; + } + | FLOAT_CONST { + auto* n = new FloatLiteralNode(); + n->value = $1; + n->lineno = @1.first_line; + $$ = n; + } + ; + +UnaryExp + : PrimaryExp { $$ = $1; } + | IDENT '(' ')' { + auto* c = new FuncCallNode(); + c->name = $1; free($1); + c->lineno = @1.first_line; + $$ = c; + } + | IDENT '(' FuncRParams ')' { + auto* c = new FuncCallNode(); + c->name = $1; free($1); + c->args = std::move(*$3); delete $3; + c->lineno = @1.first_line; + $$ = c; + } + | '+' UnaryExp { + auto* u = new UnaryExprNode(); + u->op = UnaryOp::Pos; + u->operand = ASTPtr($2); + u->lineno = @1.first_line; + $$ = u; + } + | '-' UnaryExp { + auto* u = new UnaryExprNode(); + u->op = UnaryOp::Neg; + u->operand = ASTPtr($2); + u->lineno = @1.first_line; + $$ = u; + } + | '!' UnaryExp { + auto* u = new UnaryExprNode(); + u->op = UnaryOp::Not; + u->operand = ASTPtr($2); + u->lineno = @1.first_line; + $$ = u; + } + ; + +FuncRParams + : Exp { + $$ = new ASTList(); + $$->push_back(ASTPtr($1)); + } + | FuncRParams ',' Exp { + $1->push_back(ASTPtr($3)); + $$ = $1; + } + /* putf format string parameter */ + | STRING_CONST { + $$ = new ASTList(); + auto* n = new IntLiteralNode(); /* placeholder */ + n->value = 0; + $$->push_back(ASTPtr(static_cast<ASTNode*>(n))); + free($1); + } + | FuncRParams ',' STRING_CONST { + free($3); + $$ = $1; + } + ; + +MulExp + : UnaryExp { $$ = $1; } + | MulExp '*' UnaryExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Mul; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + | MulExp '/' UnaryExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Div; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + | MulExp '%' UnaryExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Mod; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + ; + +AddExp + : MulExp { $$ = $1; } + | AddExp '+' MulExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Add; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + | AddExp '-' MulExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Sub; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + ; + +RelExp + : AddExp { $$ = $1; } + | RelExp '<' AddExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Lt; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + | RelExp '>' AddExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Gt; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + | RelExp T_LE AddExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Le; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + | RelExp T_GE AddExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Ge; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + ; + +EqExp + : RelExp { $$ = $1; } + | EqExp T_EQ RelExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Eq; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + | EqExp T_NE RelExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Ne; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + ; + +LAndExp + : EqExp { $$ = $1; } + | LAndExp T_AND EqExp { + auto* e = new BinaryExprNode(); e->op = BinOp::And; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + ; + +LOrExp + : LAndExp { $$ = $1; } + | LOrExp T_OR LAndExp { + auto* e = new BinaryExprNode(); e->op = BinOp::Or; + e->lhs = ASTPtr($1); e->rhs = ASTPtr($3); + e->lineno = @2.first_line; $$ = e; + } + ; + +%% + +/* Global Variables and Error Handling */ +CompUnitNode* g_ast_root = nullptr; + +void yyerror(const char* msg) { + fprintf(stderr, "rcc: parse error (line %d): %s\n", yylineno_val, msg); +} diff --git a/src/ir/irgen.cc b/src/ir/irgen.cc new file mode 100644 index 0000000..1332109 --- /dev/null +++ b/src/ir/irgen.cc @@ -0,0 +1,728 @@ +/* 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 irgen.cc +/// @brief AST -> SSA IR: Main framework, Braun SSA, statement translation + +#include "rcc/irgen.h" + +#include <cstddef> + +namespace rcc { + +// BType -> K class +int IRGen::btype_to_cls(BType t) { + return (t == BType::Float) ? Ks : Kw; +} + +// Constant / Ref Construction +Fn* IRGen::new_fn(const char* name) { + auto* fn = static_cast<Fn*>(alloc(ctx_, sizeof(Fn))); + memset(fn, 0, sizeof(Fn)); + fn->name = strf(ctx_, PFn, "%s", name); + + // Initialize tmp array + fn->ntmp = 0; + fn->tmp = static_cast<Tmp*>(vnew(ctx_, 0, sizeof(Tmp), PFn)); + for (int i = 0; i < Tmp0; i++) { + int cls = (T.fpr0 <= i && i < T.fpr0 + T.nfpr) ? Kd : Kl; + newtmp(ctx_, nullptr, cls, fn); + } + + // Initialize con array: con[0] = CUndef (corresponds to R/UNDEF) + fn->ncon = 2; + fn->con = static_cast<Con*>(vnew(ctx_, static_cast<ulong>(fn->ncon), sizeof(Con), PFn)); + memset(fn->con, 0, 2 * sizeof(Con)); + fn->con[0].type = Con::CUndef; + // con[1] = CBits{0} (corresponds to CON_Z) + fn->con[1].type = Con::CBits; + fn->con[1].bits.i = 0; + + fn->retty = -1; + fn->slot = 0; + fn->salign = 4; + // All user functions visible to linker + fn->lnk.export_ = 1; + return fn; +} + +Blk* IRGen::new_blk(const char* name) { + auto* b = static_cast<Blk*>(alloc(ctx_, sizeof(Blk))); + memset(b, 0, sizeof(Blk)); + b->id = cur_fn_->nblk++; + b->name = strf(ctx_, PFn, "%s.%u", name, b->id); + b->ins = static_cast<Ins*>(vnew(ctx_, 4, sizeof(Ins), PFn)); + b->nins = 0; + b->visit = 0; + + // Append to function block list tail + if (!cur_fn_->start) { + cur_fn_->start = b; + } else { + Blk* tail = cur_fn_->start; + while (tail->link) + tail = tail->link; + tail->link = b; + } + + blk_vec_.push_back(b); + return b; +} + +Blk* IRGen::get_blk(uint id) { + return blk_vec_[id]; +} + +bool IRGen::is_sealed(Blk* b) const { + auto it = sealed_.find(b->id); + return it != sealed_.end() && it->second; +} + +// Constant / Ref Construction +Ref IRGen::make_int_ref(int32_t val) { + return getcon(ctx_, static_cast<int64_t>(val), cur_fn_); +} + +Ref IRGen::make_float_ref(float val) { + Con c; + memset(&c, 0, sizeof(Con)); + c.type = Con::CBits; + c.flt = 1; + c.bits.s = val; + return newcon(ctx_, &c, cur_fn_); +} + +Ref IRGen::make_global_ref(const char* name) { + Con c; + memset(&c, 0, sizeof(Con)); + c.type = Con::CAddr; + c.sym.type = Sym::SGlo; + c.sym.id = intern(name); + return newcon(ctx_, &c, cur_fn_); +} + +Ref IRGen::make_global_addr_ref(const char* name) { + Ref sym = make_global_ref(name); + Ref dst = new_tmp_ref(Kl); + Ins i{}; + i.op = Oaddr; + i.cls = Kl; + i.to = dst; + i.arg[0] = sym; + i.arg[1] = Ref::none(); + push_ins(i); + return dst; +} + +Ref IRGen::new_tmp_ref(int cls) { + return newtmp(ctx_, "t", cls, cur_fn_); +} + +// Entry +IROutput IRGen::generate(CompUnitNode* root) { + output_ = {}; + sym_.register_builtins(); + sym_.enter_scope(); + visit_comp_unit(root); + sym_.leave_scope(); + return std::move(output_); +} + +void IRGen::visit_comp_unit(CompUnitNode* node) { + for (auto& child : node->decls_and_funcs) { + if (child->kind == NodeKind::FuncDef) + visit_func_def(static_cast<FuncDefNode*>(child.get())); + else if (child->kind == NodeKind::VarDecl) + visit_global_decl(static_cast<VarDeclNode*>(child.get())); + } +} + +// Function Definition +void IRGen::visit_func_def(FuncDefNode* node) { + cur_fn_ = new_fn(node->name.c_str()); + cur_fn_->leaf = 1; + blk_vec_.clear(); + current_def_.clear(); + incomplete_phis_.clear(); + sealed_.clear(); + + bool is_void = (node->ret_type == BType::Void); + + cur_fn_ret_cls_ = is_void ? Kx : btype_to_cls(node->ret_type); + + // Register function signature + FuncSig sig; + sig.name = node->name; + sig.ret_type = node->ret_type; + + // Entry block + Blk* entry = new_blk("entry"); + cur_blk_ = entry; + sealed_[entry->id] = true; + + sym_.enter_scope(); + + // Process formal parameters + for (auto& p : node->params) { + auto* fp = static_cast<FuncFParamNode*>(p.get()); + int cls = fp->is_array ? Kl : btype_to_cls(fp->btype); + sig.params.push_back({fp->btype, fp->is_array}); + + Symbol sym; + sym.name = fp->name; + sym.type = fp->btype; + sym.is_param = true; + sym.is_array = fp->is_array; + sym.uid = sym_.allocate_uid(); + if (fp->is_array) { + for (auto& d : fp->dims) + sym.dims.push_back(eval_const_int(d.get())); + } + + // Parameter as SSA value: emit Opar instruction (one %t =cls par per parameter) + Ref param_ref = new_tmp_ref(cls); + Ins par_i; + par_i.op = Opar; + par_i.cls = static_cast<uint>(cls); + par_i.to = param_ref; + par_i.arg[0] = Ref::none(); + par_i.arg[1] = Ref::none(); + push_ins(par_i); + + write_variable(sym.uid, cur_blk_, param_ref); + sym_.insert(fp->name, std::move(sym)); + } + + sym_.register_func(std::move(sig)); + + // Traverse function body + visit_block(static_cast<BlockNode*>(node->body.get())); + + // Ensure last block is terminated + if (!cur_blk_terminated()) { + if (is_void) { + emit_ret_void(); + } else { + Ref zero = (cur_fn_ret_cls_ == Ks) ? make_float_ref(0.0f) : make_int_ref(0); + emit_ret(zero, cur_fn_ret_cls_); + } + } + + sym_.leave_scope(); + output_.functions.push_back(cur_fn_); + cur_fn_ = nullptr; +} + +// Global Declaration +void IRGen::visit_global_decl(VarDeclNode* node) { + for (auto& d : node->defs) { + auto* def = static_cast<VarDefNode*>(d.get()); + Symbol sym; + sym.name = def->name; + sym.type = node->btype; + sym.is_const = node->is_const; + sym.is_global = true; + + for (auto& dim_expr : def->dims) + sym.dims.push_back(eval_const_int(dim_expr.get())); + sym.is_array = !sym.dims.empty(); + + GlobalData gd; + gd.name = def->name; + gd.elem_cls = btype_to_cls(node->btype); + gd.is_const = node->is_const; + gd.total_elems = 1; + for (int d2 : sym.dims) + gd.total_elems *= static_cast<size_t>(d2); + if (!sym.is_array) + gd.total_elems = 1; + + if (def->init_val) { + if (sym.is_array) { + std::vector<ASTNode*> flat; + flatten_init(sym.dims, def->init_val.get(), flat); + for (size_t i = 0; i < gd.total_elems; i++) { + if (i < flat.size() && flat[i]) { + if (node->btype == BType::Int) + gd.init_vals.push_back(eval_const_int(flat[i])); + else + gd.init_vals.push_back(eval_const_float(flat[i])); + } else { + if (node->btype == BType::Int) + gd.init_vals.push_back(static_cast<int32_t>(0)); + else + gd.init_vals.push_back(0.0f); + } + } + } else { + if (node->btype == BType::Int) + gd.init_vals.push_back(eval_const_int(def->init_val.get())); + else + gd.init_vals.push_back(eval_const_float(def->init_val.get())); + } + } else { + gd.is_zeroinit = true; + } + + if (node->is_const && !sym.is_array && def->init_val) { + if (node->btype == BType::Int) + sym.const_val = eval_const_int(def->init_val.get()); + else + sym.const_val = eval_const_float(def->init_val.get()); + } + + sym_.insert(def->name, std::move(sym)); + output_.globals.push_back(std::move(gd)); + } +} + +// Local Declaration +void IRGen::visit_local_decl(VarDeclNode* node) { + for (auto& d : node->defs) { + auto* def = static_cast<VarDefNode*>(d.get()); + Symbol sym; + sym.name = def->name; + sym.type = node->btype; + sym.is_const = node->is_const; + sym.uid = sym_.allocate_uid(); + + for (auto& dim_expr : def->dims) + sym.dims.push_back(eval_const_int(dim_expr.get())); + sym.is_array = !sym.dims.empty(); + + int cls = btype_to_cls(node->btype); + + if (sym.is_array) { + int total = 1; + for (int d2 : sym.dims) + total *= d2; + Ref size_ref = make_int_ref(total * 4); + // Arrays are always 8B aligned + Ref addr = emit_ins(Oalloc8, Kl, size_ref, Ref::none()); + sym.alloca_temp_id = static_cast<int>(addr.val); + + if (def->init_val) { + std::vector<ASTNode*> flat; + flatten_init(sym.dims, def->init_val.get(), flat); + // Count implicit zero tail + constexpr int kBulkZeroThreshold = 65536; + size_t nonzero_tail = flat.size(); + while (nonzero_tail > 0 && flat[nonzero_tail - 1] == nullptr) + nonzero_tail--; + int zero_count = total - static_cast<int>(nonzero_tail); + bool bulk = zero_count >= kBulkZeroThreshold; + if (bulk) + emit_bulk_zero(addr, total * 4); + size_t up_to = bulk ? nonzero_tail : static_cast<size_t>(total); + for (size_t i = 0; i < up_to; i++) { + bool has_init = i < flat.size() && flat[i] != nullptr; + if (bulk && !has_init) + continue; // bulk-zero covered + Ref off = make_int_ref(static_cast<int>(i * 4)); + Ref elem_addr = emit_ins(Oadd, Kl, addr, off); + Ref val; + if (has_init) { + val = visit_expr(flat[i]); + val = coerce(val, cls); + } else { + val = (cls == Ks) ? make_float_ref(0.0f) : make_int_ref(0); + } + emit_store(val, elem_addr, cls); + } + } + sym_.insert(def->name, std::move(sym)); + } else { + if (node->is_const && def->init_val) { + if (node->btype == BType::Int) + sym.const_val = eval_const_int(def->init_val.get()); + else + sym.const_val = eval_const_float(def->init_val.get()); + } + int uid = sym.uid; + sym_.insert(def->name, std::move(sym)); + + if (def->init_val) { + Ref val = visit_expr(def->init_val.get()); + val = coerce(val, cls); + write_variable(uid, cur_blk_, val); + } + } + } +} + +// Statements +void IRGen::visit_block(BlockNode* node) { + sym_.enter_scope(); + for (auto& item : node->items) { + if (cur_blk_terminated()) + break; + visit_stmt(item.get()); + } + sym_.leave_scope(); +} + +void IRGen::visit_stmt(ASTNode* node) { + if (cur_blk_terminated()) + return; + switch (node->kind) { + case NodeKind::VarDecl: + visit_local_decl(static_cast<VarDeclNode*>(node)); + break; + case NodeKind::AssignStmt: + visit_assign(static_cast<AssignStmtNode*>(node)); + break; + case NodeKind::ExprStmt: { + auto* es = static_cast<ExprStmtNode*>(node); + if (es->expr) + visit_expr(es->expr.get()); + break; + } + case NodeKind::Block: + visit_block(static_cast<BlockNode*>(node)); + break; + case NodeKind::IfStmt: + visit_if(static_cast<IfStmtNode*>(node)); + break; + case NodeKind::WhileStmt: + visit_while(static_cast<WhileStmtNode*>(node)); + break; + case NodeKind::ReturnStmt: + visit_return(static_cast<ReturnStmtNode*>(node)); + break; + case NodeKind::BreakStmt: + if (!loop_stack_.empty()) + emit_jmp(loop_stack_.back().exit_block); + break; + case NodeKind::ContinueStmt: + if (!loop_stack_.empty()) + emit_jmp(loop_stack_.back().cond_block); + break; + default: + break; + } +} + +void IRGen::visit_assign(AssignStmtNode* node) { + Ref rhs = visit_expr(node->expr.get()); + auto* lv = static_cast<LValNode*>(node->lval.get()); + Symbol* sym = sym_.lookup(lv->name); + int cls = btype_to_cls(sym->type); + // Cross-boundary type coordination + rhs = coerce(rhs, cls); + + if (!lv->indices.empty() || sym->is_array || sym->is_global) { + Ref addr; + if (!lv->indices.empty()) + addr = compute_array_addr(lv); + else + addr = get_var_addr_ref(sym); + emit_store(rhs, addr, cls); + } else { + write_variable(sym->uid, cur_blk_, rhs); + } +} + +void IRGen::visit_if(IfStmtNode* node) { + Blk* then_bb = new_blk("if.then"); + Blk* else_bb = node->else_stmt ? new_blk("if.else") : nullptr; + Blk* merge_bb = new_blk("if.merge"); + Blk* false_target = else_bb ? else_bb : merge_bb; + + visit_cond(node->cond.get(), then_bb, false_target); + + seal_block(then_bb); + cur_blk_ = then_bb; + visit_stmt(node->then_stmt.get()); + if (!cur_blk_terminated()) + emit_jmp(merge_bb); + + if (else_bb) { + seal_block(else_bb); + cur_blk_ = else_bb; + visit_stmt(node->else_stmt.get()); + if (!cur_blk_terminated()) + emit_jmp(merge_bb); + } + + seal_block(merge_bb); + cur_blk_ = merge_bb; +} + +void IRGen::visit_while(WhileStmtNode* node) { + Blk* cond_bb = new_blk("while.cond"); + Blk* body_bb = new_blk("while.body"); + Blk* exit_bb = new_blk("while.exit"); + + emit_jmp(cond_bb); + + cur_blk_ = cond_bb; + visit_cond(node->cond.get(), body_bb, exit_bb); + + seal_block(body_bb); + cur_blk_ = body_bb; + loop_stack_.push_back({cond_bb, exit_bb}); + visit_stmt(node->body.get()); + loop_stack_.pop_back(); + if (!cur_blk_terminated()) + emit_jmp(cond_bb); + + seal_block(cond_bb); + seal_block(exit_bb); + cur_blk_ = exit_bb; +} + +void IRGen::visit_return(ReturnStmtNode* node) { + if (node->expr) { + Ref val = visit_expr(node->expr.get()); + // cur_fn_ret_cls_ is set at visit_func_def entry based on AST ret_type; void functions + // won't reach here. + assert(cur_fn_ret_cls_ != Kx && "non-void return in void function"); + val = coerce(val, cur_fn_ret_cls_); + emit_ret(val, cur_fn_ret_cls_); + } else { + emit_ret_void(); + } +} + +// Braun SSA. Variable identity key is Symbol::uid(int), avoids name collision across scopes. +void IRGen::write_variable(int uid, Blk* blk, Ref val) { + current_def_[uid][blk->id] = val; +} + +Ref IRGen::read_variable(int uid, Blk* blk, int cls) { + auto it = current_def_[uid].find(blk->id); + if (it != current_def_[uid].end()) + return it->second; + return read_variable_recursive(uid, blk, cls); +} + +Ref IRGen::read_variable_recursive(int uid, Blk* blk, int cls) { + Ref val; + uint npred = blk->npred; + + if (!is_sealed(blk)) { + // Block not sealed: insert incomplete Phi + auto* phi = static_cast<Phi*>(alloc(ctx_, sizeof(Phi))); + memset(phi, 0, sizeof(Phi)); + phi->cls = static_cast<short>(cls); + phi->to = new_tmp_ref(cls); + phi->link = blk->phi; + blk->phi = phi; + val = phi->to; + incomplete_phis_.push_back({uid, phi, blk->id, cls}); + } else if (npred == 1) { + val = read_variable(uid, blk->pred[0], cls); + } else if (npred == 0) { + // Entry block, variable undefined + val = (cls == Ks) ? make_float_ref(0.0f) : make_int_ref(0); + } else { + // Multiple predecessors: insert Phi + auto* phi = static_cast<Phi*>(alloc(ctx_, sizeof(Phi))); + memset(phi, 0, sizeof(Phi)); + phi->cls = static_cast<short>(cls); + phi->to = new_tmp_ref(cls); + phi->link = blk->phi; + blk->phi = phi; + val = phi->to; + + write_variable(uid, blk, val); // Prevent infinite loop + + // Populate phi arguments + phi->narg = npred; + phi->arg = static_cast<Ref*>(alloc(ctx_, npred * sizeof(Ref))); + phi->blk = static_cast<Blk**>(alloc(ctx_, npred * sizeof(Blk*))); + for (uint i = 0; i < npred; i++) { + phi->blk[i] = blk->pred[i]; + phi->arg[i] = read_variable(uid, blk->pred[i], cls); + } + + val = try_remove_trivial_phi(phi); + } + + write_variable(uid, blk, val); + return val; +} + +void IRGen::seal_block(Blk* blk) { + // Snapshot to_process + std::vector<IncompletePhi> to_process; + for (auto it = incomplete_phis_.begin(); it != incomplete_phis_.end();) { + if (it->block_id == blk->id) { + to_process.push_back(std::move(*it)); + it = incomplete_phis_.erase(it); + } else { + ++it; + } + } + sealed_[blk->id] = true; + for (auto& p : to_process) + add_phi_operands(p.var_uid, blk, p.phi, p.cls); +} + +void IRGen::add_phi_operands(int uid, Blk* blk, Phi* phi, int cls) { + uint npred = blk->npred; + phi->narg = npred; + phi->arg = static_cast<Ref*>(alloc(ctx_, npred * sizeof(Ref))); + phi->blk = static_cast<Blk**>(alloc(ctx_, npred * sizeof(Blk*))); + for (uint i = 0; i < npred; i++) { + phi->blk[i] = blk->pred[i]; + phi->arg[i] = read_variable(uid, blk->pred[i], cls); + } + try_remove_trivial_phi(phi); +} + +Ref IRGen::try_remove_trivial_phi(Phi* phi) { + Ref same = Ref::none(); + bool same_set = false; + for (uint i = 0; i < phi->narg; i++) { + if (req(phi->arg[i], phi->to)) + continue; + if (same_set && !req(phi->arg[i], same)) + return phi->to; + same = phi->arg[i]; + same_set = true; + } + return same_set ? same : phi->to; +} + +// Emission helpers +void IRGen::push_ins(Ins i) { + vgrow(ctx_, &cur_blk_->ins, cur_blk_->nins + 1); + cur_blk_->ins[cur_blk_->nins++] = i; +} + +Ref IRGen::emit_ins(int op, int cls, Ref a0, Ref a1) { + Ref dst = new_tmp_ref(cls); + Ins i; + i.op = static_cast<uint>(op); + i.cls = static_cast<uint>(cls); + i.to = dst; + i.arg[0] = a0; + i.arg[1] = a1; + push_ins(i); + return dst; +} + +void IRGen::emit_store(Ref val, Ref addr, int cls) { + int op = (cls == Ks) ? Ostores : Ostorew; + Ins i; + i.op = static_cast<uint>(op); + i.cls = Kw; + i.to = Ref::none(); + i.arg[0] = val; + i.arg[1] = addr; + push_ins(i); +} + +Ref IRGen::emit_load(Ref addr, int cls) { + return emit_ins(Oload, cls, addr, Ref::none()); +} + +void IRGen::emit_bulk_zero(Ref addr, int size_bytes) { + // Rewrite as Ocall __rcc_memset + Ins arg_dst{}; + arg_dst.op = Oarg; + arg_dst.cls = Kl; + arg_dst.to = Ref::none(); + arg_dst.arg[0] = addr; + arg_dst.arg[1] = Ref::none(); + push_ins(arg_dst); + + Ins arg_val{}; + arg_val.op = Oarg; + arg_val.cls = Kw; + arg_val.to = Ref::none(); + arg_val.arg[0] = make_int_ref(0); + arg_val.arg[1] = Ref::none(); + push_ins(arg_val); + + Ins arg_sz{}; + arg_sz.op = Oarg; + arg_sz.cls = Kl; + arg_sz.to = Ref::none(); + arg_sz.arg[0] = make_int_ref(size_bytes); + arg_sz.arg[1] = Ref::none(); + push_ins(arg_sz); + + Ins call{}; + call.op = Ocall; + call.cls = Kw; + call.to = Ref::none(); + call.arg[0] = make_global_ref("__rcc_memset"); + call.arg[1] = Ref::none(); + push_ins(call); + + cur_fn_->leaf = 0; +} + +void IRGen::emit_jmp(Blk* target) { + if (cur_blk_terminated()) + return; + cur_blk_->jmp.type = Jjmp; + cur_blk_->s1 = target; + add_edge(cur_blk_, target); +} + +void IRGen::emit_jnz(Ref cond, Blk* true_bb, Blk* false_bb) { + if (cur_blk_terminated()) + return; + cur_blk_->jmp.type = Jjnz; + cur_blk_->jmp.arg = cond; + cur_blk_->s1 = true_bb; + cur_blk_->s2 = false_bb; + add_edge(cur_blk_, true_bb); + add_edge(cur_blk_, false_bb); +} + +void IRGen::emit_ret(Ref val, int cls) { + // Choose retw/rets based on cls + int jtype = (cls == Ks) ? Jrets : Jretw; + cur_blk_->jmp.type = static_cast<short>(jtype); + cur_blk_->jmp.arg = val; +} + +void IRGen::emit_ret_void() { + cur_blk_->jmp.type = Jret0; +} + +bool IRGen::cur_blk_terminated() const { + return cur_blk_ && cur_blk_->jmp.type != Jxxx; +} + +void IRGen::add_edge(Blk* from, Blk* to) { + // Add to 'to's pred list + uint n = to->npred; + if (n == 0) { + to->pred = static_cast<Blk**>(vnew(ctx_, 2, sizeof(Blk*), PFn)); + } else { + vgrow(ctx_, &to->pred, n + 1); + } + to->pred[n] = from; + to->npred = n + 1; +} + +Ref IRGen::get_var_addr_ref(Symbol* sym) { + if (sym->is_global) + return make_global_ref(sym->name.c_str()); + if (sym->is_param && sym->is_array) + return read_variable(sym->uid, cur_blk_, Kl); + return Ref::tmp(static_cast<uint>(sym->alloca_temp_id)); +} + +} // namespace rcc diff --git a/src/ir/irgen_expr.cc b/src/ir/irgen_expr.cc new file mode 100644 index 0000000..e18895c --- /dev/null +++ b/src/ir/irgen_expr.cc @@ -0,0 +1,328 @@ +/* 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 irgen_expr.cc +/// @brief Expression evaluation + short-circuit conditional jump + +#include "rcc/irgen.h" + +namespace rcc { + +int IRGen::ref_cls_of(Ref r, Fn* fn) { + if (rtype(r) == RTmp && r.val >= Tmp0) + return fn->tmp[r.val].cls; + if (rtype(r) == RCon) { + Con* c = &fn->con[r.val]; + if (c->type == Con::CBits && c->flt) + return Ks; + } + return Kw; +} + +Ref IRGen::coerce(Ref v, int target_cls) { + int cur = ref_cls_of(v, cur_fn_); + if (cur == target_cls) + return v; + if (target_cls == Ks && cur == Kw) + return emit_ins(Oswtof, Ks, v, Ref::none()); + if (target_cls == Kw && cur == Ks) + return emit_ins(Ostosi, Kw, v, Ref::none()); + return v; +} + +// Expression evaluation + +Ref IRGen::visit_expr(ASTNode* node) { + switch (node->kind) { + case NodeKind::IntLiteral: + return make_int_ref(static_cast<IntLiteralNode*>(node)->value); + + case NodeKind::FloatLiteral: + return make_float_ref(static_cast<FloatLiteralNode*>(node)->value); + + case NodeKind::LVal: { + auto* lv = static_cast<LValNode*>(node); + Symbol* sym = sym_.lookup(lv->name); + if (!sym) + die("undefined variable"); + + // Constant scalar: return constant value directly + if (sym->is_const && !sym->is_array && lv->indices.empty()) { + if (auto* iv = std::get_if<int32_t>(&sym->const_val)) + return make_int_ref(*iv); + if (auto* fv = std::get_if<float>(&sym->const_val)) + return make_float_ref(*fv); + } + + // Array without index -> return address + if (sym->is_array && lv->indices.empty()) + return get_var_addr_ref(sym); + + if (sym->is_array && !lv->indices.empty()) { + if (lv->indices.size() < sym->dims.size() + (sym->is_param ? 1 : 0)) + return compute_array_addr(lv); + Ref addr = compute_array_addr(lv); + return emit_load(addr, btype_to_cls(sym->type)); + } + + // Global variable + if (sym->is_global) { + Ref addr = make_global_ref(sym->name.c_str()); + return emit_load(addr, btype_to_cls(sym->type)); + } + + // Alloca variable + if (sym->alloca_temp_id >= 0) { + Ref addr = Ref::tmp(static_cast<uint>(sym->alloca_temp_id)); + return emit_load(addr, btype_to_cls(sym->type)); + } + + // Normal local scalar: Braun + int cls = btype_to_cls(sym->type); + return read_variable(sym->uid, cur_blk_, cls); + } + + case NodeKind::BinaryExpr: { + auto* bin = static_cast<BinaryExprNode*>(node); + + // Logical operation -> short-circuit evaluation -> phi(1,0) + if (bin->op == BinOp::And || bin->op == BinOp::Or) { + Blk* true_bb = new_blk("cond.true"); + Blk* false_bb = new_blk("cond.false"); + Blk* merge_bb = new_blk("cond.merge"); + + visit_cond(node, true_bb, false_bb); + + seal_block(true_bb); + cur_blk_ = true_bb; + emit_jmp(merge_bb); + + seal_block(false_bb); + cur_blk_ = false_bb; + emit_jmp(merge_bb); + + seal_block(merge_bb); + cur_blk_ = merge_bb; + + // Phi: true→1, false→0 + auto* phi = static_cast<Phi*>(alloc(ctx_, sizeof(Phi))); + memset(phi, 0, sizeof(Phi)); + phi->cls = Kw; + phi->to = new_tmp_ref(Kw); + phi->narg = 2; + phi->arg = static_cast<Ref*>(alloc(ctx_, 2 * sizeof(Ref))); + phi->blk = static_cast<Blk**>(alloc(ctx_, 2 * sizeof(Blk*))); + phi->arg[0] = make_int_ref(1); + phi->blk[0] = true_bb; + phi->arg[1] = make_int_ref(0); + phi->blk[1] = false_bb; + phi->link = merge_bb->phi; + merge_bb->phi = phi; + return phi->to; + } + + Ref lhs = visit_expr(bin->lhs.get()); + Ref rhs = visit_expr(bin->rhs.get()); + + int lk = ref_cls_of(lhs, cur_fn_); + int rk = ref_cls_of(rhs, cur_fn_); + + bool is_float = (lk == Ks || rk == Ks); + if (is_float && lk == Kw) + lhs = emit_ins(Oswtof, Ks, lhs, Ref::none()); + if (is_float && rk == Kw) + rhs = emit_ins(Oswtof, Ks, rhs, Ref::none()); + + int rt = is_float ? Ks : Kw; + switch (bin->op) { + case BinOp::Add: + return emit_ins(Oadd, rt, lhs, rhs); + case BinOp::Sub: + return emit_ins(Osub, rt, lhs, rhs); + case BinOp::Mul: + return emit_ins(Omul, rt, lhs, rhs); + case BinOp::Div: + return emit_ins(Odiv, rt, lhs, rhs); + case BinOp::Mod: + return emit_ins(Orem, rt, lhs, rhs); + case BinOp::Lt: + return emit_ins(is_float ? Oclts : Ocsltw, Kw, lhs, rhs); + case BinOp::Gt: + return emit_ins(is_float ? Ocgts : Ocsgtw, Kw, lhs, rhs); + case BinOp::Le: + return emit_ins(is_float ? Ocles : Ocslew, Kw, lhs, rhs); + case BinOp::Ge: + return emit_ins(is_float ? Ocges : Ocsgew, Kw, lhs, rhs); + case BinOp::Eq: + return emit_ins(is_float ? Oceqs : Oceqw, Kw, lhs, rhs); + case BinOp::Ne: + return emit_ins(is_float ? Ocnes : Ocnew, Kw, lhs, rhs); + default: + return make_int_ref(0); + } + } + + case NodeKind::UnaryExpr: { + auto* u = static_cast<UnaryExprNode*>(node); + Ref operand = visit_expr(u->operand.get()); + switch (u->op) { + case UnaryOp::Pos: + return operand; + case UnaryOp::Neg: { + int k = ref_cls_of(operand, cur_fn_); + if (k == Ks) + return emit_ins(Oneg, Ks, operand, Ref::none()); + return emit_ins(Osub, Kw, make_int_ref(0), operand); + } + case UnaryOp::Not: + return emit_ins(Oceqw, Kw, operand, make_int_ref(0)); + } + return make_int_ref(0); + } + + case NodeKind::FuncCall: { + auto* call_node = static_cast<FuncCallNode*>(node); + + // starttime/stoptime special handling + if (call_node->name == "starttime" || call_node->name == "stoptime") { + std::string real_name = "_sysy_" + call_node->name; + Ref fn_ref = make_global_ref(real_name.c_str()); + Ref line_ref = make_int_ref(call_node->lineno); + + // arg w, line + Ins arg_i; + arg_i.op = Oarg; + arg_i.cls = Kw; + arg_i.to = Ref::none(); + arg_i.arg[0] = line_ref; + arg_i.arg[1] = Ref::none(); + push_ins(arg_i); + + // call + Ins call_i; + call_i.op = Ocall; + call_i.cls = Kw; + call_i.to = Ref::none(); + call_i.arg[0] = fn_ref; + call_i.arg[1] = Ref::none(); + push_ins(call_i); + return make_int_ref(0); + } + + FuncSig* fsig = sym_.lookup_func(call_node->name); + bool is_void = fsig && fsig->ret_type == BType::Void; + int ret_cls = fsig ? btype_to_cls(fsig->ret_type) : Kw; + + std::vector<Ref> arg_refs; + std::vector<int> arg_clses; + arg_refs.reserve(call_node->args.size()); + arg_clses.reserve(call_node->args.size()); + for (size_t i = 0; i < call_node->args.size(); i++) { + Ref v = visit_expr(call_node->args[i].get()); + int acls = Kw; + if (fsig && i < fsig->params.size()) + acls = fsig->params[i].second ? Kl : btype_to_cls(fsig->params[i].first); + + if (acls == Kw || acls == Ks) + v = coerce(v, acls); + arg_refs.push_back(v); + arg_clses.push_back(acls); + } + for (size_t i = 0; i < arg_refs.size(); i++) { + Ins arg_i; + arg_i.op = Oarg; + arg_i.cls = static_cast<uint>(arg_clses[i]); + arg_i.to = Ref::none(); + arg_i.arg[0] = arg_refs[i]; + arg_i.arg[1] = Ref::none(); + push_ins(arg_i); + } + + // call instruction + Ref fn_ref = make_global_ref(call_node->name.c_str()); + if (is_void) { + Ins call_i; + call_i.op = Ocall; + call_i.cls = Kw; + call_i.to = Ref::none(); + call_i.arg[0] = fn_ref; + call_i.arg[1] = Ref::none(); + push_ins(call_i); + cur_fn_->leaf = 0; + return make_int_ref(0); + } + + Ref dst = new_tmp_ref(ret_cls); + Ins call_i; + call_i.op = Ocall; + call_i.cls = static_cast<uint>(ret_cls); + call_i.to = dst; + call_i.arg[0] = fn_ref; + call_i.arg[1] = Ref::none(); + push_ins(call_i); + cur_fn_->leaf = 0; + return dst; + } + + case NodeKind::ImplicitCast: { + auto* c = static_cast<ImplicitCastNode*>(node); + Ref v = visit_expr(c->operand.get()); + int k = ref_cls_of(v, cur_fn_); + if (c->target_type == BType::Float && k == Kw) + return emit_ins(Oswtof, Ks, v, Ref::none()); + if (c->target_type == BType::Int && k == Ks) + return emit_ins(Ostosi, Kw, v, Ref::none()); + return v; + } + + default: + return make_int_ref(0); + } +} + +// Short-circuit conditional jump + +void IRGen::visit_cond(ASTNode* cond, Blk* true_bb, Blk* false_bb) { + if (auto* bin = dynamic_cast<BinaryExprNode*>(cond)) { + if (bin->op == BinOp::And) + return visit_cond_and(bin, true_bb, false_bb); + if (bin->op == BinOp::Or) + return visit_cond_or(bin, true_bb, false_bb); + } + Ref val = visit_expr(cond); + emit_jnz(val, true_bb, false_bb); +} + +void IRGen::visit_cond_and(BinaryExprNode* node, Blk* true_bb, Blk* false_bb) { + Blk* rhs_bb = new_blk("and.rhs"); + visit_cond(node->lhs.get(), rhs_bb, false_bb); + seal_block(rhs_bb); + cur_blk_ = rhs_bb; + visit_cond(node->rhs.get(), true_bb, false_bb); +} + +void IRGen::visit_cond_or(BinaryExprNode* node, Blk* true_bb, Blk* false_bb) { + Blk* rhs_bb = new_blk("or.rhs"); + visit_cond(node->lhs.get(), true_bb, rhs_bb); + seal_block(rhs_bb); + cur_blk_ = rhs_bb; + visit_cond(node->rhs.get(), true_bb, false_bb); +} + +} // namespace rcc diff --git a/src/ir/irgen_util.cc b/src/ir/irgen_util.cc new file mode 100644 index 0000000..e7e89f5 --- /dev/null +++ b/src/ir/irgen_util.cc @@ -0,0 +1,244 @@ +/* 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 irgen_util.cc +/// @brief Constant evaluation, array init flattening, array address computation + +#include "rcc/irgen.h" + +#include <cstddef> + +namespace rcc { + +// Compile-time constant evaluation + +int32_t IRGen::eval_const_int(ASTNode* node) { + switch (node->kind) { + case NodeKind::IntLiteral: + return static_cast<IntLiteralNode*>(node)->value; + case NodeKind::FloatLiteral: + return static_cast<int32_t>(static_cast<FloatLiteralNode*>(node)->value); + case NodeKind::LVal: { + auto* lv = static_cast<LValNode*>(node); + Symbol* sym = sym_.lookup(lv->name); + if (sym && sym->is_const && !sym->is_array) { + if (auto* iv = std::get_if<int32_t>(&sym->const_val)) + return *iv; + if (auto* fv = std::get_if<float>(&sym->const_val)) + return static_cast<int32_t>(*fv); + } + return 0; + } + case NodeKind::UnaryExpr: { + auto* u = static_cast<UnaryExprNode*>(node); + int32_t v = eval_const_int(u->operand.get()); + if (u->op == UnaryOp::Neg) + return -v; + if (u->op == UnaryOp::Not) + return !v; + return v; + } + case NodeKind::BinaryExpr: { + auto* b = static_cast<BinaryExprNode*>(node); + int32_t l = eval_const_int(b->lhs.get()); + int32_t r = eval_const_int(b->rhs.get()); + switch (b->op) { + case BinOp::Add: + return l + r; + case BinOp::Sub: + return l - r; + case BinOp::Mul: + return l * r; + case BinOp::Div: + return r ? l / r : 0; + case BinOp::Mod: + return r ? l % r : 0; + case BinOp::Lt: + return l < r; + case BinOp::Gt: + return l > r; + case BinOp::Le: + return l <= r; + case BinOp::Ge: + return l >= r; + case BinOp::Eq: + return l == r; + case BinOp::Ne: + return l != r; + case BinOp::And: + return l && r; + case BinOp::Or: + return l || r; + } + return 0; + } + default: + return 0; + } +} + +float IRGen::eval_const_float(ASTNode* node) { + switch (node->kind) { + case NodeKind::FloatLiteral: + return static_cast<FloatLiteralNode*>(node)->value; + case NodeKind::IntLiteral: + return static_cast<float>(static_cast<IntLiteralNode*>(node)->value); + case NodeKind::LVal: { + auto* lv = static_cast<LValNode*>(node); + Symbol* sym = sym_.lookup(lv->name); + if (sym && sym->is_const && !sym->is_array) { + if (auto* fv = std::get_if<float>(&sym->const_val)) + return *fv; + if (auto* iv = std::get_if<int32_t>(&sym->const_val)) + return static_cast<float>(*iv); + } + return 0.0f; + } + case NodeKind::UnaryExpr: { + auto* u = static_cast<UnaryExprNode*>(node); + float v = eval_const_float(u->operand.get()); + if (u->op == UnaryOp::Neg) + return -v; + return v; + } + case NodeKind::BinaryExpr: { + auto* b = static_cast<BinaryExprNode*>(node); + float l = eval_const_float(b->lhs.get()); + float r = eval_const_float(b->rhs.get()); + switch (b->op) { + case BinOp::Add: + return l + r; + case BinOp::Sub: + return l - r; + case BinOp::Mul: + return l * r; + case BinOp::Div: + return r != 0.0f ? l / r : 0.0f; + default: + return 0.0f; + } + } + default: + return 0.0f; + } +} + +// Array initialization flattening + +void IRGen::flatten_init(const std::vector<int>& dims, ASTNode* init, std::vector<ASTNode*>& out) { + if (!init) + return; + + if (init->kind != NodeKind::InitList) { + out.push_back(init); + return; + } + + auto* list = static_cast<InitListNode*>(init); + if (dims.empty()) + return; + + int sub_size = 1; + for (size_t i = 1; i < dims.size(); i++) + sub_size *= dims[i]; + + int total = dims[0] * sub_size; + int pos = 0; + + for (auto& item : list->items) { + if (pos >= total) + break; + + if (item->kind == NodeKind::InitList) { + if (dims.size() > 1) { + std::vector<int> sub_dims(dims.begin() + 1, dims.end()); + int before = static_cast<int>(out.size()); + flatten_init(sub_dims, item.get(), out); + int filled = static_cast<int>(out.size()) - before; + while (filled < sub_size) { + out.push_back(nullptr); + filled++; + } + pos += sub_size; + } else { + auto* inner = static_cast<InitListNode*>(item.get()); + for (auto& elem : inner->items) { + if (pos < total) { + out.push_back(elem.get()); + pos++; + } + } + } + } else { + out.push_back(item.get()); + pos++; + } + } + + while (pos < total) { + out.push_back(nullptr); + pos++; + } +} + +// Array address calculation + +Ref IRGen::compute_array_addr(LValNode* node) { + Symbol* sym = sym_.lookup(node->name); + Ref base = get_var_addr_ref(sym); + + if (sym->is_param && sym->is_array) + base = read_variable(sym->uid, cur_blk_, Kl); + + const auto& dims = sym->dims; + size_t ndims = dims.size(); + + Ref offset = Ref::none(); + bool offset_valid = false; + + for (size_t i = 0; i < node->indices.size(); i++) { + Ref idx = visit_expr(node->indices[i].get()); + + int stride = 1; + size_t dim_idx = sym->is_param ? i : (i + 1); + for (size_t j = dim_idx; j < ndims; j++) + stride *= dims[j]; + + // Extend index to 'l' type for pointer arithmetic + int idx_cls = Kw; + if (rtype(idx) == RTmp && idx.val >= Tmp0) + idx_cls = cur_fn_->tmp[idx.val].cls; + if (idx_cls == Kw) + idx = emit_ins(Oextsw, Kl, idx, Ref::none()); + + Ref byte_off = emit_ins(Omul, Kl, idx, make_int_ref(stride * 4)); + + if (offset_valid) { + offset = emit_ins(Oadd, Kl, offset, byte_off); + } else { + offset = byte_off; + offset_valid = true; + } + } + + if (offset_valid) + return emit_ins(Oadd, Kl, base, offset); + return base; +} + +} // namespace rcc diff --git a/src/ir/print.cc b/src/ir/print.cc new file mode 100644 index 0000000..d452a6b --- /dev/null +++ b/src/ir/print.cc @@ -0,0 +1,388 @@ +/* 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 print.cc +/// @brief IR text output: Fn / Blk / Phi / Ins / Jmp / Con / Ref + +#include "rcc/all.h" +#include "rcc/irgen.h" + +#include <cinttypes> + +namespace rcc { + +namespace { + +/// K class characters: Kw='w' Kl='l' Ks='s' Kd='d'. Others are treated as errors. +char cls_char(int k) { + switch (k) { + case Kw: + return 'w'; + case Kl: + return 'l'; + case Ks: + return 's'; + case Kd: + return 'd'; + default: + return '?'; + } +} + +const char* jmp_name(int j) { + static constexpr auto tab = [] { + std::array<const char*, NJmp> a{}; +#define X(n) a[J##n] = #n; + JMPS(X) +#undef X + return a; + }(); + if (j <= Jxxx || j >= NJmp) + return "??"; + return tab[static_cast<size_t>(j)]; +} + +/// Mem (= Addr) internal addressing component output: [ offset + base + scale*index ] +void print_mem(Mem* m, Fn* fn, FILE* f); + +bool op_needs_cls_suffix(int op) { + switch (op) { + case Oarg: + case Oswap: + case Oxcmp: + case Oacmp: + case Oacmn: + case Oafcmp: + case Oxtest: + case Oxdiv: + case Oxidiv: + return true; + default: + return false; + } +} + +} // namespace + +// printcon + +static void printcon(Con* c, FILE* f) { + switch (c->type) { + case Con::CUndef: + return; + case Con::CAddr: + if (c->sym.type & Sym::SExt) + fputs("extern ", f); + if (c->sym.type & Sym::SThr) + fputs("thread ", f); + fprintf(f, "$%s", str(c->sym.id)); + if (c->bits.i) + fprintf(f, "%+" PRId64, c->bits.i); + return; + case Con::CBits: + if (c->flt == 1) + fprintf(f, "s_%f", static_cast<double>(c->bits.s)); + else if (c->flt == 2) + fprintf(f, "d_%lf", c->bits.d); + else + fprintf(f, "%" PRId64, c->bits.i); + return; + } +} + +// printref + +void printref(Ref r, Fn* fn, FILE* f) { + switch (rtype(r)) { + case RTmp: + if (r.val < Tmp0) + fprintf(f, "R%u", r.val); + else + fprintf(f, "%%%s", fn->tmp[r.val].name); + return; + + case RCon: + if (req(r, Ref::undef())) + fputs("UNDEF", f); + else + printcon(&fn->con[r.val], f); + return; + + case RSlot: + fprintf(f, "S%d", rsval(r)); + return; + + case RCall: + fprintf(f, "%04x", r.val); + return; + + case RType: + fprintf(f, ":%s", typ[r.val].name); + return; + + case RMem: + print_mem(&fn->mem[r.val], fn, f); + return; + + case RInt: + fprintf(f, "%d", rsval(r)); + return; + + case -1: + fputc('R', f); + return; + } +} + +namespace { + +void print_mem(Mem* m, Fn* fn, FILE* f) { + fputc('[', f); + bool need_plus = false; + if (m->offset.type != Con::CUndef) { + printcon(&m->offset, f); + need_plus = true; + } + if (!req(m->base, Ref::none())) { + if (need_plus) + fputs(" + ", f); + printref(m->base, fn, f); + need_plus = true; + } + if (!req(m->index, Ref::none())) { + if (need_plus) + fputs(" + ", f); + fprintf(f, "%d * ", m->scale); + printref(m->index, fn, f); + } + fputc(']', f); +} + +// Phi / Ins / Jmp single-line output + +void print_phi_line(Phi* p, Fn* fn, FILE* f) { + fputc('\t', f); + printref(p->to, fn, f); + fprintf(f, " =%c phi ", cls_char(p->cls)); + assert(p->narg && "phi has no arg"); + for (uint n = 0; n < p->narg; n++) { + fprintf(f, "@%s ", p->blk[n]->name); + printref(p->arg[n], fn, f); + if (n + 1 < p->narg) + fputs(", ", f); + } + fputc('\n', f); +} + +void print_ins_line(Ins* i, Fn* fn, FILE* f) { + fputc('\t', f); + if (!req(i->to, Ref::none())) { + printref(i->to, fn, f); + fprintf(f, " =%c ", cls_char(static_cast<int>(i->cls))); + } + assert(optab[i->op].name && "missing op name in optab"); + fputs(optab[i->op].name, f); + + if (req(i->to, Ref::none()) && op_needs_cls_suffix(i->op)) + fputc(cls_char(static_cast<int>(i->cls)), f); + + if (!req(i->arg[0], Ref::none())) { + fputc(' ', f); + printref(i->arg[0], fn, f); + } + if (!req(i->arg[1], Ref::none())) { + fputs(", ", f); + printref(i->arg[1], fn, f); + } + fputc('\n', f); +} + +/// Terminating instructions: ret / jmp / jnz / floating-point conditional jump / hlt +void print_jmp_line(Blk* b, Fn* fn, FILE* f) { + int jt = b->jmp.type; + + if (isret(jt)) { + fputs("\tret", f); + if (jt != Jret0 || !req(b->jmp.arg, Ref::none())) { + fputc(' ', f); + printref(b->jmp.arg, fn, f); + } + fputc('\n', f); + return; + } + + if (jt == Jhlt) { + fputs("\thlt\n", f); + return; + } + + if (jt == Jjmp) { + // If fall-through to next block, jmp can be omitted (QBE output style) + if (b->s1 != b->link) + fprintf(f, "\tjmp @%s\n", b->s1->name); + return; + } + + // jnz or jf* series conditional jumps: two successors + fprintf(f, "\t%s ", jmp_name(jt)); + if (jt == Jjnz) { + printref(b->jmp.arg, fn, f); + fputs(", ", f); + } + assert(b->s1 && b->s2 && "branch missing successor"); + fprintf(f, "@%s, @%s\n", b->s1->name, b->s2->name); +} + +void print_call(Ins* call, Ins** args, size_t nargs, Fn* fn, FILE* f) { + fputc('\t', f); + if (!req(call->to, Ref::none())) { + printref(call->to, fn, f); + fprintf(f, " =%c ", cls_char(static_cast<int>(call->cls))); + } + fputs("call ", f); + printref(call->arg[0], fn, f); + fputc('(', f); + for (size_t k = 0; k < nargs; k++) { + if (k) + fputs(", ", f); + Ins* a = args[k]; + fprintf(f, "%c ", cls_char(static_cast<int>(a->cls))); + printref(a->arg[0], fn, f); + } + fputs(")\n", f); +} + +void print_block(Blk* b, Fn* fn, FILE* f) { + fprintf(f, "@%s\n", b->name); + for (Phi* p = b->phi; p; p = p->link) + print_phi_line(p, fn, f); + + // Scan: skip Opar at entry block start (already in signature); Oarg sequence is merged and + // printed when encountering Ocall, and call itself is output at the merge point. + Ins* pending_args[NIns]; + size_t npending = 0; + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (i->op == Onop) + continue; // promote/coalesce marks instructions as nop to "delete" them, QBE text + // doesn't accept nop keyword + if (i->op == Opar) + continue; + if (i->op == Oarg) { + assert(npending < NIns && "too many call args"); + pending_args[npending++] = i; + continue; + } + if (i->op == Ocall) { + print_call(i, pending_args, npending, fn, f); + npending = 0; + continue; + } + // There should be no dangling Oarg before normal instructions + assert(npending == 0 && "orphan arg before non-call op"); + print_ins_line(i, fn, f); + } + assert(npending == 0 && "orphan arg at end of block"); + + print_jmp_line(b, fn, f); +} + +void print_signature(Fn* fn, FILE* f) { + fputs("export\nfunction ", f); + + int ret_cls = Kx; + for (Blk* b = fn->start; b; b = b->link) { + int jt = b->jmp.type; + if (!isret(jt)) + continue; + if (jt >= Jretw && jt <= Jretd) + ret_cls = jt - Jretw; // Jretw..Jretd align with Kw..Kd + break; + } + if (ret_cls >= 0) + fprintf(f, "%c ", cls_char(ret_cls)); + fprintf(f, "$%s(", fn->name); + + // Scan Opar in entry to collect parameters + Blk* entry = fn->start; + bool first = true; + if (entry) { + for (uint k = 0; k < entry->nins; k++) { + Ins* i = &entry->ins[k]; + if (i->op != Opar) // Opar are contiguous at entry head, break on first non-Opar + break; + if (!first) + fputs(", ", f); + fprintf(f, "%c ", cls_char(static_cast<int>(i->cls))); + printref(i->to, fn, f); + first = false; + } + } + fputs(") {\n", f); +} + +} // namespace + +// printfn - Top-level assembly + +void printfn(Fn* fn, FILE* f) { + print_signature(fn, f); + for (Blk* b = fn->start; b; b = b->link) + print_block(b, fn, f); + fputs("}\n", f); +} + +// print_globals - Data segment + +namespace { + +constexpr size_t ELEM_BYTES = 4; // Kw / Ks are both 4 bytes + +void print_global_cell(int elem_cls, const std::variant<int32_t, float>& v, FILE* f) { + if (elem_cls == Ks) { + // QBE floating-point constant literal s_<float> + float fv = std::holds_alternative<float>(v) ? std::get<float>(v) + : static_cast<float>(std::get<int32_t>(v)); + fprintf(f, "s s_%f", static_cast<double>(fv)); + } else { + int32_t iv = std::holds_alternative<int32_t>(v) ? std::get<int32_t>(v) + : static_cast<int32_t>(std::get<float>(v)); + fprintf(f, "w %d", iv); + } +} + +} // namespace + +void print_globals(const std::vector<GlobalData>& globals, FILE* f) { + for (const auto& g : globals) { + fprintf(f, "data $%s = align 4 { ", g.name.c_str()); + if (g.is_zeroinit) { + fprintf(f, "z %zu", g.total_elems * ELEM_BYTES); + } else { + for (size_t i = 0; i < g.init_vals.size(); i++) { + if (i) + fputs(", ", f); + print_global_cell(g.elem_cls, g.init_vals[i], f); + } + } + fputs(" }\n", f); + } +} + +} // namespace rcc diff --git a/src/main.cc b/src/main.cc new file mode 100644 index 0000000..a4b88ec --- /dev/null +++ b/src/main.cc @@ -0,0 +1,386 @@ +/* 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 main.cc +/// @brief RCC compiler entry point. + +#include "rcc/all.h" +#include "rcc/ast.h" +#include "rcc/irgen.h" +#include "rcc/rv64.h" + +#include <cctype> +#include <cstdio> +#include <cstring> +#include <string> + +// External Flex/Bison declarations +extern FILE* yyin; +extern int yyparse(); +extern rcc::CompUnitNode* g_ast_root; + +namespace { + +struct Args { + std::string input; + std::string output; + int opt_level = 0; + bool dump_ast = false; + bool dump_ir = false; + bool emit_il = false; // Output QBE-compatible SSA text to -o (or stdout) + bool dbg = false; // Set if any -d flag is enabled; triggers skipping emit +}; + +Args parse_args(int argc, char* argv[]) { + Args args; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-S") == 0) { + continue; + } else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) { + args.output = argv[++i]; + } else if (strcmp(argv[i], "-O1") == 0) { + args.opt_level = 1; + } else if (strcmp(argv[i], "-O2") == 0) { + args.opt_level = 2; + } else if (strcmp(argv[i], "--dump-ast") == 0) { + args.dump_ast = true; + } else if (strcmp(argv[i], "--dump-ir") == 0) { + args.dump_ir = true; + } else if (strcmp(argv[i], "--emit-il") == 0) { + args.emit_il = true; + } else if (strcmp(argv[i], "-d") == 0 && i + 1 < argc) { + for (char* p = argv[++i]; *p; ++p) { + unsigned char c = static_cast<unsigned char>(*p); + if (std::isalpha(c)) { + rcc::debug[std::toupper(c)] = 1; + args.dbg = true; + } + } + } else if (argv[i][0] != '-') { + args.input = argv[i]; + } + } + + if (args.input.empty()) { + fprintf(stderr, "rcc: error: no input file\n"); + fprintf(stderr, "usage: compiler input.sy -S -o output.s [-O1] [--emit-il] [-d <flags>]\n"); + exit(1); + } + if (args.output.empty()) { + args.output = args.emit_il ? "-" : "a.s"; + } + return args; +} + +// globals → Dat* → emitdat + +constexpr size_t kElemBytes = 4; + +void emit_one_global(const rcc::GlobalData& g, FILE* f) { + rcc::Lnk lnk{}; + lnk.align = 4; + // Do not export: SysY globals are not visible externally. + + std::string name = g.name; + rcc::Dat d{}; + d.lnk = &lnk; + d.name = name.data(); + + d.type = rcc::Dat::DStart; + rcc::emitdat(&d, f); + + if (g.is_zeroinit) { + d.type = rcc::Dat::DZ; + d.u.num = static_cast<int64_t>(g.total_elems * kElemBytes); + rcc::emitdat(&d, f); + } else { + for (const auto& v : g.init_vals) { + int32_t bits32 = 0; + if (std::holds_alternative<int32_t>(v)) { + int32_t iv = std::get<int32_t>(v); + if (g.elem_cls == rcc::Ks) { + float fv = static_cast<float>(iv); + std::memcpy(&bits32, &fv, sizeof(bits32)); + } else { + bits32 = iv; + } + } else { + float fv = std::get<float>(v); + if (g.elem_cls == rcc::Kw) { + bits32 = static_cast<int32_t>(fv); + } else { + std::memcpy(&bits32, &fv, sizeof(bits32)); + } + } + d.type = rcc::Dat::DW; + d.isref = 0; + d.isstr = 0; + d.u.num = static_cast<int64_t>(static_cast<uint32_t>(bits32)); + rcc::emitdat(&d, f); + } + } + + d.type = rcc::Dat::DEnd; + rcc::emitdat(&d, f); +} + +void emit_globals_asm(const std::vector<rcc::GlobalData>& globals, FILE* f) { + for (const auto& g : globals) + emit_one_global(g, f); +} + +// Mid-end pass chain +// ssacheck after gcm; ifconvert only if '-d K' is set. +void run_midend(rcc::Ctx* ctx, rcc::Fn* fn) { + ctx->fn = fn; + ctx->spill_fp_live_peak = 0; + rcc::T.abi0(fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::promote(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssa(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::filldom(fn); + rcc::loopinterchange(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::ssacheck(fn); + rcc::lsr(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::loop_idiom(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::ssacheck(fn); + rcc::dce(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::reduction_unroll(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::ssacheck(fn); + rcc::dce(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::dead_loop(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::unroll(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::unswitch(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::tre(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::fillalias(fn); + rcc::loadopt(ctx, fn); + rcc::filluse(ctx, fn); + rcc::sccp(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::ssacheck(fn); + rcc::fillalias(fn); + rcc::licm(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::fillalias(fn); + rcc::scalar_repl(ctx, fn); + rcc::filluse(ctx, fn); + rcc::coalesce(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::ssacheck(fn); + rcc::sccp(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::ssacheck(fn); + rcc::reassoc(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + rcc::gvn(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::simplcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::gcm(ctx, fn); + rcc::filluse(ctx, fn); + rcc::divopt(ctx, fn); + rcc::filluse(ctx, fn); + rcc::mulopt(ctx, fn); + rcc::filluse(ctx, fn); + rcc::storemrg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::dce(ctx, fn); + rcc::filluse(ctx, fn); + rcc::ssacheck(fn); + if (rcc::T.cansel) { + rcc::ifconvert(ctx, fn); + rcc::fillcfg(ctx, fn); + rcc::filluse(ctx, fn); + rcc::filldom(fn); + rcc::ssacheck(fn); + } +} + +// Backend pass chain +void run_backend(rcc::Ctx* ctx, rcc::Fn* fn) { +#define STAGE(call) \ + do { \ + call; \ + if (rcc::debug['B']) { \ + fprintf(stderr, "\n>>>> after %s (%s):\n", #call, fn->name); \ + rcc::printfn(fn, stderr); \ + } \ + } while (0) + STAGE(rcc::T.abi1(ctx, fn)); + STAGE(rcc::simpl(ctx, fn)); + STAGE(rcc::fillcfg(ctx, fn)); + STAGE(rcc::filluse(ctx, fn)); + STAGE(rcc::T.isel(ctx, fn)); + STAGE(rcc::fillcfg(ctx, fn)); + STAGE(rcc::filllive(ctx, fn)); + STAGE(rcc::fillloop(fn)); + STAGE(rcc::fillcost(ctx, fn)); + STAGE(rcc::spill(ctx, fn)); + STAGE(rcc::rega(ctx, fn)); + STAGE(rcc::peephole(ctx, fn)); + STAGE(rcc::fillcfg(ctx, fn)); + STAGE(rcc::simpljmp(ctx, fn)); + STAGE(rcc::fillcfg(ctx, fn)); +#undef STAGE + + // Convert rpo[] to linked list for rv64_emitfn + assert(fn->rpo[0] == fn->start); + for (uint n = 0;; ++n) { + if (n == fn->nblk - 1) { + fn->rpo[n]->link = nullptr; + break; + } + fn->rpo[n]->link = fn->rpo[n + 1]; + } +} + +} // namespace + +int main(int argc, char* argv[]) { + auto args = parse_args(argc, argv); + + // Front-end: Lexical + Syntax Analysis → AST + yyin = fopen(args.input.c_str(), "r"); + if (!yyin) { + fprintf(stderr, "rcc: error: cannot open '%s'\n", args.input.c_str()); + return 1; + } + + int parse_ret = yyparse(); + fclose(yyin); + + if (parse_ret != 0 || !g_ast_root) { + fprintf(stderr, "rcc: error: parsing failed\n"); + return 1; + } + + if (args.dump_ast) { + rcc::ast_dump(g_ast_root); + } + + // Target descriptor: rv64 lp64d. Assigned before worker creation, read-only afterwards. + rcc::T = rcc::T_rv64; + + rcc::Ctx ctx_main; + ctx_main.init(); + ctx_main.subpool = new rcc::SubPool(); + ctx_main.dbg_buf = stderr; // Dump destination for if (debug['X']) + + // IR Generation: AST → SSA IR (Braun algorithm) + rcc::IRGen irgen(&ctx_main); + auto ir = irgen.generate(g_ast_root); + + if (args.dump_ir) { + fprintf(stderr, "[%zu functions, %zu globals]\n", ir.functions.size(), ir.globals.size()); + } + + rcc::inline_module(&ctx_main, ir.functions); + + if (args.emit_il) { + for (rcc::Fn* fn : ir.functions) + run_midend(&ctx_main, fn); + + FILE* out = (args.output == "-") ? stdout : fopen(args.output.c_str(), "w"); + if (!out) { + fprintf(stderr, "rcc: error: cannot open '%s' for write\n", args.output.c_str()); + return 1; + } + rcc::print_globals(ir.globals, out); + for (rcc::Fn* fn : ir.functions) + rcc::printfn(fn, out); + if (out != stdout) + fclose(out); + } else { + FILE* outf = nullptr; + if (!args.dbg) { + outf = (args.output == "-") ? stdout : fopen(args.output.c_str(), "w"); + if (!outf) { + fprintf(stderr, "rcc: error: cannot open '%s' for write\n", args.output.c_str()); + return 1; + } + emit_globals_asm(ir.globals, outf); + } + + for (rcc::Fn* fn : ir.functions) { + if (args.dbg) + fprintf(stderr, "**** Function %s ****", fn->name); + run_midend(&ctx_main, fn); + run_backend(&ctx_main, fn); + if (!args.dbg) { + rcc::set_die_ctx(&ctx_main); + rcc::T.emitfn(fn, outf); + fprintf(outf, "/* end function %s */\n\n", fn->name); + } else { + fprintf(stderr, "\n"); + } + } + + if (!args.dbg) { + rcc::T.emitfin(&ctx_main, outf); + if (outf != stdout) + fclose(outf); + } + } + + // Clean AST + delete g_ast_root; + g_ast_root = nullptr; + + // Release SubPool; ~Ctx automatically frees insb + ctx_main.release(); + + return 0; +} diff --git a/src/mid/abi.cc b/src/mid/abi.cc new file mode 100644 index 0000000..c4924da --- /dev/null +++ b/src/mid/abi.cc @@ -0,0 +1,45 @@ +/* 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 abi.cc +/// @brief elimsb - Eliminate ABI-level sub-word op variants (sb/ub/sh/uh for arg/par/ret). +/// +/// See docs/abi.md for architectural notes. + +#include "rcc/all.h" + +namespace rcc { + +void elimsb(Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) { + Ins* ie = b->ins + b->nins; + for (Ins* i = b->ins; i < ie; ++i) { + if (isargbh(i->op)) { + i->op = Oarg; + } else if (isparbh(i->op)) { + i->op = Opar; + } + } + if (isretbh(b->jmp.type)) { + b->jmp.type = Jretw; + } + } +} + +} // namespace rcc diff --git a/src/mid/alias.cc b/src/mid/alias.cc new file mode 100644 index 0000000..8dc6041 --- /dev/null +++ b/src/mid/alias.cc @@ -0,0 +1,445 @@ +/* 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 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 <cstring> + +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<int>(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<int>(NBit) || off < 0 || off >= static_cast<int64_t>(NBit)) + m = static_cast<bits>(-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<int64_t>(NBit)) + a->u.loc.sz = static_cast<int>(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 diff --git a/src/mid/cfg.cc b/src/mid/cfg.cc new file mode 100644 index 0000000..e03b1e1 --- /dev/null +++ b/src/mid/cfg.cc @@ -0,0 +1,582 @@ +/* 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 cfg.cc +/// @brief CFG structure analysis and simplification passes. +/// +/// See docs/cfg.md for architectural notes. + +#include "rcc/all.h" + +#include <utility> + +namespace rcc { + +// Block construction + +Blk* newblk(Ctx* ctx) { + auto* b = static_cast<Blk*>(alloc(ctx, sizeof(Blk))); + memset(b, 0, sizeof *b); + b->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn)); + b->pred = static_cast<Blk**>(vnew(ctx, 0, sizeof(Blk*), PFn)); + return b; +} + +// Reverse Post-Order (RPO) numbering + dead block elimination + +namespace { + +void post_visit(Blk* b, uint* counter) { + if (!b || b->visit) + return; + b->visit = 1; + // Heuristic: recurse outer branch first. + Blk* a = b->s1; + Blk* c = b->s2; + if (a && c && a->loop > c->loop) + std::swap(a, c); + post_visit(a, counter); + post_visit(c, counter); + b->id = (*counter)++; +} + +void fillrpo(Ctx* ctx, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + b->visit = 0; + + uint cnt = 0; + post_visit(fn->start, &cnt); + fn->nblk = cnt; + + vensure(ctx, &fn->rpo, fn->nblk, sizeof(Blk*), PFn); + + // Second pass: remove unvisited, invert post-order. + Blk** slot = &fn->start; + while (Blk* b = *slot) { + if (!b->visit) { + b->id = ~0u; + *slot = b->link; + } else { + b->id = fn->nblk - 1 - b->id; + fn->rpo[b->id] = b; + slot = &b->link; + } + } +} + +} // namespace + +// Predecessor sets + +void fillpreds(Ctx* ctx, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + b->npred = 0; + + for (Blk* src = fn->start; src; src = src->link) { + if (src->s1) + src->s1->npred++; + if (src->s2 && src->s2 != src->s1) + src->s2->npred++; + } + + for (Blk* b = fn->start; b; b = b->link) { + if (b->npred) + vensure(ctx, &b->pred, b->npred, sizeof(Blk*), PFn); + b->npred = 0; + } + + for (Blk* src = fn->start; src; src = src->link) { + if (src->s1) + src->s1->pred[src->s1->npred++] = src; + if (src->s2 && src->s2 != src->s1) + src->s2->pred[src->s2->npred++] = src; + } +} + +// phi cleanup. + +namespace { + +void fixphis(Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) { + for (Phi* p = b->phi; p; p = p->link) { + uint keep = 0; + for (uint i = 0; i < p->narg; i++) { + Blk* src = p->blk[i]; + if (src->id == ~0u) + continue; + if (src->s1 != b && src->s2 != b) + continue; + if (keep != i) { + p->blk[keep] = src; + p->arg[keep] = p->arg[i]; + } + keep++; + } + assert(keep > 0 && "phi has no live incoming edge"); + p->narg = keep; + } + } +} + +} // namespace + +void fillcfg(Ctx* ctx, Fn* fn) { + fillrpo(ctx, fn); + fillpreds(ctx, fn); + fixphis(fn); +} + +// Dominator Tree (Cooper-Harvey-Kennedy) + +namespace { + +/// Walk up idom chain to find the lowest common ancestor of x and y in dom-tree. +/// Invariant: Larger RPO id means a deeper node in the tree, move the deeper one up by one step +/// each time. +Blk* dom_meet(Blk* x, Blk* y) { + if (!x) + return y; + while (x != y) { + if (x->id < y->id) + std::swap(x, y); + x = x->idom; + assert(x); + } + return x; +} + +} // namespace + +void filldom(Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) { + b->idom = nullptr; + b->dom = nullptr; + b->dlink = nullptr; + } + + // Fixpoint iteration. + for (bool changed = true; changed;) { + changed = false; + for (uint i = 1; i < fn->nblk; i++) { + Blk* b = fn->rpo[i]; + Blk* d = nullptr; + for (uint k = 0; k < b->npred; k++) { + Blk* p = b->pred[k]; + if (p->idom || p == fn->start) + d = dom_meet(d, p); + } + if (d != b->idom) { + b->idom = d; + changed = true; + } + } + } + + // Reverse chain: attach dom-children. + for (Blk* b = fn->start; b; b = b->link) { + if (Blk* d = b->idom) { + assert(d != b); + b->dlink = d->dom; + d->dom = b; + } + } +} + +int sdom(Blk* a, Blk* b) { + assert(a && b); + if (a == b) + return 0; + while (b->id > a->id) + b = b->idom; + return a == b; +} + +int dom(Blk* a, Blk* b) { + return a == b || sdom(a, b); +} + +// Dominance Frontier + +namespace { + +void push_fron(Ctx* ctx, Blk* host, Blk* node) { + for (uint i = 0; i < host->nfron; i++) + if (host->fron[i] == node) + return; + vensure(ctx, &host->fron, host->nfron + 1, sizeof(Blk*), PFn); + host->fron[host->nfron++] = node; +} + +/// For edge (src -> dst): walk up idom chain from src, collect dst into the dominance frontier of +/// all ancestors that do not strictly dominate dst. +void contribute_fron(Ctx* ctx, Blk* src, Blk* dst) { + for (Blk* a = src; a && !sdom(a, dst); a = a->idom) + push_fron(ctx, a, dst); +} + +} // namespace + +void fillfron(Ctx* ctx, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + b->nfron = 0; + for (Blk* b = fn->start; b; b = b->link) { + if (b->s1) + contribute_fron(ctx, b, b->s1); + if (b->s2) + contribute_fron(ctx, b, b->s2); + } +} + +// Natural loop identification / Loop weights + +namespace { + +void mark_loop_body(Blk* hd, Blk* node, void (*cb)(Blk*, Blk*)) { + if (node->id < hd->id || node->visit == hd->id) + return; + node->visit = hd->id; + cb(hd, node); + for (uint k = 0; k < node->npred; k++) + mark_loop_body(hd, node->pred[k], cb); +} + +void loop_x10(Blk* hd, Blk* b) { + (void)hd; + b->loop *= 10; +} + +} // namespace + +void loopiter(Fn* fn, void (*cb)(Blk*, Blk*)) { + for (Blk* b = fn->start; b; b = b->link) + b->visit = ~0u; + for (uint i = 0; i < fn->nblk; i++) { + Blk* hd = fn->rpo[i]; + for (uint k = 0; k < hd->npred; k++) + if (hd->pred[k]->id >= i) + mark_loop_body(hd, hd->pred[k], cb); + } +} + +void fillloop(Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + b->loop = 1; + loopiter(fn, loop_x10); +} + +// dom-tree depth + lowest common ancestor + +void filldepth(Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + b->depth = -1; + fn->start->depth = 0; + + // Walk up idom chain. + for (Blk* b = fn->start; b; b = b->link) { + if (b->depth != -1) + continue; + Blk* d = b->idom; + int steps = 1; + while (d->depth == -1) { + d = d->idom; + steps++; + } + steps += d->depth; + b->depth = steps; + for (Blk* a = b->idom; a->depth == -1; a = a->idom) + a->depth = --steps; + } +} + +Blk* lca(Blk* a, Blk* b) { + if (!a) + return b; + if (!b) + return a; + while (a->depth > b->depth) + a = a->idom; + while (b->depth > a->depth) + b = b->idom; + while (a != b) { + a = a->idom; + b = b->idom; + } + return a; +} + +// simpljmp: fold empty transit blocks. + +namespace { + +/// Union-find query with path compression: find root along uf[], redirecting each node on the chain +/// to root. +Blk* uf_find(Blk** uf, Blk* x) { + Blk* root = x; + while (uf[root->id]) + root = uf[root->id]; + while (uf[x->id] && uf[x->id] != root) { + Blk* nxt = uf[x->id]; + uf[x->id] = root; + x = nxt; + } + return root; +} + +} // namespace + +void simpljmp(Ctx* ctx, Fn* fn) { + Blk* exit_blk = newblk(ctx); + exit_blk->id = fn->nblk++; + exit_blk->jmp.type = Jret0; + exit_blk->name = strf(ctx, PFn, "ret"); + + Blk** uf = static_cast<Blk**>(emalloc(fn->nblk * sizeof(Blk*))); + memset(uf, 0, fn->nblk * sizeof(Blk*)); + + for (Blk* b = fn->start; b; b = b->link) { + assert(!b->phi && "simpljmp requires phi-free CFG"); + if (b->jmp.type == Jret0) { + b->jmp.type = Jjmp; + b->s1 = exit_blk; + } + if (b->nins == 0 && b->jmp.type == Jjmp) { + Blk* r = uf_find(uf, b->s1); + if (r != b) + uf[b->id] = r; + } + } + + Blk** tail = &fn->start; + for (Blk* b; (b = *tail); tail = &b->link) { + if (b->s1) + b->s1 = uf_find(uf, b->s1); + if (b->s2) + b->s2 = uf_find(uf, b->s2); + if (b->s1 && b->s1 == b->s2) { + b->jmp.type = Jjmp; + b->s2 = nullptr; + } + } + *tail = exit_blk; + + free(uf); +} + +// Reachability / if-graph pattern recognition + +namespace { + +int dfs_can_reach(Blk* from, Blk* to) { + if (from == to) + return 1; + if (!from || from->visit) + return 0; + from->visit = 1; + if (dfs_can_reach(from->s1, to)) + return 1; + if (dfs_can_reach(from->s2, to)) + return 1; + return 0; +} + +} // namespace + +int reaches(Fn* fn, Blk* from, Blk* to) { + assert(to); + int r = dfs_can_reach(from, to); + for (Blk* b = fn->start; b; b = b->link) + b->visit = 0; + return r; +} + +int reachesnotvia(Fn* fn, Blk* from, Blk* to, Blk* excl) { + excl->visit = 1; + return reaches(fn, from, to); +} + +int ifgraph(Blk* head, Blk** pthen, Blk** pelse, Blk** pjoin) { + if (head->jmp.type != Jjnz) + return 0; + + Blk* lo = head->s1; + Blk* hi = head->s2; + bool swapped = false; + if (lo->id > hi->id) { + std::swap(lo, hi); + swapped = true; + } + if (lo == hi) + return 0; + if (lo->jmp.type != Jjmp || lo->npred != 1) + return 0; + + Blk** out_then = swapped ? pelse : pthen; + Blk** out_else = swapped ? pthen : pelse; + + if (lo->s1 == hi) { + // if-then (no else branch: head directly serves as else) + if (hi->npred != 2) + return 0; + *out_then = lo; + *out_else = head; + *pjoin = hi; + return 1; + } + + if (hi->jmp.type != Jjmp || hi->npred != 1) + return 0; + if (lo->s1 != hi->s1 || lo->s1->npred != 2) + return 0; + + assert(lo->s1 != head); + *out_then = lo; + *out_else = hi; + *pjoin = lo->s1; + return 1; +} + +// simplcfg + +namespace { + +struct JmpSnap { + short type; + Ref arg; + Blk* s1; + Blk* s2; +}; + +bool snap_eq(const JmpSnap& a, const JmpSnap& b) { + return a.type == b.type && req(a.arg, b.arg) && a.s1 == b.s1 && a.s2 == b.s2; +} + +bool target_phi_free(const JmpSnap& j) { + if (j.s1 && j.s1->phi) + return false; + if (j.s2 && j.s2->phi) + return false; + return true; +} + +bool block_is_empty(Blk* b) { + if (b->phi) + return false; + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (i->op != Onop && i->op != Odbgloc) + return false; + return true; +} + +void phi_to_copies(Ctx* ctx, Blk* b) { + assert(b->npred == 1); + Blk* p = b->pred[0]; + Ins copy{}; + copy.op = Ocopy; + for (Phi* phi = b->phi; phi; phi = phi->link) { + copy.cls = static_cast<uint>(phi->cls); + copy.to = phi->to; + copy.arg[0] = phiarg(phi, p); + copy.arg[1] = Ref::none(); + addins(ctx, &p->ins, &p->nins, ©); + } + b->phi = nullptr; +} + +} // namespace + +void simplcfg(Ctx* ctx, Fn* fn) { + if (debug['C']) { + fprintf(ctx->dbg_buf, "\n> Before CFG simplification:\n"); + printfn(fn, ctx->dbg_buf); + } + + for (Blk* b = fn->start; b; b = b->link) + if (b->npred == 1) + phi_to_copies(ctx, b); + + auto* snap = static_cast<JmpSnap*>(emalloc(fn->nblk * sizeof(JmpSnap))); + auto* empty = static_cast<bool*>(emalloc(fn->nblk * sizeof(bool))); + for (Blk* b = fn->start; b; b = b->link) { + snap[b->id] = {b->jmp.type, b->jmp.arg, b->s1, b->s2}; + empty[b->id] = block_is_empty(b); + } + + bool dirty; + do { + dirty = false; + for (Blk* b = fn->start; b; b = b->link) { + if (b->id == ~0u) + continue; + JmpSnap* j = &snap[b->id]; + + if (j->type == Jjmp && j->s1->npred == 1) { + Blk* s = j->s1; + assert(!s->phi); + addbins(ctx, &b->ins, &b->nins, s); + empty[b->id] = empty[b->id] && empty[s->id]; + + JmpSnap* sj = &snap[s->id]; + Blk* succs[2] = {sj->s1, sj->s2}; + for (Blk* sb : succs) { + if (!sb) + continue; + for (Phi* p = sb->phi; p; p = p->link) { + uint n = phiargn(p, s); + if (n != ~0u) + p->blk[n] = b; + } + } + + s->id = ~0u; + *j = *sj; + dirty = true; + } else if (j->type == Jjnz && empty[j->s1->id] && empty[j->s2->id] && + snap_eq(snap[j->s1->id], snap[j->s2->id]) && + target_phi_free(snap[j->s1->id])) { + *j = snap[j->s1->id]; + dirty = true; + } + } + } while (dirty); + + for (Blk* b = fn->start; b; b = b->link) { + if (b->id == ~0u) + continue; + JmpSnap* j = &snap[b->id]; + b->jmp.type = j->type; + b->jmp.arg = j->arg; + b->s1 = j->s1; + b->s2 = j->s2; + assert(!j->s1 || j->s1->id != ~0u); + assert(!j->s2 || j->s2->id != ~0u); + } + + fillcfg(ctx, fn); + + free(empty); + free(snap); + + if (debug['C']) { + fprintf(ctx->dbg_buf, "\n> After CFG simplification:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +} // namespace rcc diff --git a/src/mid/copy.cc b/src/mid/copy.cc new file mode 100644 index 0000000..8a35498 --- /dev/null +++ b/src/mid/copy.cc @@ -0,0 +1,360 @@ +/* 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 copy.cc +/// @brief copy equivalence check + narrow parameter sinking - for GVN, no independent driver. +/// +/// See docs/copy.md for architectural notes. + +#include "rcc/all.h" + +#include <algorithm> + +namespace rcc { + +namespace { + +struct Ext { + char zext; + char nopw; + char usew; +}; + +bool decode_ext(int op, Ext* e) { + switch (op) { + case Oextsb: + *e = Ext{0, 7, 8}; + return true; + case Oextub: + *e = Ext{1, 8, 8}; + return true; + case Oextsh: + *e = Ext{0, 15, 16}; + return true; + case Oextuh: + *e = Ext{1, 16, 16}; + return true; + case Oextsw: + *e = Ext{0, 31, 32}; + return true; + case Oextuw: + *e = Ext{1, 32, 32}; + return true; + } + return false; +} + +int bitwidth(uint64_t v) { + return v ? 64 - __builtin_clzll(v) : 0; +} + +template <typename Walk> +int walk_phi_memo(Fn* fn, Ref r, int w, Walk walk) { + int ret = walk(fn, r, w); + for (Blk* b = fn->start; b; b = b->link) + for (Phi* p = b->phi; p; p = p->link) + p->visit = 0; + return ret; +} + +int uwl(Fn* fn, Ref r, int w); + +bool phi_use_ok(Fn* fn, Phi* p, int w) { + if (p->visit || req(p->to, Ref::none())) + return true; + p->visit = 1; + return uwl(fn, p->to, w) != 0; +} + +bool ins_use_ok(Fn* fn, Ins* i, Ref r, int w) { + Ext e; + if (i->op == Ocopy && uwl(fn, i->to, w)) + return true; + if (decode_ext(i->op, &e)) { + if (e.usew <= w) + return true; + return uwl(fn, i->to, w) != 0; + } + if (i->op == Oand) { + Ref rc; + if (req(r, i->arg[0])) { + rc = i->arg[1]; + } else { + assert(req(r, i->arg[1])); + rc = i->arg[0]; + } + int64_t v; + return isconbits(fn, rc, &v) && bitwidth(static_cast<uint64_t>(v)) <= w; + } + return false; +} + +int uwl(Fn* fn, Ref r, int w) { + assert(rtype(r) == RTmp); + Tmp* t = &fn->tmp[r.val]; + for (Use* u = &t->use[0]; u < &t->use[t->nuse]; ++u) { + bool ok; + switch (u->type) { + case Use::UPhi: + ok = phi_use_ok(fn, u->u.phi, w); + break; + case Use::UIns: + assert(u->u.ins); + ok = ins_use_ok(fn, u->u.ins, r, w); + break; + default: + ok = false; + break; + } + if (!ok) + return 0; + } + return 1; +} + +/* no more than w bits are used */ +int usewidthle(Fn* fn, Ref r, int w) { + return walk_phi_memo(fn, r, w, uwl); +} + +int dwl(Fn* fn, Ref r, int w) { + int64_t v; + if (isconbits(fn, r, &v) && bitwidth(static_cast<uint64_t>(v)) <= w) + return 1; + if (w <= 0) + return 0; + if (rtype(r) != RTmp) + return 0; + Tmp* t = &fn->tmp[r.val]; + if (t->cls != Kw) + return 0; + + if (!t->def) { + Phi* p = fn->rpo[t->bid]->phi; + for (; p; p = p->link) + if (req(p->to, r)) + break; + assert(p); + if (p->visit && p->visit <= w) + return 1; + p->visit = w; + for (uint n = 0; n < p->narg; ++n) + if (!dwl(fn, p->arg[n], w)) + return 0; + return 1; + } + + Ins* i = t->def; + if (i->op == Ocopy) + return dwl(fn, i->arg[0], w); + + if (i->op == Oshr || i->op == Osar) { + if (isconbits(fn, i->arg[1], &v) && 0 < v && v <= 32) { + if (i->op == Oshr && w + v >= 32) + return 1; + if (w < 32) { + int cap = (i->op == Osar) ? 31 : 32; + w = std::min(cap, static_cast<int>(w + v)); + } + } + return dwl(fn, i->arg[0], w); + } + + int x; + if (iscmp(i->op, &x, &x)) + return w >= 1; + + if (i->op == Oand) + return (dwl(fn, i->arg[0], w) || dwl(fn, i->arg[1], w)) ? 1 : 0; + + if (i->op == Oor || i->op == Oxor) + return (dwl(fn, i->arg[0], w) && dwl(fn, i->arg[1], w)) ? 1 : 0; + + Ext e; + if (decode_ext(i->op, &e)) { + if (e.zext && e.usew <= w) + return 1; + w = std::min(w, static_cast<int>(e.nopw)); + return dwl(fn, i->arg[0], w); + } + + return 0; +} + +/* is the ref narrower than w bits */ +int defwidthle(Fn* fn, Ref r, int w) { + return walk_phi_memo(fn, r, w, dwl); +} + +int isw1(Fn* fn, Ref r) { + return defwidthle(fn, r, 1); +} + +bool phieq(Phi* pa, Phi* pb) { + assert(pa->narg == pb->narg); + for (uint n = 0; n < pa->narg; ++n) + if (!req(pa->arg[n], phiarg(pb, pa->blk[n]))) + return false; + return true; +} + +} // namespace + +Ref copyref(Ctx* ctx, Fn* fn, Blk* b, Ins* i) { + static const bits extcpy[] = { + /*WFull*/ 0, + /*Wsb*/ BIT(Tmp::Wsb) | BIT(Tmp::Wsh) | BIT(Tmp::Wsw), + /*Wub*/ BIT(Tmp::Wub) | BIT(Tmp::Wuh) | BIT(Tmp::Wuw), + /*Wsh*/ BIT(Tmp::Wsh) | BIT(Tmp::Wsw), + /*Wuh*/ BIT(Tmp::Wuh) | BIT(Tmp::Wuw), + /*Wsw*/ BIT(Tmp::Wsw), + /*Wuw*/ BIT(Tmp::Wuw), + }; + MAKESURE(extcpy_covers_all_widths, sizeof(extcpy) / sizeof(extcpy[0]) == Tmp::Wuw + 1); + + if (i->op == Ocopy) + return i->arg[0]; + + const Op& od = optab[i->op]; + + if (od.hasid && KBASE(i->cls) == 0 && req(i->arg[1], ctx->gvn_con01[od.idval]) && + (!od.cmpeqwl || isw1(fn, i->arg[0]))) + return i->arg[0]; + + if (od.idemp && req(i->arg[0], i->arg[1])) + return i->arg[0]; + + if ((od.cmpeqwl || od.cmplgtewl) && req(i->arg[0], i->arg[1])) + return ctx->gvn_con01[od.eqval]; + + int z; + if (od.cmpeqwl && req(i->arg[1], Ref::con_z()) && zeroval(fn, b, i->arg[0], argcls(i, 0), &z)) + return ctx->gvn_con01[od.eqval ^ z ^ 1]; + + int64_t v; + if (i->op == Oand && isconbits(fn, i->arg[1], &v) && v > 0 && ((v + 1) & v) == 0 && + defwidthle(fn, i->arg[0], bitwidth(static_cast<uint64_t>(v)))) + return i->arg[0]; + + if (i->cls == Kw && (i->op == Oextsw || i->op == Oextuw)) + return i->arg[0]; + + Ext e; + if (decode_ext(i->op, &e) && rtype(i->arg[0]) == RTmp) { + Tmp* t = &fn->tmp[i->arg[0].val]; + assert(KBASE(t->cls) == 0); + + if (KWIDE(i->cls) > KWIDE(t->cls)) + return Ref::none(); + + int w = Tmp::Wsb + (static_cast<int>(i->op) - Oextsb); + if (BIT(w) & extcpy[t->width]) + return i->arg[0]; + + if ((!t->def || !ispar(t->def->op)) && usewidthle(fn, i->to, e.usew)) + return i->arg[0]; + + if (defwidthle(fn, i->arg[0], e.nopw)) + return i->arg[0]; + } + + return Ref::none(); +} + +Ref phicopyref(Ctx* ctx, Fn* fn, Blk* b, Phi* p) { + Ref r = Ref::none(); + uint n = 0; + for (; n < p->narg; ++n) + if (!req(p->arg[n], p->to)) { + if (req(r, Ref::none())) + r = p->arg[n]; + else if (!req(p->arg[n], r)) + break; + } + if (n == p->narg) + return r; + + for (Phi* p1 = b->phi; p1 != p; p1 = p1->link) { + assert(p1); + if (phieq(p1, p)) + return p1->to; + } + + Blk* d = b->idom; + if (p->narg != 2 || d->jmp.type != Jjnz || !isw1(fn, d->jmp.arg)) + return Ref::none(); + + Blk* s[2] = {nullptr, nullptr}; + for (uint k = 0; k < 2; ++k) + for (uint c = 0; c < 2; ++c) + if (req(p->arg[k], ctx->gvn_con01[c])) + s[c] = p->blk[k]; + + if (d->s1 == s[1] && d->s2 == s[0] && d->s1->jmp.type == Jjmp && d->s2->jmp.type == Jjmp) + return d->jmp.arg; + + return Ref::none(); +} + +void narrowpars(Ctx* ctx, Fn* fn) { + bool has_loop = false; + for (Blk* b = fn->start; b; b = b->link) + if (b->loop > 1) { + has_loop = true; + break; + } + if (!has_loop) + return; + + Blk* b = fn->start; + + uint npar = 0; + for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) { + if (!ispar(i->op)) + break; + ++npar; + } + if (npar == 0) + return; + + uint nins = b->nins + npar; + Ins* ins = static_cast<Ins*>(vnew(ctx, nins, sizeof(Ins), PFn)); + icpy(ins, b->ins, npar); + icpy(ins + 2 * npar, b->ins + npar, b->nins - npar); + b->ins = ins; + b->nins = nins; + + for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) { + if (!ispar(i->op)) + break; + Ins extins = {}; + extins.op = Onop; + if (i->cls == Kw && usewidthle(fn, i->to, 16)) { + extins.op = usewidthle(fn, i->to, 8) ? Oextub : Oextuh; + Ref r = newtmp(ctx, "vw", i->cls, fn); + extins.cls = i->cls; + extins.to = i->to; + extins.arg[0] = r; + i->to = r; + } + *(i + npar) = extins; + } +} + +} // namespace rcc diff --git a/src/mid/dce.cc b/src/mid/dce.cc new file mode 100644 index 0000000..6ee631a --- /dev/null +++ b/src/mid/dce.cc @@ -0,0 +1,149 @@ +/* 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 dce.cc +/// @brief Dead Code Elimination: mark-and-sweep starting from side-effect / control flow roots. +/// +/// See docs/dce.md for architectural notes. + +#include "rcc/all.h" + +#include <vector> + +namespace rcc { + +namespace { + +bool is_root(uint op) { + if (isstore(op)) + return true; + if (isalloc(op)) + return true; + if (ispar(op)) + return true; + if (isarg(op)) + return true; + if (op == Ocall) + return true; + if (op == Oblit0 || op == Oblit1) + return true; + if (op == Ovastart || op == Ovaarg) + return true; + if (op == Odbgloc) + return true; + + if (op == Osel0 || op == Osel1) + return true; + return false; +} + +} // namespace + +void dce(Ctx*, Fn* fn) { + int ntmp = fn->ntmp; + std::vector<Phi*> phi_def(static_cast<size_t>(ntmp), nullptr); + std::vector<Ins*> ins_def(static_cast<size_t>(ntmp), nullptr); + + for (Blk* b = fn->start; b; b = b->link) { + for (Phi* p = b->phi; p; p = p->link) + if (rtype(p->to) == RTmp && p->to.val < static_cast<uint>(ntmp)) + phi_def[p->to.val] = p; + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (rtype(i->to) == RTmp && i->to.val < static_cast<uint>(ntmp)) + ins_def[i->to.val] = i; + } + } + + std::vector<char> alive(static_cast<size_t>(ntmp), 0); + std::vector<uint> wl; + auto mark = [&](Ref r) { + if (rtype(r) == RTmp && r.val < static_cast<uint>(ntmp) && !alive[r.val]) { + alive[r.val] = 1; + wl.push_back(r.val); + } + }; + + for (Blk* b = fn->start; b; b = b->link) { + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (is_root(i->op)) { + mark(i->arg[0]); + mark(i->arg[1]); + } + } + mark(b->jmp.arg); + } + + while (!wl.empty()) { + uint t = wl.back(); + wl.pop_back(); + if (Ins* d = ins_def[t]) { + mark(d->arg[0]); + mark(d->arg[1]); + continue; + } + if (Phi* p = phi_def[t]) { + for (uint a = 0; a < p->narg; a++) + mark(p->arg[a]); + } + } + + int killed_ins = 0, killed_phi = 0; + for (Blk* b = fn->start; b; b = b->link) { + Phi** pp = &b->phi; + while (*pp) { + Phi* p = *pp; + bool dead = + (rtype(p->to) == RTmp && p->to.val < static_cast<uint>(ntmp) && !alive[p->to.val]); + if (dead) { + *pp = p->link; + killed_phi++; + } else { + pp = &p->link; + } + } + + uint w = 0; + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (i->op == Onop) { + killed_ins++; + continue; + } + bool root = is_root(i->op); + bool has_to = (rtype(i->to) == RTmp && i->to.val < static_cast<uint>(ntmp)); + if (!root && has_to && !alive[i->to.val]) { + killed_ins++; + continue; + } + if (w != k) + b->ins[w] = *i; + w++; + } + b->nins = w; + } + + if (debug['D']) { + fprintf(stderr, "\n> DCE for %s: killed %d ins, %d phi\n", fn->name, killed_ins, + killed_phi); + } +} + +} // namespace rcc diff --git a/src/mid/dead_loop.cc b/src/mid/dead_loop.cc new file mode 100644 index 0000000..26b915a --- /dev/null +++ b/src/mid/dead_loop.cc @@ -0,0 +1,227 @@ +/* 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 dead_loop.cc +/// @brief Dead Loop Elimination: removal of entire loops that have no side-effects, are provably +/// terminating, and have no live-outs. +/// +/// See docs/dead_loop.md for architectural notes. + +#include "rcc/all.h" +#include "rcc/loopinfo.h" + +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +bool has_side_effect_op(uint op) { + if (isstore(op)) + return true; + if (isalloc(op)) + return true; + if (ispar(op)) + return true; + if (isarg(op)) + return true; + if (op == Ocall) + return true; + if (op == Oblit0 || op == Oblit1) + return true; + if (op == Ovastart || op == Ovaarg) + return true; + if (op == Odbgloc) + return true; + if (op == Osel0 || op == Osel1) + return true; + return false; +} + +bool defined_outside(Fn* fn, const Loop& L, Ref r) { + if (rtype(r) == RCon) + return true; + if (rtype(r) != RTmp) + return false; + if (r.val >= static_cast<uint>(fn->ntmp)) + return false; + uint bid = fn->tmp[r.val].bid; + return L.body_bid.find(bid) == L.body_bid.end(); +} + +Ins* find_cmp_in(Blk* b, Ref cond) { + for (uint i = 0; i < b->nins; i++) + if (req(b->ins[i].to, cond)) + return &b->ins[i]; + return nullptr; +} + +int find_biv_idx(const std::vector<Biv>& bivs, Ref r) { + for (size_t k = 0; k < bivs.size(); k++) + if (req(bivs[k].to, r)) + return static_cast<int>(k); + return -1; +} + +void collect_body_defs(const Loop& L, std::vector<Ref>& defs) { + for (Blk* b : L.body) { + for (Phi* p = b->phi; p; p = p->link) + if (rtype(p->to) == RTmp) + defs.push_back(p->to); + for (uint k = 0; k < b->nins; k++) + if (rtype(b->ins[k].to) == RTmp) + defs.push_back(b->ins[k].to); + } +} + +bool all_uses_inside(Fn* fn, const Loop& L, Ref tmp_ref) { + if (rtype(tmp_ref) != RTmp) + return true; + if (tmp_ref.val >= static_cast<uint>(fn->ntmp)) + return true; + Tmp* t = &fn->tmp[tmp_ref.val]; + for (uint u = 0; u < t->nuse; u++) { + const Use& uu = t->use[u]; + if (L.body_bid.find(uu.bid) == L.body_bid.end()) + return false; + } + return true; +} + +bool delete_loop_if_dead(Fn* fn, const Loop& L) { + // Topology: unique exit_block + unique exiting_block + Blk* exit_block = nullptr; + Blk* exiting_block = nullptr; + for (Blk* b : L.body) { + Blk* succs[2] = {b->s1, b->s2}; + for (Blk* s : succs) { + if (!s) + continue; + if (L.body_bid.find(s->id) != L.body_bid.end()) + continue; // Internal edge + // Exit loop edge + if (!exit_block) { + exit_block = s; + exiting_block = b; + } else if (exit_block != s || exiting_block != b) { + return false; // Multiple exits or multiple exiting blocks + } + } + } + if (!exit_block) + return false; // No exit -- infinite loop + + // Termination: at least one constant BIV + header Jjnz cmp(BIV, invariant) + std::vector<Biv> bivs; + detect_bivs(fn, L, bivs); + if (bivs.empty()) + return false; + if (L.header->jmp.type != Jjnz) + return false; + Ref cond = L.header->jmp.arg; + if (rtype(cond) != RTmp) + return false; + Ins* cmp = find_cmp_in(L.header, cond); + if (!cmp) + return false; + int bi = find_biv_idx(bivs, cmp->arg[0]); + Ref other = cmp->arg[1]; + if (bi < 0) { + bi = find_biv_idx(bivs, cmp->arg[1]); + other = cmp->arg[0]; + } + if (bi < 0) + return false; + if (!defined_outside(fn, L, other)) + return false; + if (exiting_block != L.header) + return false; + + // Side-effect scan + for (Blk* b : L.body) { + for (uint k = 0; k < b->nins; k++) { + if (has_side_effect_op(b->ins[k].op)) + return false; + } + } + + // Live-out scan + for (Phi* p = exit_block->phi; p; p = p->link) { + for (uint k = 0; k < p->narg; k++) { + if (p->blk[k] != exiting_block) + continue; + if (!defined_outside(fn, L, p->arg[k])) + return false; + } + } + std::vector<Ref> defs; + collect_body_defs(L, defs); + for (Ref r : defs) { + if (!all_uses_inside(fn, L, r)) + return false; + } + + for (Phi* p = exit_block->phi; p; p = p->link) { + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == L.preheader) + return false; + } + + // Rewrite + // preheader's jmp outgoing edge: the one pointing to header is changed to point to exit_block; + if (L.preheader->s1 == L.header) + L.preheader->s1 = exit_block; + if (L.preheader->s2 == L.header) + L.preheader->s2 = exit_block; + for (Phi* p = exit_block->phi; p; p = p->link) { + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == exiting_block) + p->blk[k] = L.preheader; + } + + return true; +} + +} // namespace + +void dead_loop(Ctx* ctx, Fn* fn) { + fillpreds(ctx, fn); + fillcfg(ctx, fn); + filluse(ctx, fn); + + std::vector<Loop> loops; + find_loops(fn, loops); + if (loops.empty()) + return; + + int deleted = 0; + for (const Loop& L : loops) { + if (delete_loop_if_dead(fn, L)) + deleted++; + } + + if (debug['L']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "\n> dead_loop %s: deleted %d of %zu loop(s)\n", fn->name, deleted, + loops.size()); + } +} + +} // namespace rcc diff --git a/src/mid/divopt.cc b/src/mid/divopt.cc new file mode 100644 index 0000000..6e9269f --- /dev/null +++ b/src/mid/divopt.cc @@ -0,0 +1,189 @@ +/* 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 divopt.cc +/// @brief Constant integer division/modulo strength reduction: rewrite `x / c`, `x % c` +/// into mul + shift + sign correction sequence when c is a compile-time constant. +/// +/// See docs/divopt.md for architectural notes. + +#include "rcc/all.h" + +#include <cstdint> + +namespace rcc { + +namespace { + +struct Magic { + int32_t M; + int s; + bool need_add; +}; + +Magic compute_magic_s32(uint32_t ad) { + const uint32_t t = 0x80000000u; // 2^31 + uint32_t anc = t - 1 - t % ad; + uint32_t p = 31; + uint32_t q1 = t / anc, r1 = t - q1 * anc; + uint32_t q2 = t / ad, r2 = t - q2 * ad; + uint32_t delta; + do { + ++p; + q1 *= 2; + r1 *= 2; + if (r1 >= anc) { + ++q1; + r1 -= anc; + } + q2 *= 2; + r2 *= 2; + if (r2 >= ad) { + ++q2; + r2 -= ad; + } + delta = ad - r2; + } while (q1 < delta || (q1 == delta && r1 == 0)); + + Magic m; + uint32_t M_u = q2 + 1; + m.M = static_cast<int32_t>(M_u); + m.s = static_cast<int>(p - 32); + m.need_add = (M_u >= 0x80000000u); + return m; +} + +void emit_pow2_div_into(Ctx* ctx, Fn* fn, Ref dst, Ref x, int k) { + Ref sign = newtmp(ctx, "divopt", Kw, fn); + Ref mask = newtmp(ctx, "divopt", Kw, fn); + Ref tmp = newtmp(ctx, "divopt", Kw, fn); + + emit(ctx, Osar, Kw, dst, tmp, getcon(ctx, k, fn)); + emit(ctx, Oadd, Kw, tmp, x, mask); + emit(ctx, Oshr, Kw, mask, sign, getcon(ctx, 32 - k, fn)); + emit(ctx, Osar, Kw, sign, x, getcon(ctx, 31, fn)); +} + +void emit_magic_div_into(Ctx* ctx, Fn* fn, Ref dst, Ref x, int32_t d_abs, bool d_neg) { + Magic m = compute_magic_s32(static_cast<uint32_t>(d_abs)); + int64_t M64 = static_cast<int64_t>(m.M); // Sign extend i32 to i64 + + Ref xl = newtmp(ctx, "divopt", Kl, fn); + Ref prod = newtmp(ctx, "divopt", Kl, fn); + Ref hi = newtmp(ctx, "divopt", Kl, fn); + Ref hi2 = m.need_add ? newtmp(ctx, "divopt", Kl, fn) : hi; + Ref q1 = (m.s > 0) ? newtmp(ctx, "divopt", Kl, fn) : hi2; + Ref sg = newtmp(ctx, "divopt", Kl, fn); + Ref qpre = d_neg ? newtmp(ctx, "divopt", Kw, fn) : dst; + + // Reverse emit: + if (d_neg) + emit(ctx, Oneg, Kw, dst, qpre, Ref::none()); + emit(ctx, Osub, Kw, qpre, q1, sg); + emit(ctx, Osar, Kl, sg, xl, getcon(ctx, 63, fn)); + if (m.s > 0) + emit(ctx, Osar, Kl, q1, hi2, getcon(ctx, m.s, fn)); + if (m.need_add) + emit(ctx, Oadd, Kl, hi2, hi, xl); + emit(ctx, Osar, Kl, hi, prod, getcon(ctx, 32, fn)); + emit(ctx, Omul, Kl, prod, xl, getcon(ctx, M64, fn)); + emit(ctx, Oextsw, Kl, xl, x, Ref::none()); +} + +bool lower_div(Ctx* ctx, Fn* fn, Ins* i, Blk* b, bool* rebuilt) { + if (i->op != Odiv && i->op != Orem) + return false; + if (i->cls != Kw) + return false; + if (rtype(i->arg[1]) != RCon) + return false; + Con* c = &fn->con[i->arg[1].val]; + if (c->type != Con::CBits) + return false; + int32_t d = static_cast<int32_t>(c->bits.i); + if (d == 0 || d == static_cast<int32_t>(0x80000000)) + return false; // Skip d=0 (leave to hardware) and d=INT_MIN (|d| overflows i32) + + bool is_rem = (i->op == Orem); + Ref dst = i->to; + Ref x = i->arg[0]; + + // First hit: move unprocessed tail to end of insb (same as simpl.cc) + if (!*rebuilt) { + ctx->curi = ctx->insb + NIns; + ulong tail = static_cast<ulong>((b->ins + b->nins) - (i + 1)); + ctx->curi -= tail; + icpy(ctx->curi, i + 1, tail); + *rebuilt = true; + } + + // d = +-1 degenerate: rem -> 0; div(1)=x; div(-1)=-x. + if (d == 1 || d == -1) { + if (is_rem) + emit(ctx, Ocopy, Kw, dst, getcon(ctx, 0, fn), Ref::none()); + else if (d == 1) + emit(ctx, Ocopy, Kw, dst, x, Ref::none()); + else + emit(ctx, Oneg, Kw, dst, x, Ref::none()); + return true; + } + + // For rem: emit tail r = x - q*d first (reverse order), then div sequence. + Ref q_dst = is_rem ? newtmp(ctx, "divopt", Kw, fn) : dst; + if (is_rem) { + Ref qd = newtmp(ctx, "divopt", Kw, fn); + emit(ctx, Osub, Kw, dst, x, qd); + emit(ctx, Omul, Kw, qd, q_dst, getcon(ctx, static_cast<int64_t>(d), fn)); + } + + int32_t ad = (d < 0) ? -d : d; + if ((ad & (ad - 1)) == 0) { + // |d| is power of 2, k >= 1 + int k = __builtin_ctz(static_cast<uint32_t>(ad)); + if (d < 0) { + Ref q_pos = newtmp(ctx, "divopt", Kw, fn); + emit(ctx, Oneg, Kw, q_dst, q_pos, Ref::none()); + emit_pow2_div_into(ctx, fn, q_pos, x, k); + } else { + emit_pow2_div_into(ctx, fn, q_dst, x, k); + } + } else { + emit_magic_div_into(ctx, fn, q_dst, x, ad, d < 0); + } + return true; +} + +} // namespace + +void divopt(Ctx* ctx, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) { + bool rebuilt = false; + for (Ins* i = b->ins + b->nins; i != b->ins;) { + --i; + if (lower_div(ctx, fn, i, b, &rebuilt)) + continue; + if (rebuilt) + emiti(ctx, *i); + } + if (rebuilt) + idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi)); + } +} + +} // namespace rcc diff --git a/src/mid/fold.cc b/src/mid/fold.cc new file mode 100644 index 0000000..df88694 --- /dev/null +++ b/src/mid/fold.cc @@ -0,0 +1,423 @@ +/* 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 fold.cc +/// @brief Constant folding utilities -- called by GVN (dedupins / assoccon), no standalone driver. +/// +/// See docs/fold.md for architectural notes. + +#include "rcc/all.h" + +#include <cstdint> +#include <cstring> + +namespace rcc { + +namespace { + +inline bool iscon(const Con* c, int w, uint64_t k) { + if (c->type != Con::CBits) + return false; + int64_t v = w ? c->bits.i : static_cast<int32_t>(c->bits.i); + return static_cast<uint64_t>(v) == (w ? k : static_cast<uint32_t>(k)); +} + +union Bits64 { + int64_t s; + uint64_t u; + float fs; + double fd; +}; + +inline Bits64 read_bits(const Con* c) { + Bits64 b; + b.s = c->bits.i; + return b; +} + +int addr_combine(int op, const Con* cl, const Con* cr, int* typ_out, Sym* sym_out) { + int ctyp = Con::CBits; + Sym sym; + std::memset(&sym, 0, sizeof sym); + + bool al = (cl->type == Con::CAddr); + bool ar = (cr->type == Con::CAddr); + + if (op == Oadd) { + if (al && ar) + return 1; + if (al || ar) { + ctyp = Con::CAddr; + sym = al ? cl->sym : cr->sym; + } + } else if (op == Osub) { + if (al && ar) { + if (!symeq(cl->sym, cr->sym)) + return 1; + // Subtract same sym. + } else if (al) { + ctyp = Con::CAddr; + sym = cl->sym; + } else if (ar) { + return 1; // Number - address is invalid + } + } else if (op == Ocast) { + if (al) { + ctyp = Con::CAddr; + sym = cl->sym; + } + } else if (al || ar) { + return 1; + } + *typ_out = ctyp; + *sym_out = sym; + return 0; +} + +uint64_t foldcmp_int(int cmp, int w, Bits64 l, Bits64 r) { + if (!w) { + l.u = static_cast<uint64_t>(static_cast<int32_t>(l.u)); + r.u = static_cast<uint64_t>(static_cast<int32_t>(r.u)); + } + switch (cmp) { + case Cieq: + return l.u == r.u; + case Cine: + return l.u != r.u; + case Ciule: + return l.u <= r.u; + case Ciult: + return l.u < r.u; + case Ciuge: + return l.u >= r.u; + case Ciugt: + return l.u > r.u; + case Cisle: + return l.s <= r.s; + case Cislt: + return l.s < r.s; + case Cisge: + return l.s >= r.s; + case Cisgt: + return l.s > r.s; + default: + die("foldcmp_int: bad cmp %d", cmp); + } +} + +template <typename F> +uint64_t foldcmp_flt(int cmp, F l, F r) { + switch (cmp) { + case Cfeq: + return l == r; + case Cfne: + return l != r; + case Cfle: + return l <= r; + case Cflt: + return l < r; + case Cfge: + return l >= r; + case Cfgt: + return l > r; + case Cfo: + return (l < r) || (l >= r); + case Cfuo: + return !((l < r) || (l >= r)); + default: + die("foldcmp_flt: bad cmp %d", cmp); + } +} + +void foldflt(Con* res, int op, int w, const Con* cl, const Con* cr) { + if (cl->type != Con::CBits || cr->type != Con::CBits) + err(const_cast<char*>("invalid address operand for '%s'"), optab[op].name); + + res->type = Con::CBits; + res->sym = Sym{}; + std::memset(&res->bits, 0, sizeof res->bits); + res->flt = 0; + + if (w) { + double ld = cl->bits.d, rd = cr->bits.d, xd; + switch (op) { + case Oadd: + xd = ld + rd; + break; + case Osub: + xd = ld - rd; + break; + case Oneg: + xd = -ld; + break; + case Odiv: + xd = ld / rd; + break; + case Omul: + xd = ld * rd; + break; + case Oswtof: + xd = static_cast<double>(static_cast<int32_t>(cl->bits.i)); + break; + case Ouwtof: + xd = static_cast<double>(static_cast<uint32_t>(cl->bits.i)); + break; + case Osltof: + xd = static_cast<double>(static_cast<int64_t>(cl->bits.i)); + break; + case Oultof: + xd = static_cast<double>(static_cast<uint64_t>(cl->bits.i)); + break; + case Oexts: + xd = static_cast<double>(cl->bits.s); + break; + case Ocast: + xd = ld; + break; + default: + die("foldflt: bad op Kd %d", op); + } + res->bits.d = xd; + res->flt = 2; + } else { + float ls = cl->bits.s, rs = cr->bits.s, xs; + switch (op) { + case Oadd: + xs = ls + rs; + break; + case Osub: + xs = ls - rs; + break; + case Oneg: + xs = -ls; + break; + case Odiv: + xs = ls / rs; + break; + case Omul: + xs = ls * rs; + break; + case Oswtof: + xs = static_cast<float>(static_cast<int32_t>(cl->bits.i)); + break; + case Ouwtof: + xs = static_cast<float>(static_cast<uint32_t>(cl->bits.i)); + break; + case Osltof: + xs = static_cast<float>(static_cast<int64_t>(cl->bits.i)); + break; + case Oultof: + xs = static_cast<float>(static_cast<uint64_t>(cl->bits.i)); + break; + case Otruncd: + xs = static_cast<float>(cl->bits.d); + break; + case Ocast: + xs = ls; + break; + default: + die("foldflt: bad op Ks %d", op); + } + res->bits.s = xs; + res->flt = 1; + } +} + +} // namespace + +int foldint(Con* res, int op, int w, Con* cl, Con* cr) { + // Address combination check. + int ctyp; + Sym sym; + if (addr_combine(op, cl, cr, &ctyp, &sym)) + return 1; + + Bits64 l = read_bits(cl); + Bits64 r = read_bits(cr); + + // Div / rem. + if (op == Odiv || op == Orem || op == Oudiv || op == Ourem) { + if (iscon(cr, w, 0)) + return 1; + if (op == Odiv || op == Orem) { + uint64_t intmin = w ? static_cast<uint64_t>(INT64_MIN) + : static_cast<uint64_t>(static_cast<uint32_t>(INT32_MIN)); + if (iscon(cr, w, static_cast<uint64_t>(-1)) && iscon(cl, w, intmin)) + return 1; + } + } + + // Dispatch. + uint64_t x; + + if (op >= Ocmpw && op <= Ocmpw1) { + x = foldcmp_int(op - Ocmpw, /*w=*/0, l, r); + } else if (op >= Ocmpl && op <= Ocmpl1) { + x = foldcmp_int(op - Ocmpl, /*w=*/1, l, r); + } else if (op >= Ocmps && op <= Ocmps1) { + x = foldcmp_flt<float>(op - Ocmps, cl->bits.s, cr->bits.s); + } else if (op >= Ocmpd && op <= Ocmpd1) { + x = foldcmp_flt<double>(op - Ocmpd, cl->bits.d, cr->bits.d); + } else { + // Shift count mask. + const uint64_t shmask = static_cast<uint64_t>(31 | (w << 5)); + switch (op) { + case Oadd: + x = l.u + r.u; + break; + case Osub: + x = l.u - r.u; + break; + case Oneg: + x = -l.u; + break; + case Omul: + x = l.u * r.u; + break; + case Odiv: + x = w ? static_cast<uint64_t>(l.s / r.s) + : static_cast<uint64_t>(static_cast<int32_t>(l.s) / + static_cast<int32_t>(r.s)); + break; + case Orem: + x = w ? static_cast<uint64_t>(l.s % r.s) + : static_cast<uint64_t>(static_cast<int32_t>(l.s) % + static_cast<int32_t>(r.s)); + break; + case Oudiv: + x = w ? l.u / r.u : static_cast<uint32_t>(l.u) / static_cast<uint32_t>(r.u); + break; + case Ourem: + x = w ? l.u % r.u : static_cast<uint32_t>(l.u) % static_cast<uint32_t>(r.u); + break; + case Oand: + x = l.u & r.u; + break; + case Oor: + x = l.u | r.u; + break; + case Oxor: + x = l.u ^ r.u; + break; + // Signed right shift. + case Osar: + x = static_cast<uint64_t>((w ? l.s : static_cast<int32_t>(l.s)) >> (r.u & shmask)); + break; + case Oshr: + x = (w ? l.u : static_cast<uint32_t>(l.u)) >> (r.u & shmask); + break; + case Oshl: + x = l.u << (r.u & shmask); + break; + + case Oextsb: + x = static_cast<uint64_t>(static_cast<int8_t>(l.u)); + break; + case Oextub: + x = static_cast<uint8_t>(l.u); + break; + case Oextsh: + x = static_cast<uint64_t>(static_cast<int16_t>(l.u)); + break; + case Oextuh: + x = static_cast<uint16_t>(l.u); + break; + case Oextsw: + x = static_cast<uint64_t>(static_cast<int32_t>(l.u)); + break; + case Oextuw: + x = static_cast<uint32_t>(l.u); + break; + + case Ostosi: + x = w ? static_cast<uint64_t>(static_cast<int64_t>(cl->bits.s)) + : static_cast<uint64_t>(static_cast<int32_t>(cl->bits.s)); + break; + case Ostoui: + x = w ? static_cast<uint64_t>(cl->bits.s) + : static_cast<uint64_t>(static_cast<uint32_t>(cl->bits.s)); + break; + case Odtosi: + x = w ? static_cast<uint64_t>(static_cast<int64_t>(cl->bits.d)) + : static_cast<uint64_t>(static_cast<int32_t>(cl->bits.d)); + break; + case Odtoui: + x = w ? static_cast<uint64_t>(cl->bits.d) + : static_cast<uint64_t>(static_cast<uint32_t>(cl->bits.d)); + break; + case Ocast: + x = l.u; + break; + default: + die("foldint: unsupported op %d", op); + } + } + + // Write back. + res->type = static_cast<decltype(res->type)>(ctyp); + res->sym = sym; + std::memset(&res->bits, 0, sizeof res->bits); + res->bits.i = static_cast<int64_t>(x); + res->flt = 0; + return 0; +} + +namespace { + +Ref opfold(Ctx* ctx, int op, int cls, Con* cl, Con* cr, Fn* fn) { + Con c; + std::memset(&c, 0, sizeof c); + + if (cls == Kw || cls == Kl) { + if (foldint(&c, op, cls == Kl, cl, cr)) + return Ref::none(); + } else { + foldflt(&c, op, cls == Kd, cl, cr); + } + + if (!KWIDE(cls)) + c.bits.i &= 0xffffffff; + + Ref r = newcon(ctx, &c, fn); + assert((cls != Ks && cls != Kd) || c.flt); + return r; +} + +} // namespace + +Ref foldref(Ctx* ctx, Fn* fn, Ins* i) { + if (rtype(i->to) != RTmp) + return Ref::none(); + if (!optab[i->op].canfold) + return Ref::none(); + if (rtype(i->arg[0]) != RCon) + return Ref::none(); + + Ref rr = i->arg[1]; + if (req(rr, Ref::none())) + rr = Ref::con_z(); + if (rtype(rr) != RCon) + return Ref::none(); + + Con* cl = &fn->con[i->arg[0].val]; + Con* cr = &fn->con[rr.val]; + return opfold(ctx, i->op, i->cls, cl, cr, fn); +} + +} // namespace rcc diff --git a/src/mid/gcm.cc b/src/mid/gcm.cc new file mode 100644 index 0000000..cb3c1df --- /dev/null +++ b/src/mid/gcm.cc @@ -0,0 +1,373 @@ +/* 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 gcm.cc +/// @brief Global Code Motion (GCM, Click's algorithm) -- rearranges instructions into the +/// "shallowest loop" legal block on the dominator tree. +/// +/// See docs/gcm.md for architectural notes. + +#include "rcc/all.h" + +namespace rcc { + +namespace { + +constexpr uint NOBID = ~0u; + +bool isdivwl(Ins* i) { + return INRANGE(i->op, Odiv, Ourem) && KBASE(i->cls) == 0; +} + +bool canelim(Ins* i) { + return isload(i->op) || isalloc(i->op) || isdivwl(i); +} + +Ins nopins() { + Ins i = {}; + i.op = Onop; + return i; +} + +bool cheap(Ins* i) { + if (KBASE(i->cls) != 0) + return false; + switch (i->op) { + case Oneg: + case Oadd: + case Osub: + case Omul: + case Oand: + case Oor: + case Oxor: + case Osar: + case Oshr: + case Oshl: + return true; + default: { + int x; + return iscmp(i->op, &x, &x) != 0; + } + } +} + +// schedearly + +uint earlyins(Fn*, Blk*, Ins*); + +uint schedearly(Fn* fn, Ref r) { + if (rtype(r) != RTmp) + return 0; + + Tmp* t = &fn->tmp[r.val]; + if (t->gcmbid != NOBID) + return t->gcmbid; + + if (!t->def) + return t->gcmbid = t->bid; // phi doesn't move, pinned to def block + + Blk* b = fn->rpo[t->bid]; + assert(b->ins <= t->def && t->def < &b->ins[b->nins]); + t->gcmbid = 0; // Visiting sentinel (recursive backedge gets 0 = start) + return t->gcmbid = earlyins(fn, b, t->def); +} + +uint earlyins(Fn* fn, Blk* b, Ins* i) { + uint ba = schedearly(fn, i->arg[0]); + uint bb = schedearly(fn, i->arg[1]); + assert(ba != NOBID && bb != NOBID); + + // Take the deeper operand block in dominator tree (shallower dominates deeper) + if (fn->rpo[ba]->depth < fn->rpo[bb]->depth) { + assert(dom(fn->rpo[ba], fn->rpo[bb])); + ba = bb; + } + return pinned(i) ? b->id : ba; +} + +void earlyblk(Fn* fn, uint bid) { + Blk* b = fn->rpo[bid]; + + for (Phi* p = b->phi; p; p = p->link) + for (uint n = 0; n < p->narg; n++) + schedearly(fn, p->arg[n]); + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (pinned(i)) { + schedearly(fn, i->arg[0]); + schedearly(fn, i->arg[1]); + } + schedearly(fn, b->jmp.arg); +} + +// schedlate + bestbid + +uint lcabid(Fn* fn, uint b1, uint b2) { + if (b1 == NOBID) + return b2; + if (b2 == NOBID) + return b1; + Blk* b = lca(fn->rpo[b1], fn->rpo[b2]); + assert(b); + return b->id; +} + +uint bestbid(Fn* fn, uint earlybid, uint latebid) { + if (latebid == NOBID) + return NOBID; // Unused + assert(earlybid != NOBID); + + Blk* earlyb = fn->rpo[earlybid]; + Blk* bestb = fn->rpo[latebid]; + assert(dom(earlyb, bestb)); + + for (Blk* curb = bestb; curb != earlyb;) { + curb = curb->idom; + if (curb->loop < bestb->loop) + bestb = curb; + } + return bestb->id; +} + +uint lateins(Fn*, Blk*, Ins*, Ref); +uint latephi(Fn*, Phi*, Ref); +uint latejmp(Blk*, Ref); + +uint schedlate(Fn* fn, Ref r) { + if (rtype(r) != RTmp) + return NOBID; + + Tmp* t = &fn->tmp[r.val]; + if (t->visit) + return t->gcmbid; + t->visit = 1; + + uint earlybid = t->gcmbid; + if (earlybid == NOBID) + return NOBID; // Unused + + t->gcmbid = t->bid; // Reuse gcmbid field to temporarily store late + uint latebid = NOBID; + for (Use* u = t->use; u < &t->use[t->nuse]; u++) { + assert(u->bid < fn->nblk); + Blk* b = fn->rpo[u->bid]; + uint usebid; + switch (u->type) { + case Use::UPhi: + usebid = latephi(fn, u->u.phi, r); + break; + case Use::UIns: + usebid = lateins(fn, b, u->u.ins, r); + break; + case Use::UJmp: + usebid = latejmp(b, r); + break; + default: + die("unreachable"); + } + latebid = lcabid(fn, latebid, usebid); + } + // latebid might still be NOBID: temp is only indirectly used by "deletable pinned instructions" + + if (t->def && !pinned(t->def)) + t->gcmbid = bestbid(fn, earlybid, latebid); + // Otherwise keep early + + return t->gcmbid; +} + +uint lateins(Fn* fn, Blk* b, Ins* i, Ref r) { + assert(b->ins <= i && i < &b->ins[b->nins]); + assert(req(i->arg[0], r) || req(i->arg[1], r)); + + uint latebid = schedlate(fn, i->to); + if (pinned(i)) { + if (latebid == NOBID && canelim(i)) + return NOBID; + return b->id; + } + return latebid; +} + +uint latephi(Fn* fn, Phi* p, Ref r) { + if (!p->narg) + return NOBID; // Marked unused + + uint latebid = NOBID; + for (uint n = 0; n < p->narg; n++) + if (req(p->arg[n], r)) + latebid = lcabid(fn, latebid, p->blk[n]->id); + + assert(latebid != NOBID); + return latebid; +} + +uint latejmp(Blk* b, Ref r) { + if (req(b->jmp.arg, Ref::none())) + return NOBID; + assert(req(b->jmp.arg, r)); + return b->id; +} + +void lateblk(Fn* fn, uint bid) { + Blk* b = fn->rpo[bid]; + + for (Phi** pp = &b->phi; *pp;) { + if (schedlate(fn, (*pp)->to) == NOBID) { + (*pp)->narg = 0; // Mark unused + *pp = (*pp)->link; // Remove this phi + } else { + pp = &(*pp)->link; + } + } + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (pinned(i)) + schedlate(fn, i->to); +} + +// gcmmove + +void addgcmins(Ctx* ctx, Fn* fn, Ins* vins, uint nins) { + for (Ins* i = vins; i < &vins[nins]; i++) { + assert(rtype(i->to) == RTmp); + Tmp* t = &fn->tmp[i->to.val]; + Blk* b = fn->rpo[t->gcmbid]; + addins(ctx, &b->ins, &b->nins, i); + } +} + +void gcmmove(Ctx* ctx, Fn* fn) { + uint nins = 0; + Ins* vins = static_cast<Ins*>(vnew(ctx, nins, sizeof vins[0], PFn)); + + for (Tmp* t = fn->tmp; t < &fn->tmp[fn->ntmp]; t++) { + if (!t->def || t->bid == t->gcmbid) + continue; + Ins* i = t->def; + if (pinned(i) && !canelim(i)) + continue; + assert(rtype(i->to) == RTmp && t == &fn->tmp[i->to.val]); + if (t->gcmbid != NOBID) + addins(ctx, &vins, &nins, i); + *i = nopins(); + } + addgcmins(ctx, fn, vins, nins); +} + +// schedblk (intra-block DFS ordering) + +Ins* schedins(Ctx* ctx, Fn* fn, Blk* b, Ins* i, Ins** pvins, uint* pnins) { + Ins *i0, *i1; + igroup(b, i, &i0, &i1); + + // First recursively order the defs in this block that are depended on by operands in this group + for (i = i0; i < i1; i++) + for (uint n = 0; n < 2; n++) { + if (rtype(i->arg[n]) != RTmp) + continue; + Tmp* t = &fn->tmp[i->arg[n].val]; + if (t->bid != b->id || !t->def) + continue; + schedins(ctx, fn, b, t->def, pvins, pnins); + } + // Then emit this group, wipe original to nop (prevent double emission) + for (i = i0; i < i1; i++) { + addins(ctx, pvins, pnins, i); + *i = nopins(); + } + return i1; +} + +void schedblk(Ctx* ctx, Fn* fn) { + Ins* vins = static_cast<Ins*>(vnew(ctx, 0, sizeof vins[0], PHeap)); + + for (Blk* b = fn->start; b; b = b->link) { + uint nins = 0; + for (Ins* i = b->ins; i < &b->ins[b->nins];) + i = schedins(ctx, fn, b, i, &vins, &nins); + idup(ctx, b, vins, nins); + } + vfree(vins); +} + +// sink (trivial op sinking) + +void sinkref(Ctx* ctx, Fn* fn, Blk* b, Ref* pr) { + if (rtype(*pr) != RTmp) + return; + Tmp* t = &fn->tmp[pr->val]; + if (!t->def || t->bid == b->id || pinned(t->def) || !cheap(t->def)) + return; + + // Copy t->def and sink to b + Ins i = *t->def; + Ref r = newtmp(ctx, "snk", t->cls, fn); + t = nullptr; // newtmp might realloc fn->tmp, old pointer invalid + *pr = r; + i.to = r; + fn->tmp[r.val].gcmbid = b->id; + emiti(ctx, i); + sinkref(ctx, fn, b, &i.arg[0]); + sinkref(ctx, fn, b, &i.arg[1]); +} + +void sink(Ctx* ctx, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (isload(i->op)) + sinkref(ctx, fn, b, &i->arg[0]); + else if (isstore(i->op)) + sinkref(ctx, fn, b, &i->arg[1]); + sinkref(ctx, fn, b, &b->jmp.arg); + } + addgcmins(ctx, fn, ctx->curi, static_cast<uint>(&ctx->insb[NIns] - ctx->curi)); +} + +} // namespace + +int pinned(Ins* i) { + return optab[i->op].pinned || isdivwl(i); +} + +void gcm(Ctx* ctx, Fn* fn) { + filldepth(fn); + fillloop(fn); + + for (Tmp* t = fn->tmp; t < &fn->tmp[fn->ntmp]; t++) { + t->visit = 0; + t->gcmbid = NOBID; + } + for (uint bid = 0; bid < fn->nblk; bid++) + earlyblk(fn, bid); + for (uint bid = 0; bid < fn->nblk; bid++) + lateblk(fn, bid); + + gcmmove(ctx, fn); + filluse(ctx, fn); + ctx->curi = &ctx->insb[NIns]; + sink(ctx, fn); + filluse(ctx, fn); + schedblk(ctx, fn); + + if (debug['G']) { + fprintf(ctx->dbg_buf, "\n> After GCM:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +} // namespace rcc diff --git a/src/mid/gvn.cc b/src/mid/gvn.cc new file mode 100644 index 0000000..8e029da --- /dev/null +++ b/src/mid/gvn.cc @@ -0,0 +1,414 @@ +/* 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 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<int>(a.val) - static_cast<int>(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<int>(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<int>(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<int32_t>(c.bits.i) < 0))) { + int fail = negcon(i1->cls, &c); + assert(fail == 0); + (void)fail; + op = Osub; + } + + i1->op = static_cast<uint>(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<Blk**>(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<Ins**>(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 diff --git a/src/mid/ifopt.cc b/src/mid/ifopt.cc new file mode 100644 index 0000000..c82fb4c --- /dev/null +++ b/src/mid/ifopt.cc @@ -0,0 +1,127 @@ +/* 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 ifopt.cc +/// @brief If-conversion: collapses if-then[-else] diamond CFG into straight-line block + sel +/// instructions. +/// +/// See docs/ifopt.md for architectural notes. + +#include "rcc/all.h" + +namespace rcc { + +namespace { + +enum { + MaxIns = 2, + MaxPhis = 2, +}; + +bool okbranch(Blk* b) { + int n = 0; + for (Ins* i = b->ins; i < &b->ins[b->nins]; ++i) { + if (i->op == Odbgloc) + continue; + if (pinned(i)) + return false; + if (i->op != Onop) + ++n; + } + return n <= MaxIns; +} + +bool okjoin(Blk* b) { + int n = 0; + for (Phi* p = b->phi; p; p = p->link) { + if (KBASE(p->cls) != 0) + return false; + ++n; + } + return n <= MaxPhis; +} + +bool okgraph(Blk* ifb, Blk* thenb, Blk* elseb, Blk* joinb) { + assert(thenb != elseb); + return joinb->npred == 2 && okjoin(joinb) && (thenb == ifb || okbranch(thenb)) && + (elseb == ifb || okbranch(elseb)); +} + +void convert(Ctx* ctx, Blk* ifb, Blk* thenb, Blk* elseb, Blk* joinb) { + Ins* ins = static_cast<Ins*>(vnew(ctx, 0, sizeof ins[0], PHeap)); + uint nins = 0; + + addbins(ctx, &ins, &nins, ifb); + if (thenb != ifb) + addbins(ctx, &ins, &nins, thenb); + if (elseb != ifb) + addbins(ctx, &ins, &nins, elseb); + + assert(joinb->npred == 2); + Ins sel = {}; + if (joinb->phi) { + sel.op = Osel0; + sel.cls = static_cast<uint>(Kw); + sel.arg[0] = ifb->jmp.arg; + addins(ctx, &ins, &nins, &sel); + } + + sel = {}; + sel.op = Osel1; + for (Phi* p = joinb->phi; p; p = p->link) { + sel.to = p->to; + sel.cls = static_cast<uint>(p->cls); + sel.arg[0] = phiarg(p, thenb); + sel.arg[1] = phiarg(p, elseb); + addins(ctx, &ins, &nins, &sel); + } + + idup(ctx, ifb, ins, nins); + ifb->jmp.type = Jjmp; + ifb->jmp.arg = Ref::none(); + ifb->s1 = joinb; + ifb->s2 = nullptr; + joinb->npred = 1; + joinb->pred[0] = ifb; + joinb->phi = nullptr; + vfree(ins); +} + +} // namespace + +void ifconvert(Ctx* ctx, Fn* fn) { + if (debug['K']) + fputs("\n> If-conversion:\n", ctx->dbg_buf); + + Blk *thenb, *elseb, *joinb; + for (Blk* ifb = fn->start; ifb; ifb = ifb->link) + if (ifgraph(ifb, &thenb, &elseb, &joinb) && okgraph(ifb, thenb, elseb, joinb)) { + if (debug['K']) + fprintf(ctx->dbg_buf, " @%s -> @%s, @%s -> @%s\n", ifb->name, thenb->name, + elseb->name, joinb->name); + convert(ctx, ifb, thenb, elseb, joinb); + } + + if (debug['K']) { + fprintf(ctx->dbg_buf, "\n> After if-conversion:\n"); + printfn(fn, ctx->dbg_buf); + } +} + +} // namespace rcc diff --git a/src/mid/inline.cc b/src/mid/inline.cc new file mode 100644 index 0000000..60e2dc1 --- /dev/null +++ b/src/mid/inline.cc @@ -0,0 +1,484 @@ +/* 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 inline.cc +/// @brief Function inlining (with SCC check to prevent recursion + fixpoint iteration). +/// +/// See docs/inline.md for architectural notes. + +#include "rcc/all.h" + +#include <algorithm> +#include <cstring> +#include <functional> +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +constexpr int kInlineLeafThreshold = 64; +constexpr int kInlineSingleCallerThreshold = 256; +constexpr int kInlineNonLeafThreshold = 48; +constexpr int kFixpointMaxIters = 16; +constexpr int kConstArgDiscount = 8; + +// Metrics + +bool is_leaf(Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (i->op == Ocall) + return false; + return true; +} + +int count_real_ins(Fn* fn) { + int n = 0; + for (Blk* b = fn->start; b; b = b->link) + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (i->op != Onop) + n++; + // phi also counts towards cost, one phi equals one ins + for (Blk* b = fn->start; b; b = b->link) + for (Phi* p = b->phi; p; p = p->link) + n++; + return n; +} + +Fn* lookup_callee(Ref fn_ref, Fn* caller, const std::vector<Fn*>& fns) { + if (rtype(fn_ref) != RCon) + return nullptr; + Con* c = &caller->con[fn_ref.val]; + if (c->type != Con::CAddr) + return nullptr; + const char* name = str(c->sym.id); + for (Fn* f : fns) + if (std::strcmp(f->name, name) == 0) + return f; + return nullptr; +} + +std::unordered_map<std::string, int> collect_call_counts(const std::vector<Fn*>& fns) { + std::unordered_map<std::string, int> cnt; + for (Fn* caller : fns) { + for (Blk* b = caller->start; b; b = b->link) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (i->op != Ocall) + continue; + if (rtype(i->arg[0]) != RCon) + continue; + Con* c = &caller->con[i->arg[0].val]; + if (c->type != Con::CAddr) + continue; + cnt[str(c->sym.id)]++; + } + } + } + return cnt; +} + +// Call Graph SCC + +std::unordered_map<Fn*, std::unordered_set<Fn*>> build_call_graph(const std::vector<Fn*>& fns) { + std::unordered_map<Fn*, std::unordered_set<Fn*>> g; + for (Fn* caller : fns) { + g[caller]; // Ensure node exists (even without outgoing edges) + for (Blk* b = caller->start; b; b = b->link) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (i->op != Ocall) + continue; + Fn* callee = lookup_callee(i->arg[0], caller, fns); + if (callee) + g[caller].insert(callee); + } + } + } + return g; +} + +std::unordered_set<Fn*> find_cycle_members( + const std::vector<Fn*>& fns, const std::unordered_map<Fn*, std::unordered_set<Fn*>>& g) { + std::unordered_set<Fn*> cyclic; + std::unordered_map<Fn*, int> idx_of, lowlink; + std::unordered_set<Fn*> on_stack; + std::vector<Fn*> stk; + int counter = 0; + + std::function<void(Fn*)> strongconnect = [&](Fn* v) { + idx_of[v] = counter; + lowlink[v] = counter; + counter++; + stk.push_back(v); + on_stack.insert(v); + + auto it = g.find(v); + if (it != g.end()) { + for (Fn* w : it->second) { + if (idx_of.find(w) == idx_of.end()) { + strongconnect(w); + lowlink[v] = std::min(lowlink[v], lowlink[w]); + } else if (on_stack.count(w)) { + lowlink[v] = std::min(lowlink[v], idx_of[w]); + } + } + } + + if (lowlink[v] == idx_of[v]) { + std::vector<Fn*> scc; + Fn* w; + do { + w = stk.back(); + stk.pop_back(); + on_stack.erase(w); + scc.push_back(w); + } while (w != v); + + if (scc.size() >= 2) { + for (Fn* f : scc) + cyclic.insert(f); + } else { + // Single-node SCC: only count if it has self-loop (direct recursion) + Fn* f = scc[0]; + auto it2 = g.find(f); + if (it2 != g.end() && it2->second.count(f)) + cyclic.insert(f); + } + } + }; + + for (Fn* f : fns) { + if (idx_of.find(f) == idx_of.end()) + strongconnect(f); + } + return cyclic; +} + +// Splice + +struct RemapTables { + std::vector<Ref> tmp; // callee tmp id → caller Ref (RTmp) + std::vector<Ref> con; // callee con id → caller Ref (RCon) + std::vector<Blk*> blk; // callee blk id → cloned Blk* in caller +}; + +Ref remap_ref(Ref r, const RemapTables& m) { + switch (rtype(r)) { + case RTmp: + if (r.val == 0) + return r; // Ref::none() + assert(r.val < m.tmp.size()); + return m.tmp[r.val]; + case RCon: + assert(r.val < m.con.size()); + return m.con[r.val]; + default: + return r; // RInt/RType/RSlot/RMem/RCall do not need remap + } +} + +struct RetEntry { + Blk* blk; // cloned ret block in caller + Ref val; // ret value (remapped), Ref::none() if void +}; + +void clone_blk_body(Ctx* ctx, Fn* caller, Blk* src, Blk* dst, const RemapTables& m, + const std::vector<Ref>& par_args, Blk* b_post, std::vector<RetEntry>& rets) { + // phi + Phi** ptail = &dst->phi; + *ptail = nullptr; + for (Phi* sp = src->phi; sp; sp = sp->link) { + Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + np->to = remap_ref(sp->to, m); + np->cls = sp->cls; + np->visit = 0; + np->narg = sp->narg; + np->arg = static_cast<Ref*>(vnew(ctx, sp->narg, sizeof(Ref), PFn)); + np->blk = static_cast<Blk**>(vnew(ctx, sp->narg, sizeof(Blk*), PFn)); + for (uint k = 0; k < sp->narg; k++) { + np->arg[k] = remap_ref(sp->arg[k], m); + np->blk[k] = m.blk[sp->blk[k]->id]; + } + np->link = nullptr; + *ptail = np; + ptail = &np->link; + } + + // ins: Opar -> Ocopy from par_args[k] + dst->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn)); + dst->nins = 0; + int par_idx = 0; + for (Ins* si = src->ins; si < &src->ins[src->nins]; si++) { + Ins ni; + if (ispar(si->op)) { + // Replace all uses of par tmp with arg ref in caller. Produces an Ocopy to maintain SSA + // def, later copy/gvn will eliminate. + assert(par_idx < static_cast<int>(par_args.size())); + ni.op = Ocopy; + ni.cls = si->cls; + ni.to = remap_ref(si->to, m); + ni.arg[0] = par_args[static_cast<size_t>(par_idx++)]; + ni.arg[1] = Ref::none(); + } else { + ni.op = si->op; + ni.cls = si->cls; + ni.to = remap_ref(si->to, m); + ni.arg[0] = remap_ref(si->arg[0], m); + ni.arg[1] = remap_ref(si->arg[1], m); + } + addins(ctx, &dst->ins, &dst->nins, &ni); + } + + // jmp: change isret to Jjmp pointing to b_post; pass through other jmps (s1/s2 updated by + // m.blk) + if (isret(src->jmp.type)) { + rets.push_back({dst, src->jmp.type == Jret0 ? Ref::none() : remap_ref(src->jmp.arg, m)}); + dst->jmp.type = Jjmp; + dst->jmp.arg = Ref::none(); + dst->s1 = b_post; + dst->s2 = nullptr; + } else { + dst->jmp.type = src->jmp.type; + dst->jmp.arg = remap_ref(src->jmp.arg, m); + dst->s1 = src->s1 ? m.blk[src->s1->id] : nullptr; + dst->s2 = src->s2 ? m.blk[src->s2->id] : nullptr; + } + // pred/rpo/dom etc. left for fillcfg to rebuild + (void)caller; +} + +void splice_one(Ctx* ctx, Fn* caller, Blk* b, int call_idx, Fn* callee) { + Ins* call_ins = &b->ins[call_idx]; + Ref call_to = call_ins->to; + int call_cls = static_cast<int>(call_ins->cls); + (void)call_cls; + + // Collect arg_refs: scan backwards until non-isarg + int arg_start = call_idx; + while (arg_start > 0 && isarg(b->ins[arg_start - 1].op)) + arg_start--; + std::vector<Ref> arg_refs; + arg_refs.reserve(static_cast<size_t>(call_idx - arg_start)); + for (int k = arg_start; k < call_idx; k++) + arg_refs.push_back(b->ins[k].arg[0]); + + // Three remap tables + RemapTables m; + m.tmp.assign(static_cast<size_t>(callee->ntmp), Ref::none()); + m.con.assign(static_cast<size_t>(callee->ncon), Ref::none()); + m.blk.assign(static_cast<size_t>(callee->nblk), nullptr); + + // Tmp: 0=none, [1..R] skipped (reg slots), use newtmp. User tmp starts at 32. + constexpr int kFirstUserTmp = + 0; // rcc IRGen does not use reg-tmp reservations, starts at 0; tmp[0]=R + for (int t = 1; t < callee->ntmp; t++) { + const char* prfx = callee->tmp[t].name ? callee->tmp[t].name : "il"; + m.tmp[static_cast<size_t>(t)] = newtmp(ctx, prfx, callee->tmp[t].cls, caller); + } + (void)kFirstUserTmp; + // Con: 0=undef (fixed), 1=const_zero (rcc convention), others deep-copy + m.con[0] = Ref::con(0); + for (int c = 1; c < callee->ncon; c++) + m.con[static_cast<size_t>(c)] = newcon(ctx, &callee->con[c], caller); + // Blk: all newblk. inline_module is called before run_midend, callee->rpo not built yet, + // traverse start->link chain; blk->id from IRGen used as remap key. + static int splice_seq = 0; + std::vector<Blk*> callee_blks; // Align with traversal order for phase 4 + callee_blks.reserve(callee->nblk); + for (Blk* cb = callee->start; cb; cb = cb->link) { + Blk* nb = newblk(ctx); + char buf[256]; + std::snprintf(buf, sizeof(buf), "i%d.%s", splice_seq, cb->name ? cb->name : "blk"); + nb->name = strf(ctx, PFn, "%s", buf); + nb->id = caller->nblk++; + assert(cb->id < callee->nblk); + m.blk[cb->id] = nb; + callee_blks.push_back(cb); + } + ++splice_seq; + + // Cut b: new b_post, takes over b->ins[call_idx+1..] / b->jmp / s1/s2 / link + Blk* b_post = newblk(ctx); + { + static int post_seq = 0; + char buf[64]; + std::snprintf(buf, sizeof(buf), "iret.%d", post_seq++); + b_post->name = strf(ctx, PFn, "%s", buf); + b_post->id = caller->nblk++; + } + b_post->phi = nullptr; + // Take over tail ins + int tail_n = static_cast<int>(b->nins) - call_idx - 1; + if (tail_n > 0) { + for (int k = call_idx + 1; k < static_cast<int>(b->nins); k++) + addins(ctx, &b_post->ins, &b_post->nins, &b->ins[k]); + } + b_post->jmp = b->jmp; + b_post->s1 = b->s1; + b_post->s2 = b->s2; + Blk* original_b_link = b->link; + // Truncate b to b_pre (up to arg_start), jmp = Jjmp to callee.start_copy + b->nins = static_cast<uint>(arg_start); + b->jmp.type = Jjmp; + b->jmp.arg = Ref::none(); + b->s1 = m.blk[callee->start->id]; + b->s2 = nullptr; + + // Deep copy each callee blk + std::vector<RetEntry> rets; + for (Blk* src : callee_blks) { + Blk* dst = m.blk[src->id]; + clone_blk_body(ctx, caller, src, dst, m, arg_refs, b_post, rets); + } + + // If not void, insert phi at head of b_post to merge ret value + if (!req(call_to, Ref::none()) && !rets.empty()) { + Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + np->to = call_to; + np->cls = static_cast<short>(call_ins->cls); + np->visit = 0; + np->narg = static_cast<uint>(rets.size()); + np->arg = static_cast<Ref*>(vnew(ctx, np->narg, sizeof(Ref), PFn)); + np->blk = static_cast<Blk**>(vnew(ctx, np->narg, sizeof(Blk*), PFn)); + for (size_t k = 0; k < rets.size(); k++) { + np->arg[k] = rets[k].val; + np->blk[k] = rets[k].blk; + } + np->link = b_post->phi; + b_post->phi = np; + } + + // Fix external phi refs: phi inputs in original b's successors from b -> b_post + auto fix_phis_in = [&](Blk* succ) { + if (!succ) + return; + for (Phi* p = succ->phi; p; p = p->link) + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == b) + p->blk[k] = b_post; + }; + fix_phis_in(b_post->s1); + fix_phis_in(b_post->s2); + + // Link chain: b -> callee blks -> b_post -> original_b_link + Blk* last_callee_blk = nullptr; + b->link = m.blk[callee->start->id]; // start_copy immediately follows b + for (Blk* src : callee_blks) { + Blk* cur = m.blk[src->id]; + if (last_callee_blk) + last_callee_blk->link = cur; + last_callee_blk = cur; + } + assert(last_callee_blk); + last_callee_blk->link = b_post; + b_post->link = original_b_link; +} + +// Pass driver + +bool inline_pass(Ctx* ctx, std::vector<Fn*>& fns, const std::unordered_set<Fn*>& in_cycle) { + auto call_counts = collect_call_counts(fns); + bool any_change = false; + + for (Fn* caller : fns) { + for (Blk* b = caller->start; b; b = b->link) { + // Splice modifies b->ins and b->link. After a splice, b becomes b_pre, and tail ins go + // to b_post. We only scan each b once, breaking on hit (remaining calls handled in next + // fixpoint round, ensuring b->ins pointer stability). + bool spliced_in_b = false; + for (int k = 0; k < static_cast<int>(b->nins); k++) { + if (b->ins[k].op != Ocall) + continue; + Fn* callee = lookup_callee(b->ins[k].arg[0], caller, fns); + if (!callee) + continue; // extern (sylib) + if (callee == caller) + continue; // Direct self-call + if (callee->vararg) + continue; + if (in_cycle.count(callee)) + continue; // Direct/indirect recursion: cannot be callee + int cost = count_real_ins(callee); + // const-arg discount: scan backward [arg_start..k-1] for Oarg with RCon + int arg_start = k; + while (arg_start > 0 && isarg(b->ins[arg_start - 1].op)) + arg_start--; + int const_args = 0; + for (int j = arg_start; j < k; j++) + if (rtype(b->ins[j].arg[0]) == RCon) + const_args++; + cost = std::max(0, cost - const_args * kConstArgDiscount); + bool single = false; + auto it = call_counts.find(callee->name); + if (it != call_counts.end() && it->second == 1) + single = true; + int threshold; + if (single) + threshold = kInlineSingleCallerThreshold; + else if (is_leaf(callee)) + threshold = kInlineLeafThreshold; + else + threshold = kInlineNonLeafThreshold; + if (cost > threshold) + continue; + + splice_one(ctx, caller, b, k, callee); + any_change = true; + spliced_in_b = true; + if (debug['I']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> inline %s into %s (cost=%d, threshold=%d)\n", callee->name, + caller->name, cost, threshold); + } + break; + } + if (spliced_in_b) { + // b is now b_pre, followed by callee blks and b_post. Continue safely handles this. + continue; + } + } + } + return any_change; +} + +} // namespace + +void inline_module(Ctx* ctx, std::vector<Fn*>& fns) { + auto graph = build_call_graph(fns); + auto in_cycle = find_cycle_members(fns, graph); + if (debug['I']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> inline_module: call-graph %zu fn, %zu in cycle\n", fns.size(), + in_cycle.size()); + } + int iter = 0; + for (; iter < kFixpointMaxIters; iter++) { + if (!inline_pass(ctx, fns, in_cycle)) + break; + } + if (debug['I']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> inline_module: %d round%s\n", iter, iter == 1 ? "" : "s"); + } +} + +} // namespace rcc diff --git a/src/mid/licm.cc b/src/mid/licm.cc new file mode 100644 index 0000000..87d61a3 --- /dev/null +++ b/src/mid/licm.cc @@ -0,0 +1,213 @@ +/* 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 licm.cc +/// @brief Phase A Loop Invariant Code Motion (LICM). +/// +/// See docs/licm.md for architectural notes. + +#include "rcc/all.h" + +#include <vector> + +namespace rcc { + +namespace { + +struct LoopInfo { + Blk* header; + std::vector<Blk*> body; // includes header + Blk* preheader; // nullptr if no unique non-back-edge predecessor +}; + +Ins make_nop() { + Ins i = {}; + i.op = Onop; + return i; +} + +// Loop Identification. +void build_loops(Fn* fn, std::vector<LoopInfo>& loops, std::vector<int>& blk_loop) { + loops.clear(); + blk_loop.assign(static_cast<size_t>(fn->nblk), -1); + + for (Blk* b = fn->start; b; b = b->link) + b->visit = ~0u; + + auto refine_inner = [&](uint bid, int lidx) { + int cur = blk_loop[bid]; + if (cur < 0 || loops[static_cast<size_t>(cur)].header->id < + loops[static_cast<size_t>(lidx)].header->id) + blk_loop[bid] = lidx; + }; + + for (uint i = 0; i < fn->nblk; i++) { + Blk* hd = fn->rpo[i]; + bool has_back = false; + for (uint k = 0; k < hd->npred; k++) { + if (hd->pred[k]->id >= i) { + has_back = true; + break; + } + } + if (!has_back) + continue; + + int lidx = static_cast<int>(loops.size()); + loops.push_back({hd, {hd}, nullptr}); + refine_inner(hd->id, lidx); + + // DFS body collect. + std::vector<Blk*> stack; + for (uint k = 0; k < hd->npred; k++) + if (hd->pred[k]->id >= i) + stack.push_back(hd->pred[k]); + + while (!stack.empty()) { + Blk* n = stack.back(); + stack.pop_back(); + if (n->id < hd->id || n->visit == hd->id) + continue; + n->visit = hd->id; + loops[static_cast<size_t>(lidx)].body.push_back(n); + refine_inner(n->id, lidx); + for (uint k = 0; k < n->npred; k++) + stack.push_back(n->pred[k]); + } + } + + // Find preheader. + for (auto& L : loops) { + Blk* phdr = nullptr; + bool unique = true; + for (uint k = 0; k < L.header->npred; k++) { + Blk* p = L.header->pred[k]; + bool in_body = false; + for (Blk* b : L.body) + if (b == p) { + in_body = true; + break; + } + if (in_body) + continue; // back-edge + if (phdr) { + unique = false; + break; + } + phdr = p; + } + L.preheader = unique ? phdr : nullptr; + } +} + +// Invariance +bool blk_in(Blk* x, const std::vector<Blk*>& body) { + for (Blk* b : body) + if (b == x) + return true; + return false; +} + +bool is_invariant(Fn* fn, Ref r, const LoopInfo& L) { + if (rtype(r) == RCon) + return true; + if (rtype(r) != RTmp) + return false; + Tmp* t = &fn->tmp[r.val]; + if (t->bid == ~0u) + return true; // Undefined (treat as param/global-like) - equivalent to "outside loop" + return !blk_in(fn->rpo[t->bid], L.body); +} + +// Check safety. +bool clobbered_in_loop(Fn* fn, Ins* load_ins, Ref addr, int sz, const LoopInfo& L) { + for (Blk* b : L.body) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (i == load_ins) + continue; + if (i->op == Ocall) + return true; // Conservative: call is treated as writing all escapable memory + if (isstore(i->op)) { + int ssz = storesz(i); + int delta; + if (alias(addr, 0, sz, i->arg[1], ssz, &delta, fn) != NoAlias) + return true; + } + } + } + return false; +} + +} // namespace + +void licm(Ctx* ctx, Fn* fn) { + fillloop(fn); + + // Phase A hoisted load. + int total_hoisted = 0; + int iter = 0; + for (; iter < 16; iter++) { + std::vector<LoopInfo> loops; + std::vector<int> blk_loop; + build_loops(fn, loops, blk_loop); + if (loops.empty()) + break; + + int round_hoisted = 0; + for (Blk* b = fn->start; b; b = b->link) { + if (b->id >= blk_loop.size()) + continue; + int lidx = blk_loop[b->id]; + if (lidx < 0) + continue; + LoopInfo& L = loops[static_cast<size_t>(lidx)]; + if (!L.preheader) + continue; + + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (!isload(i->op)) + continue; + if (!is_invariant(fn, i->arg[0], L)) + continue; + int sz = loadsz(i); + if (clobbered_in_loop(fn, i, i->arg[0], sz, L)) + continue; + + // Hit: move to preheader. + addins(ctx, &L.preheader->ins, &L.preheader->nins, i); + *i = make_nop(); + round_hoisted++; + } + } + + total_hoisted += round_hoisted; + if (round_hoisted == 0) + break; + // Reset loop state. + filluse(ctx, fn); + } + + if (debug['L']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "\n> LICM hoisted %d load(s) in %s (in %d round%s)\n", total_hoisted, fn->name, + iter, iter == 1 ? "" : "s"); + } +} + +} // namespace rcc diff --git a/src/mid/live.cc b/src/mid/live.cc new file mode 100644 index 0000000..ee718ad --- /dev/null +++ b/src/mid/live.cc @@ -0,0 +1,198 @@ +/* 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 live.cc +/// @brief Backward dataflow liveness analysis. +/// +/// See docs/live.md for architectural notes. + +#include "rcc/all.h" + +namespace rcc { + +void liveon(BSet* v, Blk* b, Blk* s) { + bscopy(v, s->in); + + // s's phi-defs. + for (Phi* p = s->phi; p; p = p->link) + if (rtype(p->to) == RTmp) + bsclr(v, p->to.val); + + // phi-arg edge check. + for (Phi* p = s->phi; p; p = p->link) { + for (uint n = 0; n < p->narg; n++) { + if (p->blk[n] != b) + continue; + if (rtype(p->arg[n]) != RTmp) + continue; + uint t = p->arg[n].val; + bsset(v, t); + bsset(b->gen, t); + } + } +} + +namespace { + +/// Add ref (if tmp) to b->gen and b->in; on first entry to b->in, increment live count for its K +/// bucket. +inline void use_ref(Ref r, Blk* b, int nlv[2], Tmp* tmp) { + if (rtype(r) != RTmp) + return; + uint t = r.val; + bsset(b->gen, t); + if (!bshas(b->in, t)) { + nlv[KBASE(tmp[t].cls)]++; + bsset(b->in, t); + } +} + +/// Get maximum of two elements. +inline int imax(int a, int b) { + return a > b ? a : b; +} + +} // namespace + +void filllive(Ctx* ctx, Fn* fn) { + BSet prev_out[1]; + BSet edge_set[1]; + + bsinit(ctx, prev_out, static_cast<uint>(fn->ntmp)); + bsinit(ctx, edge_set, static_cast<uint>(fn->ntmp)); + + // Initialize sets. + for (Blk* b = fn->start; b; b = b->link) { + bsinit(ctx, b->in, static_cast<uint>(fn->ntmp)); + bsinit(ctx, b->out, static_cast<uint>(fn->ntmp)); + bsinit(ctx, b->gen, static_cast<uint>(fn->ntmp)); + } + + // Fixpoint loop. + bool changed = true; + while (changed) { + changed = false; + + // Reverse rpo scan. + for (int n = static_cast<int>(fn->nblk) - 1; n >= 0; n--) { + Blk* b = fn->rpo[n]; + + // Merge successors liveon. + bscopy(prev_out, b->out); + + if (b->s1) { + liveon(edge_set, b, b->s1); + bsunion(b->out, edge_set); + } + if (b->s2) { + liveon(edge_set, b, b->s2); + bsunion(b->out, edge_set); + } + if (!bsequal(b->out, prev_out)) + changed = true; + + // Initial in. + int nlv[2] = {0, 0}; + b->out->t[0] |= T.rglob; + bscopy(b->in, b->out); + for (int t = 0; bsiter(b->in, &t); t++) + nlv[KBASE(fn->tmp[t].cls)]++; + + // Terminal jump args. + if (rtype(b->jmp.arg) == RCall) { + assert(static_cast<int>(bscount(b->in)) == T.nrglob && b->in->t[0] == T.rglob); + b->in->t[0] |= T.retregs(b->jmp.arg, nlv); + } else { + use_ref(b->jmp.arg, b, nlv, fn->tmp); + } + // Fused cmp+jnz check. + use_ref(b->jmp.arg2, b, nlv, fn->tmp); + + // Initial nlive. + for (int k = 0; k < 2; k++) + b->nlive[k] = nlv[k]; + + // Reverse scan instructions. + for (int idx = static_cast<int>(b->nins) - 1; idx >= 0; idx--) { + Ins* i = &b->ins[idx]; + + // Post-ABI lowering call. + if (i->op == Ocall && rtype(i->arg[1]) == RCall) { + int call_kill[2]; + b->in->t[0] &= ~T.retregs(i->arg[1], call_kill); + for (int k = 0; k < 2; k++) { + nlv[k] -= call_kill[k]; + // Midpoint call. + nlv[k] += T.nrsave[k]; + b->nlive[k] = imax(b->nlive[k], nlv[k]); + } + int call_arg[2]; + b->in->t[0] |= T.argregs(i->arg[1], call_arg); + for (int k = 0; k < 2; k++) { + nlv[k] -= T.nrsave[k]; + nlv[k] += call_arg[k]; + } + } + + // def kill. + if (!req(i->to, Ref::none())) { + assert(rtype(i->to) == RTmp); + uint t = i->to.val; + if (bshas(b->in, t)) + nlv[KBASE(fn->tmp[t].cls)]--; + bsset(b->gen, t); + bsclr(b->in, t); + } + + // args gen. + for (int k = 0; k < 2; k++) { + Ref a = i->arg[k]; + if (rtype(a) == RMem) { + Mem* m = &fn->mem[a.val]; + use_ref(m->base, b, nlv, fn->tmp); + use_ref(m->index, b, nlv, fn->tmp); + } else { + use_ref(a, b, nlv, fn->tmp); + } + } + + // Update peak. + for (int k = 0; k < 2; k++) + b->nlive[k] = imax(b->nlive[k], nlv[k]); + } + } + } + + // Debug dump. + if (debug['L']) { + FILE* f = ctx->dbg_buf; + fprintf(f, "\n> Liveness analysis:\n"); + for (Blk* b = fn->start; b; b = b->link) { + fprintf(f, "\t%-10sin: ", b->name); + dumpts(b->in, fn->tmp, f); + fprintf(f, "\t out: "); + dumpts(b->out, fn->tmp, f); + fprintf(f, "\t gen: "); + dumpts(b->gen, fn->tmp, f); + fprintf(f, "\t live: %d %d\n", b->nlive[0], b->nlive[1]); + } + } +} + +} // namespace rcc diff --git a/src/mid/load.cc b/src/mid/load.cc new file mode 100644 index 0000000..a26f847 --- /dev/null +++ b/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/src/mid/loop_idiom.cc b/src/mid/loop_idiom.cc new file mode 100644 index 0000000..8cbbf00 --- /dev/null +++ b/src/mid/loop_idiom.cc @@ -0,0 +1,401 @@ +/* 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 loop_idiom.cc +/// @brief Recognize zero-fill / array-copy inner loops -> rewrite to runtime Ocall. +/// +/// See docs/loop_idiom.md for architectural notes. + +#include "rcc/all.h" +#include "rcc/loopinfo.h" + +#include <algorithm> +#include <cstring> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +// store op -> byte width; non-store returns 0 +int store_bytes(int op) { + switch (op) { + case Ostoreb: + return 1; + case Ostoreh: + return 2; + case Ostorew: + case Ostores: + return 4; + case Ostorel: + case Ostored: + return 8; + default: + return 0; + } +} + +int load_bytes(int op, int cls) { + switch (op) { + case Oloadsb: + case Oloadub: + return 1; + case Oloadsh: + case Oloaduh: + return 2; + case Oloadsw: + case Oloaduw: + return 4; + case Oload: + return (cls == Kl || cls == Kd) ? 8 : 4; + default: + return 0; + } +} + +// Check byte splat. +bool is_byte_splat(Fn* fn, Ref v, uint8_t* byte_out) { + if (rtype(v) != RCon) + return false; + Con* c = &fn->con[v.val]; + if (c->type != Con::CBits || c->flt) + return false; + uint64_t bits = static_cast<uint64_t>(c->bits.i); + uint8_t b = bits & 0xff; + uint64_t splat = static_cast<uint64_t>(b); + splat |= splat << 8; + splat |= splat << 16; + splat |= splat << 32; + if (bits != splat) + return false; + *byte_out = b; + return true; +} + +// Trace Ocopy chain back to a non-Ocopy root Ref. +Ref base_root(Fn* fn, Ref r) { + for (int hop = 0; hop < 16; hop++) { + Ins* d = def_ins(fn, r); + if (!d || d->op != Ocopy) + break; + r = d->arg[0]; + } + return r; +} + +// Find IV increment index. +int find_iv_inc_idx(Blk* latch, Ref next) { + for (uint k = 0; k < latch->nins; k++) + if (req(latch->ins[k].to, next)) + return static_cast<int>(k); + return -1; +} + +enum class Idiom { None, Memset, Memcpy, Memmove }; + +struct Match { + Idiom kind; + Ref base_dst; + Ref base_src; // memcpy/memmove only + uint8_t value; // memset only + Ref end_dst; // for size = end - base + Blk* after; +}; + +bool try_match_loop(Fn* fn, const Loop& L, Match* out) { + // Validate loop structure. + if (L.body.size() > 2) + return false; + // Disallow nesting. + + // Primary pointer IV (dst pointer for memset/memcpy) + PointerIV iv_dst{}; + Phi* phi_dst = nullptr; + int found = 0; + for (Phi* ph = L.header->phi; ph; ph = ph->link) { + PointerIV cand{}; + if (pick_pointer_iv(fn, L, ph, &cand)) { + iv_dst = cand; + phi_dst = ph; + found++; + } + } + if (found == 0) + return false; + // 1 IV -> memset candidate; 2 -> memcpy candidate; >2 -> reject + PointerIV iv_src{}; + Phi* phi_src = nullptr; + if (found >= 2) { + if (found != 2) + return false; + // Rescan to get the two + std::vector<std::pair<PointerIV, Phi*>> ivs; + for (Phi* ph = L.header->phi; ph; ph = ph->link) { + PointerIV cand{}; + if (pick_pointer_iv(fn, L, ph, &cand)) + ivs.push_back({cand, ph}); + } + if (ivs.size() != 2) + return false; + // Will decide which is dst when scanning store in body + iv_dst = ivs[0].first; + phi_dst = ivs[0].second; + iv_src = ivs[1].first; + phi_src = ivs[1].second; + } + + // exit cmp must be based on iv_dst (memset) or either one (memcpy) + Ref end_dst; + Blk* after = nullptr; + if (!check_exit_cmp(fn, L, iv_dst.phi_to, &end_dst, &after)) { + if (phi_src && check_exit_cmp(fn, L, iv_src.phi_to, &end_dst, &after)) { + // Reversed: cmp based on src, swap dst/src naming + std::swap(iv_dst, iv_src); + std::swap(phi_dst, phi_src); + } else { + return false; + } + } + if (iv_dst.step != iv_src.step && phi_src) + return false; + + // Find IV self-update ins (add in latch) + int iv_dst_inc = find_iv_inc_idx(L.latch, iv_dst.next); + int iv_src_inc = phi_src ? find_iv_inc_idx(L.latch, iv_src.next) : -1; + if (iv_dst_inc < 0) + return false; + if (phi_src && iv_src_inc < 0) + return false; + + // Body scan. + Ins* load_ins = nullptr; + Ins* store_ins = nullptr; + for (Blk* bb : L.body) { + for (uint k = 0; k < bb->nins; k++) { + Ins* i = &bb->ins[k]; + int op = i->op; + // Allowed: exit cmp (in header), IV self-update (in latch) + if (bb == L.header && req(i->to, L.header->jmp.arg)) + continue; // exit cmp + if (bb == L.latch && + (static_cast<int>(k) == iv_dst_inc || static_cast<int>(k) == iv_src_inc)) + continue; + // Disallowed: call / alloc / FP / div / vastart / vaarg / dbgloc / sel / blit + if (op == Ocall || op == Oalloc4 || op == Oalloc8 || op == Oalloc16 || op == Ovaarg || + op == Ovastart || op == Odbgloc || op == Osel0 || op == Osel1 || op == Oblit0 || + op == Oblit1 || op == Osalloc) + return false; + if (op == Oudiv || op == Ourem || op == Odiv || op == Orem) + return false; + if (op == Ocopy && (i->cls == Ks || i->cls == Kd)) + return false; + // load / store + int lb = load_bytes(op, static_cast<int>(i->cls)); + int sb = store_bytes(op); + if (lb > 0) { + if (load_ins) + return false; // Reject multiple loads + load_ins = i; + continue; + } + if (sb > 0) { + if (store_ins) + return false; + store_ins = i; + continue; + } + // Other allowed. + if (op == Oadd || op == Osub || op == Omul || op == Oand || op == Oor || op == Oxor || + op == Oshl || op == Oshr || op == Osar || op == Oextsw || op == Oextuw || + op == Oextsh || op == Oextuh || op == Oextsb || op == Oextub || op == Ocopy || + op == Onop) + continue; + // Other cmp (besides LFTR cmp): conservatively reject + return false; + } + } + + if (!store_ins) + return false; + int sw = store_bytes(store_ins->op); + if (sw != iv_dst.step) + return false; + if (!req(store_ins->arg[1], iv_dst.phi_to)) + return false; + + if (has_outside_use(fn, L)) + return false; + + if (load_ins) { + // memcpy path + if (!phi_src) + return false; + int lw = load_bytes(load_ins->op, static_cast<int>(load_ins->cls)); + if (lw != sw) + return false; + if (!req(load_ins->arg[0], iv_src.phi_to)) + return false; + if (!req(store_ins->arg[0], load_ins->to)) + return false; + // Noalias check. + Ref root_dst = base_root(fn, iv_dst.base); + Ref root_src = base_root(fn, iv_src.base); + out->kind = req(root_dst, root_src) ? Idiom::Memmove : Idiom::Memcpy; + out->base_dst = iv_dst.base; + out->base_src = iv_src.base; + out->value = 0; + out->end_dst = end_dst; + out->after = after; + return true; + } + + // memset path + if (phi_src) + return false; // Second IV present but no load, abnormal shape + uint8_t bv = 0; + if (!is_byte_splat(fn, store_ins->arg[0], &bv)) + return false; + out->kind = Idiom::Memset; + out->base_dst = iv_dst.base; + out->base_src = Ref::none(); + out->value = bv; + out->end_dst = end_dst; + out->after = after; + return true; +} + +const char* runtime_name(Idiom k) { + switch (k) { + case Idiom::Memset: + return "__rcc_memset"; + case Idiom::Memcpy: + return "__rcc_memcpy"; + case Idiom::Memmove: + return "__rcc_memmove"; + default: + return ""; + } +} + +Ref make_runtime_callee(Ctx* ctx, Fn* fn, const char* name) { + Con c; + std::memset(&c, 0, sizeof(c)); + c.type = Con::CAddr; + c.sym.type = Sym::SGlo; + c.sym.id = intern(const_cast<char*>(name)); + return newcon(ctx, &c, fn); +} + +void execute_rewrite(Ctx* ctx, Fn* fn, const Loop& L, const Match& m) { + Blk* pre = L.preheader; + + // size = end - base (Osub Kl) + Ref size_ref = newtmp(ctx, "midiom", Kl, fn); + Ins sub_ins{}; + sub_ins.op = Osub; + sub_ins.cls = Kl; + sub_ins.to = size_ref; + sub_ins.arg[0] = m.end_dst; + sub_ins.arg[1] = m.base_dst; + addins(ctx, &pre->ins, &pre->nins, &sub_ins); + + // Oarg dst (Kl) + Ins arg0{}; + arg0.op = Oarg; + arg0.cls = Kl; + arg0.to = Ref::none(); + arg0.arg[0] = m.base_dst; + arg0.arg[1] = Ref::none(); + addins(ctx, &pre->ins, &pre->nins, &arg0); + + // Oarg val/src + Ins arg1{}; + arg1.op = Oarg; + arg1.to = Ref::none(); + arg1.arg[1] = Ref::none(); + if (m.kind == Idiom::Memset) { + arg1.cls = Kw; + arg1.arg[0] = getcon(ctx, static_cast<int64_t>(m.value), fn); + } else { + arg1.cls = Kl; + arg1.arg[0] = m.base_src; + } + addins(ctx, &pre->ins, &pre->nins, &arg1); + + // Oarg size (Kl) + Ins arg2{}; + arg2.op = Oarg; + arg2.cls = Kl; + arg2.to = Ref::none(); + arg2.arg[0] = size_ref; + arg2.arg[1] = Ref::none(); + addins(ctx, &pre->ins, &pre->nins, &arg2); + + // Ocall (returns void -> cls=Kw, to=none) + Ins call_ins{}; + call_ins.op = Ocall; + call_ins.cls = Kw; + call_ins.to = Ref::none(); + call_ins.arg[0] = make_runtime_callee(ctx, fn, runtime_name(m.kind)); + call_ins.arg[1] = Ref::none(); + addins(ctx, &pre->ins, &pre->nins, &call_ins); + + fn->leaf = 0; + + // change preheader to jump directly to after + pre->jmp.type = Jjmp; + pre->jmp.arg = Ref::none(); + pre->jmp.arg2 = Ref::none(); + pre->s1 = m.after; + pre->s2 = nullptr; +} + +} // namespace + +void loop_idiom(Ctx* ctx, Fn* fn) { + std::vector<Loop> loops; + find_loops(fn, loops); + if (loops.empty()) + return; + + // Sort innermost first. + std::sort(loops.begin(), loops.end(), + [](const Loop& a, const Loop& b) { return a.body.size() < b.body.size(); }); + + std::unordered_set<uint> consumed_blk; + for (const Loop& L : loops) { + bool clash = false; + for (Blk* bb : L.body) + if (consumed_blk.count(bb->id)) { + clash = true; + break; + } + if (clash) + continue; + Match m{}; + if (!try_match_loop(fn, L, &m)) + continue; + execute_rewrite(ctx, fn, L, m); + for (Blk* bb : L.body) + consumed_blk.insert(bb->id); + } +} + +} // namespace rcc diff --git a/src/mid/loopinfo.cc b/src/mid/loopinfo.cc new file mode 100644 index 0000000..d6041fc --- /dev/null +++ b/src/mid/loopinfo.cc @@ -0,0 +1,226 @@ +/* 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 loopinfo.cc +/// @brief find_loops + detect_bivs + ref_is_const (shared). + +#include "rcc/loopinfo.h" + +#include "rcc/all.h" + +namespace rcc { + +bool ref_is_const(Fn* fn, Ref r, int64_t& out) { + if (rtype(r) != RCon) + return false; + if (r.val >= static_cast<uint>(fn->ncon)) + return false; + Con* c = &fn->con[r.val]; + if (c->type != Con::CBits || c->flt) + return false; + out = c->bits.i; + return true; +} + +Ins* def_ins(Fn* fn, Ref r) { + if (rtype(r) != RTmp || r.val >= static_cast<uint>(fn->ntmp)) + return nullptr; + return fn->tmp[r.val].def; +} + +void find_loops(Fn* fn, std::vector<Loop>& out) { + out.clear(); + for (Blk* b = fn->start; b; b = b->link) + b->visit = ~0u; + + for (uint i = 0; i < fn->nblk; i++) { + Blk* hd = fn->rpo[i]; + if (hd->npred != 2) + continue; + + Blk* pre = nullptr; + Blk* latch = nullptr; + for (uint k = 0; k < hd->npred; k++) { + Blk* p = hd->pred[k]; + if (p->id != ~0u && p->id >= hd->id) + latch = p; + else + pre = p; + } + if (!pre || !latch) + continue; + + Loop L; + L.header = hd; + L.preheader = pre; + L.latch = latch; + L.body.push_back(hd); + L.body_bid.insert(hd->id); + hd->visit = hd->id; + std::vector<Blk*> stk = {latch}; + while (!stk.empty()) { + Blk* n = stk.back(); + stk.pop_back(); + if (n->visit == hd->id) + continue; + n->visit = hd->id; + L.body.push_back(n); + L.body_bid.insert(n->id); + for (uint k = 0; k < n->npred; k++) + stk.push_back(n->pred[k]); + } + out.push_back(std::move(L)); + } +} + +void detect_bivs(Fn* fn, const Loop& L, std::vector<Biv>& out) { + for (Phi* p = L.header->phi; p; p = p->link) { + if (p->narg != 2) + continue; + Ref init_ref = Ref::none(), next_ref = Ref::none(); + for (uint k = 0; k < p->narg; k++) { + if (p->blk[k] == L.preheader) + init_ref = p->arg[k]; + else if (p->blk[k] == L.latch) + next_ref = p->arg[k]; + } + int64_t init_v = 0; + if (!ref_is_const(fn, init_ref, init_v)) + continue; + Ins* nx = def_ins(fn, next_ref); + if (!nx || nx->op != Oadd) + continue; + int64_t step_v = 0; + if (req(nx->arg[0], p->to) && ref_is_const(fn, nx->arg[1], step_v)) { + } else if (req(nx->arg[1], p->to) && ref_is_const(fn, nx->arg[0], step_v)) { + } else { + continue; + } + if (step_v == 0) + continue; + out.push_back({p->to, static_cast<int>(p->cls), init_v, step_v}); + } +} + +bool pick_pointer_iv(Fn* fn, const Loop& L, Phi* p, PointerIV* out) { + if (p->cls != Kl) + return false; + if (p->narg != 2) + return false; + Ref base = Ref::none(), next = Ref::none(); + for (uint k = 0; k < p->narg; k++) { + if (p->blk[k] == L.preheader) + base = p->arg[k]; + else if (p->blk[k] == L.latch) + next = p->arg[k]; + else + return false; + } + if (rtype(base) == -1 || rtype(next) == -1) + return false; + Ins* nx = def_ins(fn, next); + if (!nx || nx->op != Oadd) + return false; + int64_t step = 0; + if (req(nx->arg[0], p->to) && ref_is_const(fn, nx->arg[1], step)) { + } else if (req(nx->arg[1], p->to) && ref_is_const(fn, nx->arg[0], step)) { + } else { + return false; + } + if (step <= 0) + return false; + out->phi_to = p->to; + out->base = base; + out->next = next; + out->step = step; + return true; +} + +bool check_exit_cmp(Fn* fn, const Loop& L, Ref p, Ref* end_out, Blk** after_out) { + Blk* hd = L.header; + if (hd->jmp.type != Jjnz) + return false; + Ref cmp_r = hd->jmp.arg; + if (rtype(cmp_r) != RTmp) + return false; + Ins* cmp_def = nullptr; + for (uint k = 0; k < hd->nins; k++) { + Ins* ii = &hd->ins[k]; + if (req(ii->to, cmp_r)) { + cmp_def = ii; + break; + } + } + if (!cmp_def) + return false; + if (cmp_def->op != Ocultl && cmp_def->op != Ocsltl) + return false; + if (!req(cmp_def->arg[0], p)) + return false; + Ref end = cmp_def->arg[1]; + if (rtype(end) == RTmp) { + Ins* d = def_ins(fn, end); + if (d) { + for (Blk* bb : L.body) + if (bb->ins <= d && d < bb->ins + bb->nins) + return false; + } + } + Blk* s1 = hd->s1; + Blk* s2 = hd->s2; + bool s1_in = L.body_bid.count(s1->id) != 0; + bool s2_in = L.body_bid.count(s2->id) != 0; + if (s1_in == s2_in) + return false; + *end_out = end; + *after_out = s1_in ? s2 : s1; + return true; +} + +bool has_outside_use(Fn* fn, const Loop& L) { + std::unordered_set<uint> loop_def; + for (Blk* bb : L.body) { + for (Phi* ph = bb->phi; ph; ph = ph->link) + if (rtype(ph->to) == RTmp) + loop_def.insert(ph->to.val); + for (uint k = 0; k < bb->nins; k++) + if (rtype(bb->ins[k].to) == RTmp) + loop_def.insert(bb->ins[k].to.val); + } + for (Blk* b = fn->start; b; b = b->link) { + if (L.body_bid.count(b->id)) + continue; + for (Phi* ph = b->phi; ph; ph = ph->link) + for (uint k = 0; k < ph->narg; k++) + if (rtype(ph->arg[k]) == RTmp && loop_def.count(ph->arg[k].val)) + return true; + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + for (int a = 0; a < 2; a++) + if (rtype(i->arg[a]) == RTmp && loop_def.count(i->arg[a].val)) + return true; + } + if (rtype(b->jmp.arg) == RTmp && loop_def.count(b->jmp.arg.val)) + return true; + if (rtype(b->jmp.arg2) == RTmp && loop_def.count(b->jmp.arg2.val)) + return true; + } + return false; +} + +} // namespace rcc diff --git a/src/mid/loopswap.cc b/src/mid/loopswap.cc new file mode 100644 index 0000000..cd8acf0 --- /dev/null +++ b/src/mid/loopswap.cc @@ -0,0 +1,469 @@ +/* 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 loopswap.cc +/// @brief Integer perfect 2-level nested loop interchange. +/// +/// See docs/loopswap.md for architectural notes. + +#include "rcc/all.h" +#include "rcc/loopinfo.h" + +#include <cstdlib> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +// Helper: reaches +bool reaches(Fn* fn, Ref r, Ref target, std::unordered_set<uint>& visiting, int budget) { + if (req(r, target)) + return true; + if (budget <= 0) + return false; + if (rtype(r) != RTmp || r.val >= static_cast<uint>(fn->ntmp)) + return false; + if (!visiting.insert(r.val).second) + return true; + + Ins* def = fn->tmp[r.val].def; + if (def && def->op == Ocopy) { + bool ok = reaches(fn, def->arg[0], target, visiting, budget - 1); + visiting.erase(r.val); + return ok; + } + for (Blk* bb = fn->start; bb; bb = bb->link) { + for (Phi* p = bb->phi; p; p = p->link) { + if (!req(p->to, r)) + continue; + bool ok = true; + for (uint k = 0; k < p->narg; k++) { + if (req(p->arg[k], r)) + continue; + if (!reaches(fn, p->arg[k], target, visiting, budget - 1)) { + ok = false; + break; + } + } + visiting.erase(r.val); + return ok; + } + } + visiting.erase(r.val); + return false; +} + +bool carries(Fn* fn, Ref r, Ref target) { + std::unordered_set<uint> v; + return reaches(fn, r, target, v, 64); +} + +// Affine decomposition +struct Aff { + int64_t outer_c; + int64_t inner_c; +}; + +Aff affine(Fn* fn, Ref r, Ref outer_to, Ref inner_to, int depth) { + if (depth > 24) + return {0, 0}; + if (rtype(r) == RCon) + return {0, 0}; + if (rtype(r) != RTmp) + return {0, 0}; + if (carries(fn, r, outer_to)) + return {1, 0}; + if (carries(fn, r, inner_to)) + return {0, 1}; + if (r.val >= static_cast<uint>(fn->ntmp)) + return {0, 0}; + Ins* def = fn->tmp[r.val].def; + if (!def) + return {0, 0}; + switch (def->op) { + case Oadd: { + Aff a = affine(fn, def->arg[0], outer_to, inner_to, depth + 1); + Aff b = affine(fn, def->arg[1], outer_to, inner_to, depth + 1); + return {a.outer_c + b.outer_c, a.inner_c + b.inner_c}; + } + case Osub: { + Aff a = affine(fn, def->arg[0], outer_to, inner_to, depth + 1); + Aff b = affine(fn, def->arg[1], outer_to, inner_to, depth + 1); + return {a.outer_c - b.outer_c, a.inner_c - b.inner_c}; + } + case Omul: { + int64_t c; + if (ref_is_const(fn, def->arg[1], c)) { + Aff a = affine(fn, def->arg[0], outer_to, inner_to, depth + 1); + return {a.outer_c * c, a.inner_c * c}; + } + if (ref_is_const(fn, def->arg[0], c)) { + Aff a = affine(fn, def->arg[1], outer_to, inner_to, depth + 1); + return {a.outer_c * c, a.inner_c * c}; + } + return {0, 0}; + } + case Oextsw: + case Ocopy: + return affine(fn, def->arg[0], outer_to, inner_to, depth + 1); + default: + return {0, 0}; + } +} + +// Perfect nest: L1.body \ L2.body \ {L1.header, L1.latch} must only contain empty Jjmp blocks. +bool perfect_nest(const Loop& L1, const Loop& L2) { + for (Blk* bb : L1.body) { + if (bb == L1.header || bb == L1.latch) + continue; + if (L2.body_bid.count(bb->id)) + continue; + if (bb->nins != 0) + return false; + if (bb->jmp.type != Jjmp) + return false; + } + return true; +} + +// Does the cmp ins for header.jmp.arg reference bv.to (or its carrier equiv)? +int find_header_cmp(Fn* fn, const Loop& L, const Biv& bv) { + Ref a = L.header->jmp.arg; + if (rtype(a) != RTmp) + return -1; + for (uint k = 0; k < L.header->nins; k++) { + Ins* ii = &L.header->ins[k]; + if (!req(ii->to, a)) + continue; + if (carries(fn, ii->arg[0], bv.to) || carries(fn, ii->arg[1], bv.to)) + return static_cast<int>(k); + return -1; + } + return -1; +} + +// Find index of BIV's latch increment ins (the Oadd) in latch->ins array. +int find_latch_inc(const Loop& L, const Biv& bv) { + Phi* P = nullptr; + for (Phi* p = L.header->phi; p; p = p->link) + if (req(p->to, bv.to)) { + P = p; + break; + } + if (!P) + return -1; + Ref next = Ref::none(); + for (uint k = 0; k < P->narg; k++) + if (P->blk[k] == L.latch) { + next = P->arg[k]; + break; + } + if (rtype(next) != RTmp) + return -1; + for (uint k = 0; k < L.latch->nins; k++) + if (req(L.latch->ins[k].to, next) && L.latch->ins[k].op == Oadd) + return static_cast<int>(k); + return -1; +} + +Phi* find_phi(Blk* b, Ref to) { + for (Phi* p = b->phi; p; p = p->link) + if (req(p->to, to)) + return p; + return nullptr; +} + +void unlink_phi(Blk* b, Phi* t) { + for (Phi** cur = &b->phi; *cur; cur = &(*cur)->link) + if (*cur == t) { + *cur = t->link; + t->link = nullptr; + return; + } +} + +void prepend_phi(Blk* b, Phi* p) { + p->link = b->phi; + b->phi = p; +} + +void rewrite_phi_blk(Phi* p, Blk* from, Blk* to) { + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == from) + p->blk[k] = to; +} + +void rewrite_uses(Fn* fn, Ref from, Ref to) { + for (Blk* b = fn->start; b; b = b->link) { + for (Phi* p = b->phi; p; p = p->link) + for (uint k = 0; k < p->narg; k++) + if (req(p->arg[k], from)) + p->arg[k] = to; + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (req(i->arg[0], from)) + i->arg[0] = to; + if (req(i->arg[1], from)) + i->arg[1] = to; + } + if (req(b->jmp.arg, from)) + b->jmp.arg = to; + if (req(b->jmp.arg2, from)) + b->jmp.arg2 = to; + } +} + +struct AccessRec { + Ref addr; + int64_t inner_s; + bool is_store; +}; + +bool try_swap(Ctx* ctx, Fn* fn, const Loop& L1, const Loop& L2) { + if (!perfect_nest(L1, L2)) + return false; + + std::vector<Biv> bs1, bs2; + detect_bivs(fn, L1, bs1); + detect_bivs(fn, L2, bs2); + if (bs1.empty() || bs2.empty()) + return false; + + // Pick the BIV referenced by header.cmp (SysY loops typically compare one BIV). + Biv b1{}, b2{}; + int cmp1 = -1, cmp2 = -1; + for (auto& bv : bs1) { + int idx = find_header_cmp(fn, L1, bv); + if (idx >= 0) { + b1 = bv; + cmp1 = idx; + break; + } + } + for (auto& bv : bs2) { + int idx = find_header_cmp(fn, L2, bv); + if (idx >= 0) { + b2 = bv; + cmp2 = idx; + break; + } + } + if (cmp1 < 0 || cmp2 < 0) + return false; + if ((b1.cls != Kw && b1.cls != Kl) || (b2.cls != Kw && b2.cls != Kl)) + return false; + + int inc1 = find_latch_inc(L1, b1); + int inc2 = find_latch_inc(L2, b2); + if (inc1 < 0 || inc2 < 0) + return false; + + // Scan L2.body: integer only + decompose addr. + int64_t sum_outer = 0, sum_inner = 0; + std::vector<AccessRec> mems; + for (Blk* bb : L2.body) { + for (uint k = 0; k < bb->nins; k++) { + Ins* ii = &bb->ins[k]; + int op = ii->op; + if (op == Ocall) + return false; + if (ii->cls == Ks || ii->cls == Kd) + return false; + bool is_ld = isload(op); + bool is_st = isstore(op); + if (!is_ld && !is_st) + continue; + if (op == Ostores || op == Ostored) + return false; + Ref addr = is_st ? ii->arg[1] : ii->arg[0]; + Aff a = affine(fn, addr, b1.to, b2.to, 0); + sum_outer += std::abs(a.outer_c); + sum_inner += std::abs(a.inner_c); + mems.push_back({addr, a.inner_c, is_st}); + } + } + + // accumulator pattern: if there are stores => every store has inner_s==0 and matches a load + // with same addr and inner_s==0. + bool has_store = false; + for (auto& m : mems) + if (m.is_store) { + has_store = true; + break; + } + if (has_store) { + for (auto& s : mems) { + if (!s.is_store) + continue; + if (s.inner_s != 0) + return false; + bool found = false; + for (auto& l : mems) { + if (!l.is_store && req(l.addr, s.addr) && l.inner_s == 0) { + found = true; + break; + } + } + if (!found) + return false; + } + } + + if (sum_inner == 0) + return false; + if (sum_outer * 4 >= sum_inner) + return false; + + // Execute + Blk* h1 = L1.header; + Blk* h2 = L2.header; + + Phi* P1 = find_phi(h1, b1.to); + Phi* P2 = find_phi(h2, b2.to); + if (!P1 || !P2) + return false; + + // Collect self-loop carrier of b1.to on L2.header (Braun artifact; invalid after swap, needs + // removal). + std::vector<Phi*> stale; + for (Phi* p = h2->phi; p; p = p->link) { + if (p->narg != 2) + continue; + Ref pre = Ref::none(); + bool is_self = false; + for (uint k = 0; k < p->narg; k++) { + if (p->blk[k] == L2.preheader) + pre = p->arg[k]; + else if (p->blk[k] == L2.latch && req(p->arg[k], p->to)) + is_self = true; + } + if (is_self && carries(fn, pre, b1.to)) + stale.push_back(p); + } + + // Snapshot: copy 4 target ins before swap to avoid distortion. + Ins C1 = h1->ins[cmp1]; + Ins C2 = h2->ins[cmp2]; + Ins I1 = L1.latch->ins[inc1]; + Ins I2 = L2.latch->ins[inc2]; + + // Swap phi: unlink + rewrite edge + prepend to the other. + unlink_phi(h1, P1); + unlink_phi(h2, P2); + rewrite_phi_blk(P1, L1.preheader, L2.preheader); + rewrite_phi_blk(P1, L1.latch, L2.latch); + rewrite_phi_blk(P2, L2.preheader, L1.preheader); + rewrite_phi_blk(P2, L2.latch, L1.latch); + prepend_phi(h1, P2); + prepend_phi(h2, P1); + + // Swap cmp ins + jmp descriptor. + h1->ins[cmp1].op = Onop; + h2->ins[cmp2].op = Onop; + addins(ctx, &h1->ins, &h1->nins, &C2); + addins(ctx, &h2->ins, &h2->nins, &C1); + std::swap(h1->jmp.arg, h2->jmp.arg); + std::swap(h1->jmp.cls, h2->jmp.cls); + std::swap(h1->jmp.arg2, h2->jmp.arg2); + + // Swap increment. + L1.latch->ins[inc1].op = Onop; + L2.latch->ins[inc2].op = Onop; + addins(ctx, &L1.latch->ins, &L1.latch->nins, &I2); + addins(ctx, &L2.latch->ins, &L2.latch->nins, &I1); + + // Invalidate carriers: rewrite_uses to substitute with b1.to, then unlink phi. + for (Phi* s : stale) { + rewrite_uses(fn, s->to, b1.to); + unlink_phi(h2, s); + } + + return true; +} + +// L2 is immediate-child of L1: strict subset of body, with no L3 strictly in between. +bool is_immediate_child(const std::vector<Loop>& loops, size_t i_par, size_t j_chl) { + const Loop& par = loops[i_par]; + const Loop& chl = loops[j_chl]; + if (chl.body_bid.size() >= par.body_bid.size()) + return false; + for (uint id : chl.body_bid) + if (!par.body_bid.count(id)) + return false; + for (size_t m = 0; m < loops.size(); m++) { + if (m == i_par || m == j_chl) + continue; + const Loop& mid = loops[m]; + if (mid.body_bid.size() <= chl.body_bid.size()) + continue; + if (mid.body_bid.size() >= par.body_bid.size()) + continue; + bool chl_in_mid = true; + for (uint id : chl.body_bid) + if (!mid.body_bid.count(id)) { + chl_in_mid = false; + break; + } + if (!chl_in_mid) + continue; + bool mid_in_par = true; + for (uint id : mid.body_bid) + if (!par.body_bid.count(id)) { + mid_in_par = false; + break; + } + if (mid_in_par) + return false; + } + return true; +} + +} // namespace + +void loopinterchange(Ctx* ctx, Fn* fn) { + std::vector<Loop> loops; + find_loops(fn, loops); + if (loops.size() < 2) + return; + + for (size_t i = 0; i < loops.size(); i++) { + Loop& L1 = loops[i]; + Loop* L2 = nullptr; + bool amb = false; + for (size_t j = 0; j < loops.size(); j++) { + if (i == j) + continue; + if (!is_immediate_child(loops, i, j)) + continue; + if (L2) { + amb = true; + break; + } + L2 = &loops[j]; + } + if (!L2 || amb) + continue; + if (try_swap(ctx, fn, L1, *L2)) + return; // One swap per pass, CFG/loops are invalidated + } +} + +} // namespace rcc diff --git a/src/mid/lsr.cc b/src/mid/lsr.cc new file mode 100644 index 0000000..0197eb9 --- /dev/null +++ b/src/mid/lsr.cc @@ -0,0 +1,1001 @@ +/* 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 lsr.cc +/// @brief Loop Strength Reduction (LSR). +/// +/// See docs/lsr.md for architectural notes. + +#include "rcc/all.h" +#include "rcc/loopinfo.h" + +#include <cstring> +#include <functional> +#include <optional> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +constexpr int kFixpointIters = 4; + +// IVs created by reduce_mul / insert_addr_iv. +struct CreatedIV { + Ref to; // header phi.to + Ref init_ref; // phi preheader incoming edge Ref + int64_t step; + int cls; + int src_biv; + int64_t c; +}; + +uint64_t ref_key(Ref r) { + return (static_cast<uint64_t>(static_cast<uint>(rtype(r))) << 32) | + static_cast<uint32_t>(r.val); +} + +int find_biv(const std::vector<Biv>& bivs, Ref r) { + for (size_t k = 0; k < bivs.size(); k++) + if (req(bivs[k].to, r)) + return static_cast<int>(k); + return -1; +} + +struct MulCand { + Blk* b; + uint idx; + int biv; + int64_t c; + bool is_kl; +}; + +// Returns number of hits. +int reduce_mul(Ctx* ctx, Fn* fn, const Loop& L, std::vector<Biv>& bivs, + std::vector<CreatedIV>& created_ivs) { + std::vector<MulCand> cands; + for (Blk* b : L.body) { + for (uint ix = 0; ix < b->nins; ix++) { + Ins* m = &b->ins[ix]; + if (m->op != Omul) + continue; + + int64_t c = 0; + Ref tref = Ref::none(); + if (ref_is_const(fn, m->arg[1], c) && rtype(m->arg[0]) == RTmp) + tref = m->arg[0]; + else if (ref_is_const(fn, m->arg[0], c) && rtype(m->arg[1]) == RTmp) + tref = m->arg[1]; + else + continue; + if (c == 0 || c == 1) + continue; + + int bi = -1; + bool is_kl = false; + + int d = find_biv(bivs, tref); + if (d >= 0 && static_cast<int>(m->cls) == bivs[static_cast<size_t>(d)].cls) { + bi = d; + is_kl = (m->cls == Kl); + } + if (bi < 0 && m->cls == Kl) { + Ins* ext = def_ins(fn, tref); + if (ext && ext->op == Oextsw) { + int e = find_biv(bivs, ext->arg[0]); + if (e >= 0 && bivs[static_cast<size_t>(e)].cls == Kw) { + bi = e; + is_kl = true; + } + } + } + + if (bi < 0) + continue; + cands.push_back({b, ix, bi, c, is_kl}); + } + } + if (cands.empty()) + return 0; + + struct Made { + int biv; + int64_t c; + bool is_kl; + Ref j; + int64_t init; + int64_t step; + }; + std::vector<Made> made; + + auto get_iv = [&](int bi, int64_t c, bool is_kl) -> Ref { + for (auto& md : made) + if (md.biv == bi && md.c == c && md.is_kl == is_kl) + return md.j; + + const Biv& iv = bivs[static_cast<size_t>(bi)]; + int cls = is_kl ? Kl : Kw; + int64_t j_init = iv.init * c; + int64_t j_step = iv.step * c; + Ref jt = newtmp(ctx, "lsr", cls, fn); + Ref jn = newtmp(ctx, "lsr", cls, fn); + + Ins add{}; + add.op = Oadd; + add.cls = static_cast<uint>(cls) & 3u; + add.to = jn; + add.arg[0] = jt; + add.arg[1] = getcon(ctx, j_step, fn); + addins(ctx, &L.latch->ins, &L.latch->nins, &add); + + Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + std::memset(p, 0, sizeof(Phi)); + p->to = jt; + p->cls = static_cast<short>(cls); + p->narg = 2; + p->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn)); + p->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn)); + p->arg[0] = getcon(ctx, j_init, fn); + p->blk[0] = L.preheader; + p->arg[1] = jn; + p->blk[1] = L.latch; + p->link = L.header->phi; + L.header->phi = p; + + made.push_back({bi, c, is_kl, jt, j_init, j_step}); + // Fallback: add derived IV to bivs. + bivs.push_back({jt, cls, j_init, j_step}); + // Record for LFTR. + Ref j_init_ref = getcon(ctx, j_init, fn); + created_ivs.push_back({jt, j_init_ref, j_step, cls, bi, c}); + return jt; + }; + + for (auto& cd : cands) { + Ref j = get_iv(cd.biv, cd.c, cd.is_kl); + Ins* m = &cd.b->ins[cd.idx]; // get_iv might realloc latch->ins, re-fetch here + m->op = Ocopy; + m->arg[0] = j; + m->arg[1] = Ref::none(); + // m->cls unchanged (matches j) + } + return static_cast<int>(cands.size()); +} + +// Return phi input. +Ref phi_arg_from(Phi* p, Blk* from) { + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == from) + return p->arg[k]; + return Ref::none(); +} + +Phi* find_header_phi(Blk* hd, Ref to) { + for (Phi* p = hd->phi; p; p = p->link) + if (req(p->to, to)) + return p; + return nullptr; +} + +bool is_pure_arith(int op) { + return op == Oadd || op == Osub || op == Omul || op == Oextsw || op == Ocopy; +} + +// Check load invariance. +bool load_invariant_in_loop(Fn* fn, const Loop& L, Ins* load) { + int lsz = loadsz(load); + if (lsz <= 0) + return false; + for (Blk* bb : L.body) { + for (uint i = 0; i < bb->nins; i++) { + Ins* s = &bb->ins[i]; + if (s->op == Ocall) + return false; // call may clobber any global + if (!isstore(s->op)) + continue; + int ssz = storesz(s); + int delta_unused = 0; + // storeX value, addr → arg[0]=value, arg[1]=addr + int a = alias(load->arg[0], 0, lsz, s->arg[1], ssz, &delta_unused, fn); + if (a != NoAlias) + return false; + } + } + return true; +} + +// SCEV result. +struct Scev { + int64_t step; + int src; +}; + +int insert_addr_iv(Ctx* ctx, Fn* fn, const Loop& L, const std::vector<Biv>& bivs, + std::vector<CreatedIV>& created_ivs) { + // Invariance check (memoized) + std::unordered_map<uint64_t, int> inv_memo; // -1 uncomputed, 0 false, 1 true + std::function<bool(Ref)> is_inv = [&](Ref r) -> bool { + if (rtype(r) == RCon) + return true; + if (rtype(r) != RTmp) + return false; + uint64_t k = ref_key(r); + auto it = inv_memo.find(k); + if (it != inv_memo.end()) + return it->second == 1; + inv_memo[k] = + -1; // Mark to prevent recursion cycles; non-self-loop phi returning here is false + bool result = false; + do { + uint bid = fn->tmp[r.val].bid; + if (L.body_bid.find(bid) == L.body_bid.end()) { + // Def not in loop -> invariant + result = true; + break; + } + Ins* d = fn->tmp[r.val].def; + if (!d) { + // phi def check. + if (bid != L.header->id) { + result = false; + break; + } + Phi* p = find_header_phi(L.header, r); + if (!p) { + result = false; + break; + } + bool all_ok = true; + for (uint i = 0; i < p->narg; i++) { + if (req(p->arg[i], p->to)) + continue; // self-loop + if (!is_inv(p->arg[i])) { + all_ok = false; + break; + } + } + result = all_ok; + break; + } + if (isload(d->op)) { + result = load_invariant_in_loop(fn, L, d); + break; + } + if (!is_pure_arith(d->op)) { + result = false; + break; + } + bool a0_ok = req(d->arg[0], Ref::none()) || is_inv(d->arg[0]); + bool a1_ok = req(d->arg[1], Ref::none()) || is_inv(d->arg[1]); + result = a0_ok && a1_ok; + } while (false); + inv_memo[k] = result ? 1 : 0; + return result; + }; + + // SCEV returns {step, src_biv}. + std::unordered_map<uint64_t, std::optional<Scev>> scev_memo; + std::function<std::optional<Scev>(Ref)> scev = [&](Ref r) -> std::optional<Scev> { + if (rtype(r) == RCon) + return Scev{0, -1}; + if (rtype(r) != RTmp) + return std::nullopt; + uint64_t k = ref_key(r); + auto it = scev_memo.find(k); + if (it != scev_memo.end()) + return it->second; + scev_memo[k] = std::nullopt; // prevent recursion cycle + std::optional<Scev> ans = std::nullopt; + + auto combine = [](Scev a, Scev b, int sign) -> std::optional<Scev> { + int64_t step = a.step + sign * b.step; + int src; + if (a.step == 0 && b.step == 0) + src = -1; + else if (a.step == 0) + src = b.src; + else if (b.step == 0) + src = a.src; + else if (a.src == b.src) + src = a.src; + else + return std::nullopt; + if (step == 0) + src = -1; + return Scev{step, src}; + }; + + do { + int bi = find_biv(bivs, r); + if (bi >= 0) { + ans = Scev{bivs[static_cast<size_t>(bi)].step, bi}; + break; + } + if (is_inv(r)) { + ans = Scev{0, -1}; + break; + } + Ins* d = fn->tmp[r.val].def; + if (!d) + break; // non-BIV head phi / non-invariant phi -> fail + int64_t cv = 0; + switch (d->op) { + case Ocopy: + ans = scev(d->arg[0]); + break; + case Oextsw: { + ans = scev(d->arg[0]); // sext passthrough, assumes no int32 wrap + break; + } + case Oadd: { + auto s0 = scev(d->arg[0]); + auto s1 = scev(d->arg[1]); + if (s0.has_value() && s1.has_value()) + ans = combine(*s0, *s1, +1); + break; + } + case Osub: { + auto s0 = scev(d->arg[0]); + auto s1 = scev(d->arg[1]); + if (s0.has_value() && s1.has_value()) + ans = combine(*s0, *s1, -1); + break; + } + case Omul: { + // One side linear, one side constant + if (ref_is_const(fn, d->arg[1], cv)) { + auto s = scev(d->arg[0]); + if (s.has_value()) { + int64_t step = s->step * cv; + ans = Scev{step, step == 0 ? -1 : s->src}; + } + } else if (ref_is_const(fn, d->arg[0], cv)) { + auto s = scev(d->arg[1]); + if (s.has_value()) { + int64_t step = s->step * cv; + ans = Scev{step, step == 0 ? -1 : s->src}; + } + } + break; + } + default: + break; + } + } while (false); + scev_memo[k] = ans; + return ans; + }; + + // Materialize init. + std::unordered_map<uint64_t, Ref> mat_memo; + std::function<Ref(Ref)> materialize = [&](Ref r) -> Ref { + if (rtype(r) == RCon) + return r; + uint64_t k = ref_key(r); + auto it = mat_memo.find(k); + if (it != mat_memo.end()) + return it->second; + + // BIV -> init constant + int bi = find_biv(bivs, r); + if (bi >= 0) { + Ref c = getcon(ctx, bivs[static_cast<size_t>(bi)].init, fn); + mat_memo[k] = c; + return c; + } + + // Decide based on def block. + uint bid = fn->tmp[r.val].bid; + if (L.body_bid.find(bid) == L.body_bid.end()) { + mat_memo[k] = r; + return r; + } + Ins* d = fn->tmp[r.val].def; + if (!d) { + // phi in body header. + if (bid != L.header->id) { + mat_memo[k] = + r; // Fallback: shouldn't reach here (filtered by scev), but prevents crash + return r; + } + Phi* p = find_header_phi(L.header, r); + if (!p) { + mat_memo[k] = r; + return r; + } + Ref pe = phi_arg_from(p, L.preheader); + Ref sub = materialize(pe); + mat_memo[k] = sub; + return sub; + } + + // Ins in body: Ocopy/Oextsw/Oadd/Osub/Omul - copy one to preheader + Ref a0 = req(d->arg[0], Ref::none()) ? Ref::none() : materialize(d->arg[0]); + Ref a1 = req(d->arg[1], Ref::none()) ? Ref::none() : materialize(d->arg[1]); + + if (d->op == Ocopy) { + mat_memo[k] = a0; + return a0; + } + if (d->op == Oextsw && rtype(a0) == RCon) { + mat_memo[k] = a0; // Kl constant itself is fine + return a0; + } + + int cls = static_cast<int>(d->cls); + Ref nt = newtmp(ctx, "lsr", cls, fn); + Ins nins{}; + nins.op = d->op; + nins.cls = static_cast<uint>(cls) & 3u; + nins.to = nt; + nins.arg[0] = a0; + nins.arg[1] = a1; + addins(ctx, &L.preheader->ins, &L.preheader->nins, &nins); + mat_memo[k] = nt; + return nt; + }; + + // Scan candidates. + auto resolve_copy = [&](Ref r) -> Ref { + // Trace Ocopy chain. + for (int hop = 0; hop < 8; hop++) { + Ins* d = def_ins(fn, r); + if (!d || d->op != Ocopy) + break; + r = d->arg[0]; + } + return r; + }; + struct AddrCand { + Blk* b; + uint idx; + int64_t step; + int src_biv; + uint64_t dedup_key; + }; + std::vector<AddrCand> cands; + for (Blk* b : L.body) { + for (uint ix = 0; ix < b->nins; ix++) { + Ins* m = &b->ins[ix]; + if (m->op != Oadd || m->cls != Kl) + continue; + int64_t cb = 0; + if (ref_is_const(fn, m->arg[0], cb) || ref_is_const(fn, m->arg[1], cb)) + continue; // CBits step -> BIV self-increment, skip + auto s = scev(m->to); + if (!s.has_value() || s->step == 0) + continue; + Ref a0r = resolve_copy(m->arg[0]); + Ref a1r = resolve_copy(m->arg[1]); + uint64_t k0 = ref_key(a0r); + uint64_t k1 = ref_key(a1r); + if (k0 > k1) + std::swap(k0, k1); + uint64_t dk = (k0 * 1315423911ull) ^ (k1 + 0x9E3779B97F4A7C15ull); + cands.push_back({b, ix, s->step, s->src, dk}); + } + } + if (cands.empty()) + return 0; + + // Deduplication + Rewrite + struct IVRec { + uint64_t key; + Ref p; + }; + std::vector<IVRec> ivs; + + auto find_iv = [&](uint64_t key) -> Ref { + for (auto& r : ivs) + if (r.key == key) + return r.p; + return Ref::none(); + }; + + int hits = 0; + for (auto& cd : cands) { + Ref p = find_iv(cd.dedup_key); + if (req(p, Ref::none())) { + // Materialize base (init at preheader) + Ins* m0 = &cd.b->ins[cd.idx]; + Ref orig_to = m0->to; + Ref p_init = materialize(orig_to); + + int cls = Kl; + Ref pt = newtmp(ctx, "lsr", cls, fn); + Ref pn = newtmp(ctx, "lsr", cls, fn); + + // latch: pn = pt + step + Ins add{}; + add.op = Oadd; + add.cls = static_cast<uint>(cls) & 3u; + add.to = pn; + add.arg[0] = pt; + add.arg[1] = getcon(ctx, cd.step, fn); + addins(ctx, &L.latch->ins, &L.latch->nins, &add); + + // header: pt = phi(p_init @ pre, pn @ latch) + Phi* ph = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + std::memset(ph, 0, sizeof(Phi)); + ph->to = pt; + ph->cls = static_cast<short>(cls); + ph->narg = 2; + ph->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn)); + ph->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn)); + ph->arg[0] = p_init; + ph->blk[0] = L.preheader; + ph->arg[1] = pn; + ph->blk[1] = L.latch; + ph->link = L.header->phi; + L.header->phi = ph; + + ivs.push_back({cd.dedup_key, pt}); + p = pt; + + // Record CreatedIV. + int64_t c = 0; + if (cd.src_biv >= 0) { + int64_t bs = bivs[static_cast<size_t>(cd.src_biv)].step; + if (bs != 0 && cd.step % bs == 0) + c = cd.step / bs; + } + created_ivs.push_back({pt, p_init, cd.step, Kl, cd.src_biv, c}); + } + + // Re-fetch pointer. + Ins* m = &cd.b->ins[cd.idx]; + m->op = Ocopy; + m->arg[0] = p; + m->arg[1] = Ref::none(); + // m->cls remains Kl + hits++; + } + return hits; +} + +// Exit cmp mapping. +bool classify_cmp(int op, int& op_w, int& op_w_flip, int& op_l, int& op_l_flip) { + auto setpair = [&](int w, int w_flip, int l, int l_flip) { + op_w = w; + op_w_flip = w_flip; + op_l = l; + op_l_flip = l_flip; + }; + switch (op) { + case Oceqw: + case Oceql: + setpair(Oceqw, Oceqw, Oceql, Oceql); + return true; + case Ocnew: + case Ocnel: + setpair(Ocnew, Ocnew, Ocnel, Ocnel); + return true; + case Ocsltw: + case Ocsltl: + setpair(Ocsltw, Ocsgtw, Ocsltl, Ocsgtl); + return true; + case Ocslew: + case Ocslel: + setpair(Ocslew, Ocsgew, Ocslel, Ocsgel); + return true; + case Ocsgtw: + case Ocsgtl: + setpair(Ocsgtw, Ocsltw, Ocsgtl, Ocsltl); + return true; + case Ocsgew: + case Ocsgel: + setpair(Ocsgew, Ocslew, Ocsgel, Ocslel); + return true; + default: + return false; + } +} + +int replace_exit_test(Ctx* ctx, Fn* fn, const Loop& L, const std::vector<Biv>& bivs, + const std::vector<CreatedIV>& created_ivs) { + if (L.header->jmp.type != Jjnz) + return 0; + Ref cond = L.header->jmp.arg; + if (rtype(cond) != RTmp) + return 0; + + // cmp must be in header, and nuse==1 (otherwise modifying it affects other places) + Ins* cmp = nullptr; + uint cmp_idx = 0; + for (uint i = 0; i < L.header->nins; i++) { + if (req(L.header->ins[i].to, cond)) { + cmp = &L.header->ins[i]; + cmp_idx = i; + break; + } + } + if (!cmp) + return 0; + if (fn->tmp[cond.val].nuse != 1) + return 0; + + int op_w, op_w_flip, op_l, op_l_flip; + if (!classify_cmp(static_cast<int>(cmp->op), op_w, op_w_flip, op_l, op_l_flip)) + return 0; + + // Find BIV side + invariant side + int bi = find_biv(bivs, cmp->arg[0]); + Ref n_ref; + bool biv_at_0; + if (bi >= 0) { + n_ref = cmp->arg[1]; + biv_at_0 = true; + } else { + bi = find_biv(bivs, cmp->arg[1]); + if (bi < 0) + return 0; + n_ref = cmp->arg[0]; + biv_at_0 = false; + } + // If the other side is also a BIV, LFTR skips (comparison between two IVs) + if (find_biv(bivs, n_ref) >= 0) + return 0; + + // LFTR Guard. + { + Ref biv_to = bivs[static_cast<size_t>(bi)].to; + Phi* biv_phi = find_header_phi(L.header, biv_to); + Ins* self_inc = nullptr; + if (biv_phi) { + for (uint k = 0; k < biv_phi->narg; k++) { + if (biv_phi->blk[k] == L.latch) { + Ref a = biv_phi->arg[k]; + if (rtype(a) == RTmp && a.val < static_cast<uint>(fn->ntmp)) + self_inc = fn->tmp[a.val].def; + break; + } + } + } + if (biv_to.val >= static_cast<uint>(fn->ntmp)) + return 0; + Tmp* bt = &fn->tmp[biv_to.val]; + int extra = 0; + for (uint u = 0; u < bt->nuse; u++) { + const Use& uu = bt->use[u]; + if (uu.type == Use::UIns) { + if (uu.u.ins == cmp) + continue; + if (uu.u.ins == self_inc) + continue; + // Orphans check. + Ref user_to = uu.u.ins->to; + if (rtype(user_to) == RTmp && user_to.val < static_cast<uint>(fn->ntmp) && + fn->tmp[user_to.val].nuse == 0) + continue; + extra++; + } else if (uu.type == Use::UPhi) { + // Count phi use. + extra++; + } else if (uu.type == Use::UJmp) { + extra++; + } + } + if (extra > 0) + return 0; + } + + // Materialize to preheader. + std::unordered_map<uint64_t, int> inv_memo; + std::function<bool(Ref)> is_inv = [&](Ref r) -> bool { + if (rtype(r) == RCon) + return true; + if (rtype(r) != RTmp) + return false; + uint64_t k = ref_key(r); + auto it = inv_memo.find(k); + if (it != inv_memo.end()) + return it->second == 1; + inv_memo[k] = -1; + bool result = false; + do { + uint bid = fn->tmp[r.val].bid; + if (L.body_bid.find(bid) == L.body_bid.end()) { + result = true; + break; + } + Ins* d = fn->tmp[r.val].def; + if (!d) { + if (bid != L.header->id) { + result = false; + break; + } + Phi* p = find_header_phi(L.header, r); + if (!p) { + result = false; + break; + } + bool all_ok = true; + for (uint i = 0; i < p->narg; i++) { + if (req(p->arg[i], p->to)) + continue; + if (!is_inv(p->arg[i])) { + all_ok = false; + break; + } + } + result = all_ok; + break; + } + if (isload(d->op)) { + result = load_invariant_in_loop(fn, L, d); + break; + } + if (!is_pure_arith(d->op)) { + result = false; + break; + } + bool a0_ok = req(d->arg[0], Ref::none()) || is_inv(d->arg[0]); + bool a1_ok = req(d->arg[1], Ref::none()) || is_inv(d->arg[1]); + result = a0_ok && a1_ok; + } while (false); + inv_memo[k] = result ? 1 : 0; + return result; + }; + + if (!is_inv(n_ref)) + return 0; + + // Materialize n_ref to preheader + std::unordered_map<uint64_t, Ref> mat_memo; + std::function<Ref(Ref)> materialize = [&](Ref r) -> Ref { + if (rtype(r) == RCon) + return r; + uint64_t k = ref_key(r); + auto it = mat_memo.find(k); + if (it != mat_memo.end()) + return it->second; + uint bid = fn->tmp[r.val].bid; + if (L.body_bid.find(bid) == L.body_bid.end()) { + mat_memo[k] = r; + return r; + } + Ins* d = fn->tmp[r.val].def; + if (!d) { + // phi in body (invariant): take preheader edge + if (bid != L.header->id) { + mat_memo[k] = r; + return r; + } + Phi* p = find_header_phi(L.header, r); + if (!p) { + mat_memo[k] = r; + return r; + } + Ref pe = phi_arg_from(p, L.preheader); + Ref sub = materialize(pe); + mat_memo[k] = sub; + return sub; + } + Ref a0 = req(d->arg[0], Ref::none()) ? Ref::none() : materialize(d->arg[0]); + Ref a1 = req(d->arg[1], Ref::none()) ? Ref::none() : materialize(d->arg[1]); + if (d->op == Ocopy) { + mat_memo[k] = a0; + return a0; + } + if (d->op == Oextsw && rtype(a0) == RCon) { + mat_memo[k] = a0; + return a0; + } + int cls = static_cast<int>(d->cls); + Ref nt = newtmp(ctx, "lsr.lftr", cls, fn); + Ins ni{}; + ni.op = d->op; + ni.cls = static_cast<uint>(cls) & 3u; + ni.to = nt; + ni.arg[0] = a0; + ni.arg[1] = a1; + addins(ctx, &L.preheader->ins, &L.preheader->nins, &ni); + mat_memo[k] = nt; + return nt; + }; + + Ref n_at_pre = materialize(n_ref); + + // Choose replacement IV: src_biv == bi and c != 0, pick smallest |c|. + int best = -1; + int64_t best_abs = 0; + for (size_t i = 0; i < created_ivs.size(); i++) { + const CreatedIV& iv = created_ivs[i]; + if (iv.src_biv != bi) + continue; + if (iv.c == 0) + continue; + int64_t a = iv.c < 0 ? -iv.c : iv.c; + if (best < 0 || a < best_abs) { + best = static_cast<int>(i); + best_abs = a; + } + } + if (best < 0) + return 0; + + const CreatedIV& iv = created_ivs[static_cast<size_t>(best)]; + int64_t i0 = bivs[static_cast<size_t>(bi)].init; + int64_t cc = iv.c; + int iv_cls = iv.cls; // Kw or Kl, determines cls of preheader arithmetic and new cmp + + // n_at_pre's cls must be compatible with IV: + // - iv Kl: if n Kw -> extsw; if n Kl -> use directly + // - iv Kw: if n Kw -> use directly; if n Kl -> unsafe (truncation), skip + int n_cls = Kl; + if (rtype(n_at_pre) == RTmp) + n_cls = static_cast<int>(fn->tmp[n_at_pre.val].cls); + else + n_cls = + iv_cls; // RCon treated as target cls (CBits 64-bit constant, getcon reused directly) + Ref n_use; + if (iv_cls == Kl && n_cls == Kw) { + n_use = newtmp(ctx, "lsr.lftr", Kl, fn); + Ins ni{}; + ni.op = Oextsw; + ni.cls = static_cast<uint>(Kl) & 3u; + ni.to = n_use; + ni.arg[0] = n_at_pre; + ni.arg[1] = Ref::none(); + addins(ctx, &L.preheader->ins, &L.preheader->nins, &ni); + } else if (iv_cls == n_cls || rtype(n_at_pre) == RCon) { + n_use = n_at_pre; + } else { + return 0; // iv Kw, n Kl is unsafe + } + + Ref i0_c = getcon(ctx, i0, fn); + Ref c_c = getcon(ctx, cc, fn); + + auto emit_pre = [&](int op, int cls, Ref a0, Ref a1) -> Ref { + Ref nt = newtmp(ctx, "lsr.lftr", cls, fn); + Ins ni{}; + ni.op = static_cast<uint>(op) & 0x3fffffffu; + ni.cls = static_cast<uint>(cls) & 3u; + ni.to = nt; + ni.arg[0] = a0; + ni.arg[1] = a1; + addins(ctx, &L.preheader->ins, &L.preheader->nins, &ni); + return nt; + }; + Ref diff = emit_pre(Osub, iv_cls, n_use, i0_c); + Ref scaled = emit_pre(Omul, iv_cls, diff, c_c); + Ref pend = emit_pre(Oadd, iv_cls, iv.init_ref, scaled); + + // Rewrite cmp (in-place; preheader's addins might realloc, so re-fetch is safe) + cmp = &L.header->ins[cmp_idx]; + int final_op = (iv_cls == Kl) ? (cc < 0 ? op_l_flip : op_l) : (cc < 0 ? op_w_flip : op_w); + cmp->op = static_cast<uint>(final_op) & 0x3fffffffu; + cmp->cls = static_cast<uint>(iv_cls) & 3u; + if (biv_at_0) { + cmp->arg[0] = iv.to; + cmp->arg[1] = pend; + } else { + cmp->arg[0] = pend; + cmp->arg[1] = iv.to; + } + return 1; +} + +bool run_loop(Ctx* ctx, Fn* fn, const Loop& L, int& reduced_total, int& lifted_total, + int& retested_total) { + std::vector<Biv> bivs; + detect_bivs(fn, L, bivs); + if (bivs.empty()) + return false; + + std::vector<CreatedIV> created_ivs; + int reduced = reduce_mul(ctx, fn, L, bivs, created_ivs); + int lifted = insert_addr_iv(ctx, fn, L, bivs, created_ivs); + int retested = 0; + if (!created_ivs.empty()) { + // tmp.nuse might be stale after insert_addr_iv rewrites mul->Ocopy. LFTR guard relies on + // nuse, so update once. + filluse(ctx, fn); + retested = replace_exit_test(ctx, fn, L, bivs, created_ivs); + } + + reduced_total += reduced; + lifted_total += lifted; + retested_total += retested; + return (reduced + lifted + retested) > 0; +} + +// LSR cleanup: resolve all RTmp references along Ocopy chains to their roots. +// insert_addr_iv rewrites address additions as Ocopy %new_iv, leaving isolated Ocopys. +// Downstream passes expect canonical IR. Walk uses and resolve RTmp defs if they are Ocopy. +// Keep Ocopy itself; nuse becomes 0, and DCE cleans it up. +void propagate_copies(Fn* fn) { + auto resolve = [&](Ref r) -> Ref { + for (int hop = 0; hop < 16; hop++) { + if (rtype(r) != RTmp) + break; + uint v = r.val; + if (v >= static_cast<uint>(fn->ntmp)) + break; + Ins* d = fn->tmp[v].def; + if (!d || d->op != Ocopy) + break; + if (rtype(d->arg[0]) == -1) + break; + r = d->arg[0]; + } + return r; + }; + for (Blk* b = fn->start; b; b = b->link) { + for (Phi* p = b->phi; p; p = p->link) + for (uint k = 0; k < p->narg; k++) + p->arg[k] = resolve(p->arg[k]); + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (i->op == Ocopy) + continue; // Do not resolve Ocopy's own arg to preserve chain info + for (int a = 0; a < 2; a++) + i->arg[a] = resolve(i->arg[a]); + } + b->jmp.arg = resolve(b->jmp.arg); + b->jmp.arg2 = resolve(b->jmp.arg2); + } +} + +} // namespace + +void lsr(Ctx* ctx, Fn* fn) { + // alias info used by is_inv to check loop-invariant loads. + // LSR only produces phi/add/mul/copy/sub, doesn't touch load/store, so no need to recalculate + // inside fixpoint iter. + fillalias(fn); + + int iter = 0; + int reduced_total = 0; + int lifted_total = 0; + int retested_total = 0; + for (; iter < kFixpointIters; iter++) { + std::vector<Loop> loops; + find_loops(fn, loops); + if (loops.empty()) + break; + + bool changed = false; + for (auto& L : loops) + if (run_loop(ctx, fn, L, reduced_total, lifted_total, retested_total)) + changed = true; + if (!changed) + break; + // CFG unchanged; new Ins def requires filluse to write Tmp.def for next round's + // scev/detect_bivs. + filluse(ctx, fn); + } + + // After fixpoint, resolve Ocopy chains left by insert_addr_iv globally. + // Downstream passes won't see intermediate Ocopy references. Isolated Ocopy ins cleaned by DCE. + propagate_copies(fn); + + if (debug['V']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, + "\n> LSR %s: reduced=%d mul, lifted=%d addr, retested=%d exit, in %d round%s\n", + fn->name, reduced_total, lifted_total, retested_total, iter, iter == 1 ? "" : "s"); + printfn(fn, out); + } +} + +} // namespace rcc diff --git a/src/mid/mem.cc b/src/mid/mem.cc new file mode 100644 index 0000000..12b9ef8 --- /dev/null +++ b/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/src/mid/mulopt.cc b/src/mid/mulopt.cc new file mode 100644 index 0000000..0a369db --- /dev/null +++ b/src/mid/mulopt.cc @@ -0,0 +1,208 @@ +/* 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 mulopt.cc +/// @brief Constant integer multiplication strength reduction. +/// +/// See docs/mulopt.md for architectural notes. + +#include "rcc/all.h" + +#include <cstdint> + +namespace rcc { + +namespace { + +enum Kind { + KZero, + KCopy, + KShl, + KShlAddShl, + KShlSubShl, +}; + +struct Rewrite { + Kind kind; + int sh1; // Primary shift for KShl / KShlAddShl / KShlSubShl + int sh2; // Secondary shift for KShlAddShl / KShlSubShl (0 means use x itself) +}; + +// Classify patterns. +bool classify(uint64_t ac, int max_shift, Rewrite& out) { + if (ac == 0) { + out.kind = KZero; + return true; + } + if (ac == 1) { + out.kind = KCopy; + return true; + } + if ((ac & (ac - 1)) == 0) { + int k = __builtin_ctzll(ac); + if (k >= max_shift) + return false; + out.kind = KShl; + out.sh1 = k; + return true; + } + int a = 63 - __builtin_clzll(ac); // floor log2(ac), 0 < ac, ac is not pow2 + if (a >= max_shift) + return false; + uint64_t hi = static_cast<uint64_t>(1) << a; + + // pow2 + pow2. + uint64_t low = ac - hi; + if ((low & (low - 1)) == 0) { + int b = __builtin_ctzll(low); + out.kind = KShlAddShl; + out.sh1 = a; + out.sh2 = b; + return true; + } + + // pow2 - pow2. + if (a + 1 < max_shift) { + uint64_t hi2 = hi << 1; + uint64_t diff = hi2 - ac; + // Check diff. + if ((diff & (diff - 1)) == 0) { + int b = __builtin_ctzll(diff); + out.kind = KShlSubShl; + out.sh1 = a + 1; + out.sh2 = b; + return true; + } + } + return false; +} + +// emit() prepends. +bool lower_mul(Ctx* ctx, Fn* fn, Ins* i, Blk* b, bool* rebuilt) { + if (i->op != Omul) + return false; + if (i->cls != Kw && i->cls != Kl) + return false; + if (rtype(i->arg[1]) != RCon) + return false; + Con* c = &fn->con[i->arg[1].val]; + if (c->type != Con::CBits) + return false; + + int cls = i->cls; + int64_t cv; + int max_shift; + if (cls == Kw) { + cv = static_cast<int32_t>( + c->bits.i); // Narrow to i32 to avoid interference from Kl residual bits + max_shift = 32; + } else { + cv = c->bits.i; + max_shift = 64; + } + + bool neg = (cv < 0); + uint64_t ac = static_cast<uint64_t>(cv); + if (neg) + ac = -ac; // wrap-around produces 2^31 / 2^63 for INT_MIN / INT64_MIN respectively + + Rewrite r; + if (!classify(ac, max_shift, r)) + return false; + + Ref dst = i->to; + Ref x = i->arg[0]; + + // Move unprocessed tail. + if (!*rebuilt) { + ctx->curi = ctx->insb + NIns; + ulong tail = static_cast<ulong>((b->ins + b->nins) - (i + 1)); + ctx->curi -= tail; + icpy(ctx->curi, i + 1, tail); + *rebuilt = true; + } + + switch (r.kind) { + case KZero: + // 0 * x = 0. + emit(ctx, Ocopy, cls, dst, getcon(ctx, 0, fn), Ref::none()); + break; + case KCopy: + if (neg) + emit(ctx, Oneg, cls, dst, x, Ref::none()); + else + emit(ctx, Ocopy, cls, dst, x, Ref::none()); + break; + case KShl: { + if (neg) { + Ref t = newtmp(ctx, "mulopt", cls, fn); + emit(ctx, Oneg, cls, dst, t, Ref::none()); + emit(ctx, Oshl, cls, t, x, getcon(ctx, r.sh1, fn)); + } else { + emit(ctx, Oshl, cls, dst, x, getcon(ctx, r.sh1, fn)); + } + break; + } + case KShlAddShl: { + // sum. + Ref sum = neg ? newtmp(ctx, "mulopt", cls, fn) : dst; + Ref t1 = newtmp(ctx, "mulopt", cls, fn); + Ref t2 = (r.sh2 == 0) ? x : newtmp(ctx, "mulopt", cls, fn); + if (neg) + emit(ctx, Oneg, cls, dst, sum, Ref::none()); + emit(ctx, Oadd, cls, sum, t1, t2); + if (r.sh2 != 0) + emit(ctx, Oshl, cls, t2, x, getcon(ctx, r.sh2, fn)); + emit(ctx, Oshl, cls, t1, x, getcon(ctx, r.sh1, fn)); + break; + } + case KShlSubShl: { + Ref diff = neg ? newtmp(ctx, "mulopt", cls, fn) : dst; + Ref t1 = newtmp(ctx, "mulopt", cls, fn); + Ref t2 = (r.sh2 == 0) ? x : newtmp(ctx, "mulopt", cls, fn); + if (neg) + emit(ctx, Oneg, cls, dst, diff, Ref::none()); + emit(ctx, Osub, cls, diff, t1, t2); + if (r.sh2 != 0) + emit(ctx, Oshl, cls, t2, x, getcon(ctx, r.sh2, fn)); + emit(ctx, Oshl, cls, t1, x, getcon(ctx, r.sh1, fn)); + break; + } + } + return true; +} + +} // namespace + +void mulopt(Ctx* ctx, Fn* fn) { + for (Blk* b = fn->start; b; b = b->link) { + bool rebuilt = false; + for (Ins* i = b->ins + b->nins; i != b->ins;) { + --i; + if (lower_mul(ctx, fn, i, b, &rebuilt)) + continue; + if (rebuilt) + emiti(ctx, *i); + } + if (rebuilt) + idup(ctx, b, ctx->curi, static_cast<ulong>(ctx->insb + NIns - ctx->curi)); + } +} + +} // namespace rcc diff --git a/src/mid/reassoc.cc b/src/mid/reassoc.cc new file mode 100644 index 0000000..91ff747 --- /dev/null +++ b/src/mid/reassoc.cc @@ -0,0 +1,436 @@ +/* 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 reassoc.cc +/// @brief Reassociation pass (LLVM rank-based style). +/// +/// See docs/reassoc.md for architectural notes. + +#include "rcc/all.h" + +#include <algorithm> +#include <cassert> +#include <cstdint> +#include <cstdio> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +enum Family { FAdd, FMul, FAnd, FOr, FXor, FNone }; + +Family family_of(uint op) { + switch (op) { + case Oadd: + case Osub: + return FAdd; + case Omul: + return FMul; + case Oand: + return FAnd; + case Oor: + return FOr; + case Oxor: + return FXor; + default: + return FNone; + } +} + +// Binary op for the family. +int family_binop(Family f) { + switch (f) { + case FAdd: + return Oadd; + case FMul: + return Omul; + case FAnd: + return Oand; + case FOr: + return Oor; + case FXor: + return Oxor; + default: + return Onop; + } +} + +struct Leaf { + Ref ref; + int sign; // Only meaningful for FAdd, otherwise always +1 +}; + +struct Reassoc { + Ctx* ctx; + Fn* fn; + std::unordered_set<Ins*> absorbed; + std::unordered_map<Ins*, std::vector<Ins>> + prelude; // root ins -> intermediate instructions in chain + int n_trees = 0, n_folded = 0, n_addsub = 0, n_chainmax = 0; + + bool is_family_node(Ins* d, Family fam, int cls) { + return d && family_of(d->op) == fam && static_cast<int>(d->cls) == cls; + } + + int64_t mask_val(int cls, int64_t v) { + return (cls == Kw) ? static_cast<int64_t>(static_cast<int32_t>(v)) : v; + } + + // Flatten nodes. + void collect(Ins* root, Family fam, int cls, std::vector<Leaf>& out) { + std::vector<Leaf> stk; + if (fam == FAdd) { + stk.push_back({root->arg[0], +1}); + stk.push_back({root->arg[1], (root->op == Osub) ? -1 : +1}); + } else { + stk.push_back({root->arg[0], +1}); + stk.push_back({root->arg[1], +1}); + } + while (!stk.empty()) { + Leaf cur = stk.back(); + stk.pop_back(); + if (rtype(cur.ref) == RTmp && cur.ref.val != 0) { + Tmp* t = &fn->tmp[cur.ref.val]; + Ins* d = t->def; + if (d && t->nuse == 1 && is_family_node(d, fam, cls) && !absorbed.count(d)) { + absorbed.insert(d); + if (fam == FAdd) { + stk.push_back({d->arg[0], cur.sign}); + stk.push_back({d->arg[1], (d->op == Osub) ? -cur.sign : cur.sign}); + } else { + stk.push_back({d->arg[0], cur.sign}); + stk.push_back({d->arg[1], cur.sign}); + } + continue; + } + } + out.push_back(cur); + } + } + + // rank nodes. + static uint64_t rank_of(Ref r) { + if (rtype(r) == RCon) + return (static_cast<uint64_t>(UINT32_MAX) << 32) | r.val; + return (static_cast<uint64_t>(r.val) << 32) | r.val; + } + + void sort_terms(std::vector<Leaf>& v) { + std::sort(v.begin(), v.end(), + [](const Leaf& a, const Leaf& b) { return rank_of(a.ref) < rank_of(b.ref); }); + } + + // Build left-leaning chain. + Ref build_full(const std::vector<Ref>& refs, int bop, int cls, std::vector<Ins>& pre) { + Ref acc = refs[0]; + for (size_t k = 1; k < refs.size(); k++) { + Ref t = newtmp(ctx, "rea", cls, fn); + Ins ni; + ni.op = static_cast<uint>(bop); + ni.cls = static_cast<uint>(cls); + ni.to = t; + ni.arg[0] = acc; + ni.arg[1] = refs[k]; + pre.push_back(ni); + acc = t; + } + return acc; + } + + void make_copy(Ins* root, Ref r) { + root->op = Ocopy; + root->arg[0] = r; + root->arg[1] = Ref::none(); + } + + void make_bin(Ins* root, int op, Ref a0, Ref a1) { + root->op = static_cast<uint>(op); + root->arg[0] = a0; + root->arg[1] = a1; + } + + // Add/Sub Reconstruction + void rebuild_addsub(Ins* root, int cls, std::vector<Leaf>& leaves) { + // Constant folding. + int64_t cacc = 0; + std::vector<Leaf> terms; + int nconst = 0; + for (const Leaf& lf : leaves) { + int64_t v; + if (isconbits(fn, lf.ref, &v)) { + cacc += lf.sign * v; + nconst++; + } else { + terms.push_back(lf); + } + } + cacc = mask_val(cls, cacc); + if (nconst >= 2) + n_folded++; + + sort_terms(terms); + // Net sum of signs. + std::vector<Leaf> canon; + for (size_t k = 0; k < terms.size();) { + size_t j = k; + int64_t net = 0; + while (j < terms.size() && req(terms[j].ref, terms[k].ref)) { + net += terms[j].sign; + j++; + } + int s = (net > 0) ? +1 : -1; + for (int64_t c = 0; c < (net < 0 ? -net : net); c++) + canon.push_back({terms[k].ref, s}); + k = j; + } + + // Merge constants. + std::vector<Ref> pos, neg; + for (const Leaf& lf : canon) { + if (lf.sign > 0) + pos.push_back(lf.ref); + else + neg.push_back(lf.ref); + } + if (cacc != 0) + pos.push_back(getcon(ctx, cacc, fn)); + + n_addsub++; + + if (pos.empty() && neg.empty()) { + make_copy(root, getcon(ctx, cacc, fn)); // cacc==0 here -> copy 0 + return; + } + std::vector<Ins>& pre = prelude[root]; + if (neg.empty()) { + if (pos.size() == 1) { + make_copy(root, pos[0]); + } else { + Ref acc = build_full(std::vector<Ref>(pos.begin(), pos.end() - 1), Oadd, cls, pre); + make_bin(root, Oadd, acc, pos.back()); + } + } else if (pos.empty()) { + Ref negr = build_full(neg, Oadd, cls, pre); + make_bin(root, Oneg, negr, Ref::none()); + } else { + Ref posr = build_full(pos, Oadd, cls, pre); + Ref negr = build_full(neg, Oadd, cls, pre); + make_bin(root, Osub, posr, negr); + } + size_t len = pos.size() + neg.size(); + if (static_cast<int>(len) > n_chainmax) + n_chainmax = static_cast<int>(len); + } + + // Mul/And/Or/Xor Reconstruction + void rebuild_logic(Ins* root, Family fam, int cls, std::vector<Leaf>& leaves) { + int bop = family_binop(fam); + int64_t mask = (cls == Kw) ? 0xffffffffLL : ~0LL; + int64_t allones = mask; + + // Identity starting point + int64_t acc; + switch (fam) { + case FMul: + acc = 1; + break; + case FAnd: + acc = allones; + break; + default: + acc = 0; // Or / Xor + break; + } + int nconst = 0; + std::vector<Leaf> terms; + for (const Leaf& lf : leaves) { + int64_t v; + if (isconbits(fn, lf.ref, &v)) { + nconst++; + v &= mask; + switch (fam) { + case FMul: + acc *= v; + break; + case FAnd: + acc &= v; + break; + case FOr: + acc |= v; + break; + default: + acc ^= v; + break; + } + } else { + terms.push_back(lf); + } + } + acc &= mask; + if (nconst >= 2) + n_folded++; + + // Short circuit. + if ((fam == FMul && acc == 0 && nconst > 0) || (fam == FAnd && acc == 0 && nconst > 0)) { + make_copy(root, getcon(ctx, 0, fn)); + return; + } + if (fam == FOr && acc == allones && nconst > 0) { + make_copy(root, getcon(ctx, mask_val(cls, allones), fn)); + return; + } + + sort_terms(terms); + // Cancellation. + std::vector<Ref> refs; + if (fam == FAnd || fam == FOr) { + for (size_t k = 0; k < terms.size(); k++) + if (k == 0 || !req(terms[k].ref, terms[k - 1].ref)) + refs.push_back(terms[k].ref); + } else if (fam == FXor) { + for (size_t k = 0; k < terms.size();) { + size_t j = k; + while (j < terms.size() && req(terms[j].ref, terms[k].ref)) + j++; + if ((j - k) & 1) + refs.push_back(terms[k].ref); + k = j; + } + } else { + for (const Leaf& lf : terms) + refs.push_back(lf.ref); + } + + // Whether to merge constant: only add if not identity. + bool const_is_id = (fam == FMul && acc == 1) || (fam == FAnd && acc == allones) || + ((fam == FOr || fam == FXor) && acc == 0); + if (!const_is_id || refs.empty()) + refs.push_back(getcon(ctx, mask_val(cls, acc), fn)); + + if (refs.size() == 1) { + make_copy(root, refs[0]); + } else { + std::vector<Ins>& pre = prelude[root]; + Ref acc_ref = build_full(std::vector<Ref>(refs.begin(), refs.end() - 1), bop, cls, pre); + make_bin(root, bop, acc_ref, refs.back()); + } + if (static_cast<int>(refs.size()) > n_chainmax) + n_chainmax = static_cast<int>(refs.size()); + } + + void process_root(Ins* root) { + Family fam = family_of(root->op); + int cls = static_cast<int>(root->cls); + size_t before = absorbed.size(); + std::vector<Leaf> leaves; + collect(root, fam, cls, leaves); + size_t delta = absorbed.size() - before; + + // Act on trees. + int nconst = 0; + for (const Leaf& lf : leaves) { + int64_t v; + if (isconbits(fn, lf.ref, &v)) + nconst++; + } + if (delta == 0 && nconst < 2) + return; + + n_trees++; + if (fam == FAdd) + rebuild_addsub(root, cls, leaves); + else + rebuild_logic(root, fam, cls, leaves); + } + + void run() { + // Collect root candidates. + std::vector<Ins*> roots; + for (Blk* b = fn->start; b; b = b->link) { + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (rtype(i->to) != RTmp) + continue; + Family fam = family_of(i->op); + if (fam == FNone) + continue; + if (KBASE(i->cls) != 0) + continue; // integer only + // Check root. + Tmp* t = &fn->tmp[i->to.val]; + if (t->nuse == 1 && t->use && t->use[0].type == Use::UIns) { + Ins* parent = t->use[0].u.ins; + if (is_family_node(parent, fam, static_cast<int>(i->cls))) + continue; + } + roots.push_back(i); + } + } + + // Process each root. + for (Ins* r : roots) { + if (absorbed.count(r)) + continue; + process_root(r); + } + + // Mark absorbed intermediate nodes as Onop. + for (Ins* a : absorbed) + a->op = Onop; + + // Reconstruct. + for (Blk* b = fn->start; b; b = b->link) { + Ins* nins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn)); + uint cnt = 0; + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (i->op == Onop) + continue; + auto it = prelude.find(i); + if (it != prelude.end()) + for (Ins& p : it->second) + addins(ctx, &nins, &cnt, &p); + addins(ctx, &nins, &cnt, i); + } + b->ins = nins; + b->nins = cnt; + } + + if (debug['E']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> reassoc %s: trees=%d folded=%d addsub=%d chain_max=%d\n", fn->name, + n_trees, n_folded, n_addsub, n_chainmax); + } + } +}; + +} // namespace + +void reassoc(Ctx* ctx, Fn* fn) { + // Pre/post conditions. + Reassoc r; + r.ctx = ctx; + r.fn = fn; + r.run(); +} + +} // namespace rcc diff --git a/src/mid/reduction_unroll.cc b/src/mid/reduction_unroll.cc new file mode 100644 index 0000000..08edb3d --- /dev/null +++ b/src/mid/reduction_unroll.cc @@ -0,0 +1,528 @@ +/* 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 reduction_unroll.cc +/// @brief Identifies inner loop integer reductions and unrolls them into K-way parallel +/// accumulators. +/// +/// See docs/reduction_unroll.md for architectural notes. + +#include "rcc/all.h" +#include "rcc/loopinfo.h" + +#include <algorithm> +#include <cstring> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +constexpr int kUnrollK = 4; + +int load_bytes(int op, int cls) { + switch (op) { + case Oloadsb: + case Oloadub: + return 1; + case Oloadsh: + case Oloaduh: + return 2; + case Oloadsw: + case Oloaduw: + return 4; + case Oload: + return (cls == Kl || cls == Kd) ? 8 : 4; + default: + return 0; + } +} + +bool is_int_reduce_op(int op) { + return op == Oadd || op == Omul || op == Oand || op == Oor || op == Oxor; +} + +int64_t identity_for(int op) { + switch (op) { + case Oadd: + case Oor: + case Oxor: + return 0; + case Omul: + return 1; + case Oand: + return -1; + default: + return 0; + } +} + +bool is_int_cls(int cls) { + return cls == Kw || cls == Kl; +} + +struct Match { + PointerIV iv_p; // pointer IV + Phi* phi_p; // pointer phi + Phi* phi_s; // accumulator phi + Ref s_init; // accumulator init (preheader side) + Ref s_next; // accumulator next (latch side, = s_op.to) + Ins* load_ins; // load inside body + Ins* op_ins; // reduce op inside body (s.next = OP s, v) + Ins* p_inc_ins; // pointer increment inside body + Ref end; // end ref for exit cmp + Blk* after; // exit block + int op; // op_ins->op + int s_cls; // accumulator and op cls + int load_op; // load_ins->op + int load_cls; // load_ins->cls +}; + +// Check latch body. +bool scan_body(Fn* /*fn*/, const Loop& L, const PointerIV& iv_p, Phi* phi_s, Match* m) { + m->load_ins = nullptr; + m->op_ins = nullptr; + m->p_inc_ins = nullptr; + + Ref p = iv_p.phi_to; + Ref s = phi_s->to; + Ref s_next = Ref::none(), p_next = iv_p.next; + for (uint k = 0; k < phi_s->narg; k++) + if (phi_s->blk[k] == L.latch) + s_next = phi_s->arg[k]; + if (rtype(s_next) != RTmp) + return false; + + for (Blk* bb : L.body) { + for (uint k = 0; k < bb->nins; k++) { + Ins* i = &bb->ins[k]; + int op = i->op; + // header allows exit cmp (guaranteed unique by check_exit_cmp) + if (bb == L.header && req(i->to, L.header->jmp.arg)) + continue; + if (op == Onop) + continue; + if (op == Ocall || op == Oalloc4 || op == Oalloc8 || op == Oalloc16 || op == Ovaarg || + op == Ovastart || op == Odbgloc || op == Osel0 || op == Osel1 || op == Oblit0 || + op == Oblit1 || op == Osalloc || op == Oudiv || op == Ourem || op == Odiv || + op == Orem) + return false; + // pointer increment? + if (req(i->to, p_next)) { + if (m->p_inc_ins) + return false; + m->p_inc_ins = i; + continue; + } + // accumulator increment? + if (req(i->to, s_next)) { + if (m->op_ins) + return false; + if (!is_int_reduce_op(op)) + return false; + if (!is_int_cls(static_cast<int>(i->cls))) + return false; + // Must be OP(s, v) or OP(v, s) + if (!req(i->arg[0], s) && !req(i->arg[1], s)) + return false; + m->op_ins = i; + continue; + } + // Is it a load? + int lb = load_bytes(op, static_cast<int>(i->cls)); + if (lb > 0) { + if (m->load_ins) + return false; + if (!req(i->arg[0], p)) + return false; + m->load_ins = i; + continue; + } + // Reject any other ins (reduction form requires a clean body) + return false; + } + } + if (!m->load_ins || !m->op_ins || !m->p_inc_ins) + return false; + // Loaded value must equal the other operand of the OP + Ref v_ref = m->load_ins->to; + if (!(req(m->op_ins->arg[0], v_ref) || req(m->op_ins->arg[1], v_ref))) + return false; + // class & width + int lb = load_bytes(m->load_ins->op, static_cast<int>(m->load_ins->cls)); + if (lb != iv_p.step) + return false; + if (m->op_ins->cls != m->load_ins->cls) + return false; + m->s_next = s_next; + return true; +} + +bool try_match_loop(Fn* fn, const Loop& L, Match* m) { + if (L.body.size() > 2) + return false; + if (!L.preheader) + return false; + if (L.preheader->jmp.type != Jjmp) + return false; + if (L.preheader->s1 != L.header || L.preheader->s2 != nullptr) + return false; + + // Collect header phis. + PointerIV iv_p{}; + Phi* phi_p = nullptr; + Phi* phi_s = nullptr; + int n_ptr = 0, n_acc = 0; + for (Phi* ph = L.header->phi; ph; ph = ph->link) { + PointerIV cand{}; + if (pick_pointer_iv(fn, L, ph, &cand)) { + iv_p = cand; + phi_p = ph; + n_ptr++; + continue; + } + if (ph->narg == 2 && is_int_cls(static_cast<int>(ph->cls))) { + phi_s = ph; + n_acc++; + continue; + } + return false; // Reject any third type of phi + } + if (n_ptr != 1 || n_acc != 1) + return false; + + // accumulator phi incoming edges + Ref s_init = Ref::none(); + for (uint k = 0; k < phi_s->narg; k++) { + if (phi_s->blk[k] == L.preheader) + s_init = phi_s->arg[k]; + else if (phi_s->blk[k] != L.latch) + return false; + } + if (rtype(s_init) == -1) + return false; + + Ref end_ref; + Blk* after = nullptr; + if (!check_exit_cmp(fn, L, iv_p.phi_to, &end_ref, &after)) + return false; + + if (!scan_body(fn, L, iv_p, phi_s, m)) + return false; + + // accumulator cls == op cls, must match load cls (same bitwidth) + if (phi_s->cls != m->op_ins->cls) + return false; + + if (has_outside_use(fn, L)) { + // Check outside loop uses strictly. + std::unordered_set<uint> loop_def; + for (Blk* bb : L.body) { + for (Phi* ph = bb->phi; ph; ph = ph->link) + if (rtype(ph->to) == RTmp) + loop_def.insert(ph->to.val); + for (uint k = 0; k < bb->nins; k++) + if (rtype(bb->ins[k].to) == RTmp) + loop_def.insert(bb->ins[k].to.val); + } + loop_def.erase(phi_p->to.val); + loop_def.erase(phi_s->to.val); + for (Blk* b = fn->start; b; b = b->link) { + if (L.body_bid.count(b->id)) + continue; + for (Phi* ph = b->phi; ph; ph = ph->link) + for (uint k = 0; k < ph->narg; k++) + if (rtype(ph->arg[k]) == RTmp && loop_def.count(ph->arg[k].val)) + return false; + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + for (int a = 0; a < 2; a++) + if (rtype(i->arg[a]) == RTmp && loop_def.count(i->arg[a].val)) + return false; + } + if (rtype(b->jmp.arg) == RTmp && loop_def.count(b->jmp.arg.val)) + return false; + if (rtype(b->jmp.arg2) == RTmp && loop_def.count(b->jmp.arg2.val)) + return false; + } + } + + m->iv_p = iv_p; + m->phi_p = phi_p; + m->phi_s = phi_s; + m->s_init = s_init; + m->end = end_ref; + m->after = after; + m->op = m->op_ins->op; + m->s_cls = m->op_ins->cls; + m->load_op = m->load_ins->op; + m->load_cls = static_cast<int>(m->load_ins->cls); + return true; +} + +Phi* make_phi(Ctx* ctx, int cls, Ref to, Ref arg_pre, Blk* pre, Ref arg_back, Blk* back) { + Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + p->cls = static_cast<short>(cls); + p->visit = 0; + p->to = to; + p->narg = 2; + p->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn)); + p->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn)); + p->arg[0] = arg_pre; + p->blk[0] = pre; + p->arg[1] = arg_back; + p->blk[1] = back; + p->link = nullptr; + return p; +} + +static int rd_seq = 0; + +void execute(Ctx* ctx, Fn* fn, const Loop& L, const Match& m) { + int K = kUnrollK; + int S = static_cast<int>(m.iv_p.step); + int KS = K * S; + int my_seq = rd_seq++; + + Blk* pre = L.preheader; + Blk* tail_hdr = L.header; + int s_cls = m.s_cls; + int load_op = m.load_op; + int load_cls = m.load_cls; + int op = m.op; + + // Append size / size_main / end_main to the end of preheader + Ref size_ref = newtmp(ctx, "rd.size", Kl, fn); + Ref size_main_ref = newtmp(ctx, "rd.szm", Kl, fn); + Ref end_main_ref = newtmp(ctx, "rd.endm", Kl, fn); + + Ins sub_i{}; + sub_i.op = Osub; + sub_i.cls = Kl; + sub_i.to = size_ref; + sub_i.arg[0] = m.end; + sub_i.arg[1] = m.iv_p.base; + addins(ctx, &pre->ins, &pre->nins, &sub_i); + + Ins and_i{}; + and_i.op = Oand; + and_i.cls = Kl; + and_i.to = size_main_ref; + and_i.arg[0] = size_ref; + and_i.arg[1] = getcon(ctx, -static_cast<int64_t>(KS), fn); + addins(ctx, &pre->ins, &pre->nins, &and_i); + + Ins add_i{}; + add_i.op = Oadd; + add_i.cls = Kl; + add_i.to = end_main_ref; + add_i.arg[0] = m.iv_p.base; + add_i.arg[1] = size_main_ref; + addins(ctx, &pre->ins, &pre->nins, &add_i); + + // Create main_header / main_body / main_join + Blk* mh = newblk(ctx); + mh->name = strf(ctx, PFn, "rd%d.mh", my_seq); + mh->id = fn->nblk++; + + Blk* mb = newblk(ctx); + mb->name = strf(ctx, PFn, "rd%d.mb", my_seq); + mb->id = fn->nblk++; + + Blk* mj = newblk(ctx); + mj->name = strf(ctx, PFn, "rd%d.mj", my_seq); + mj->id = fn->nblk++; + + // Pre-generate names (main_header phi.to, etc.) + Ref pm_ref = newtmp(ctx, "rd.pm", Kl, fn); + Ref pm_next_ref = newtmp(ctx, "rd.pm.n", Kl, fn); + std::vector<Ref> s_refs(static_cast<size_t>(K)), s_next_refs(static_cast<size_t>(K)); + for (size_t i = 0; i < static_cast<size_t>(K); i++) { + s_refs[i] = newtmp(ctx, "rd.s", s_cls, fn); + s_next_refs[i] = newtmp(ctx, "rd.s.n", s_cls, fn); + } + + // Identity ref + Ref ident_ref = getcon(ctx, identity_for(op), fn); + + // main_header phi chain: pm + s0..K-1 + Phi* phi_pm = make_phi(ctx, Kl, pm_ref, m.iv_p.base, pre, pm_next_ref, mb); + mh->phi = phi_pm; + Phi* tail_phi = phi_pm; + for (size_t i = 0; i < static_cast<size_t>(K); i++) { + Ref init = (i == 0) ? m.s_init : ident_ref; + Phi* ph = make_phi(ctx, s_cls, s_refs[i], init, pre, s_next_refs[i], mb); + tail_phi->link = ph; + tail_phi = ph; + } + + // main_header cmp + jnz + Ref cmpm_ref = newtmp(ctx, "rd.cmpm", Kw, fn); + Ins cmpm_i{}; + cmpm_i.op = Ocultl; + cmpm_i.cls = Kw; + cmpm_i.to = cmpm_ref; + cmpm_i.arg[0] = pm_ref; + cmpm_i.arg[1] = end_main_ref; + addins(ctx, &mh->ins, &mh->nins, &cmpm_i); + mh->jmp.type = Jjnz; + mh->jmp.arg = cmpm_ref; + mh->jmp.arg2 = Ref::none(); + mh->s1 = mb; + mh->s2 = mj; + + // main_body: K loads + K ops + 1 pointer step + std::vector<Ref> v_refs(static_cast<size_t>(K)); + for (size_t i = 0; i < static_cast<size_t>(K); i++) { + Ref addr; + if (i == 0) { + addr = pm_ref; + } else { + addr = newtmp(ctx, "rd.adr", Kl, fn); + Ins ai{}; + ai.op = Oadd; + ai.cls = Kl; + ai.to = addr; + ai.arg[0] = pm_ref; + ai.arg[1] = getcon(ctx, static_cast<int64_t>(i) * S, fn); + addins(ctx, &mb->ins, &mb->nins, &ai); + } + v_refs[i] = newtmp(ctx, "rd.v", load_cls, fn); + Ins li{}; + li.op = static_cast<uint>(load_op); + li.cls = static_cast<uint>(load_cls) & 3u; + li.to = v_refs[i]; + li.arg[0] = addr; + li.arg[1] = Ref::none(); + addins(ctx, &mb->ins, &mb->nins, &li); + } + for (size_t i = 0; i < static_cast<size_t>(K); i++) { + Ins oi{}; + oi.op = static_cast<uint>(op); + oi.cls = static_cast<uint>(s_cls) & 3u; + oi.to = s_next_refs[i]; + oi.arg[0] = s_refs[i]; + oi.arg[1] = v_refs[i]; + addins(ctx, &mb->ins, &mb->nins, &oi); + } + Ins pn_i{}; + pn_i.op = Oadd; + pn_i.cls = Kl; + pn_i.to = pm_next_ref; + pn_i.arg[0] = pm_ref; + pn_i.arg[1] = getcon(ctx, static_cast<int64_t>(KS), fn); + addins(ctx, &mb->ins, &mb->nins, &pn_i); + mb->jmp.type = Jjmp; + mb->jmp.arg = Ref::none(); + mb->jmp.arg2 = Ref::none(); + mb->s1 = mh; + mb->s2 = nullptr; + + // main_join: log2(K) tree reduction + Ref s01_ref = newtmp(ctx, "rd.s01", s_cls, fn); + Ref s23_ref = newtmp(ctx, "rd.s23", s_cls, fn); + Ref s_main_ref = newtmp(ctx, "rd.sm", s_cls, fn); + + Ins j1{}; + j1.op = static_cast<uint>(op); + j1.cls = static_cast<uint>(s_cls) & 3u; + j1.to = s01_ref; + j1.arg[0] = s_refs[0]; + j1.arg[1] = s_refs[1]; + addins(ctx, &mj->ins, &mj->nins, &j1); + + Ins j2{}; + j2.op = static_cast<uint>(op); + j2.cls = static_cast<uint>(s_cls) & 3u; + j2.to = s23_ref; + j2.arg[0] = s_refs[2]; + j2.arg[1] = s_refs[3]; + addins(ctx, &mj->ins, &mj->nins, &j2); + + Ins j3{}; + j3.op = static_cast<uint>(op); + j3.cls = static_cast<uint>(s_cls) & 3u; + j3.to = s_main_ref; + j3.arg[0] = s01_ref; + j3.arg[1] = s23_ref; + addins(ctx, &mj->ins, &mj->nins, &j3); + + mj->jmp.type = Jjmp; + mj->jmp.arg = Ref::none(); + mj->jmp.arg2 = Ref::none(); + mj->s1 = tail_hdr; + mj->s2 = nullptr; + + // Change preheader.s1 -> main_header + pre->s1 = mh; + + // Link new blocks. + mj->link = pre->link; + mb->link = mj; + mh->link = mb; + pre->link = mh; + + // Update tail_header phis. + for (Phi* ph = tail_hdr->phi; ph; ph = ph->link) { + for (uint k = 0; k < ph->narg; k++) { + if (ph->blk[k] != pre) + continue; + ph->blk[k] = mj; + if (req(ph->to, m.phi_p->to)) + ph->arg[k] = pm_ref; + else if (req(ph->to, m.phi_s->to)) + ph->arg[k] = s_main_ref; + break; + } + } +} + +} // namespace + +void reduction_unroll(Ctx* ctx, Fn* fn) { + std::vector<Loop> loops; + find_loops(fn, loops); + if (loops.empty()) + return; + + // Inner loops first (smaller body first) + std::sort(loops.begin(), loops.end(), + [](const Loop& a, const Loop& b) { return a.body.size() < b.body.size(); }); + + std::unordered_set<uint> consumed; + for (const Loop& L : loops) { + bool clash = false; + for (Blk* bb : L.body) + if (consumed.count(bb->id)) { + clash = true; + break; + } + if (clash) + continue; + Match m{}; + if (!try_match_loop(fn, L, &m)) + continue; + execute(ctx, fn, L, m); + for (Blk* bb : L.body) + consumed.insert(bb->id); + } +} + +} // namespace rcc diff --git a/src/mid/scalar_repl.cc b/src/mid/scalar_repl.cc new file mode 100644 index 0000000..04c0a82 --- /dev/null +++ b/src/mid/scalar_repl.cc @@ -0,0 +1,528 @@ +/* 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 scalar_repl.cc +/// @brief Converts in-loop memory accumulators into a register + phi node. +/// +/// See docs/scalar_repl.md for architectural notes. + +#include "rcc/all.h" + +#include <cstring> +#include <vector> + +namespace rcc { + +namespace { + +Ins make_nop_ins() { + Ins i = {}; + i.op = Onop; + return i; +} + +struct LoopInfo { + Blk* header; + std::vector<Blk*> body; + Blk* preheader; + Blk* latch; + Blk* exit_blk; + Blk* exit_pred; // exit_blk's only pred inside the body +}; + +bool blk_in(Blk* x, const std::vector<Blk*>& body) { + for (Blk* b : body) + if (b == x) + return true; + return false; +} + +bool find_loops(Fn* fn, std::vector<LoopInfo>& out) { + out.clear(); + for (Blk* b = fn->start; b; b = b->link) + b->visit = ~0u; + + for (uint i = 0; i < fn->nblk; i++) { + Blk* hd = fn->rpo[i]; + bool has_back = false; + for (uint k = 0; k < hd->npred; k++) + if (hd->pred[k]->id >= i) { + has_back = true; + break; + } + if (!has_back) + continue; + + LoopInfo L{hd, {hd}, nullptr, nullptr, nullptr, nullptr}; + std::vector<Blk*> stack; + for (uint k = 0; k < hd->npred; k++) + if (hd->pred[k]->id >= i) + stack.push_back(hd->pred[k]); + while (!stack.empty()) { + Blk* n = stack.back(); + stack.pop_back(); + if (n->id < hd->id || n->visit == hd->id) + continue; + n->visit = hd->id; + L.body.push_back(n); + for (uint k = 0; k < n->npred; k++) + stack.push_back(n->pred[k]); + } + + Blk* phdr = nullptr; + Blk* latch = nullptr; + bool unique = true; + for (uint k = 0; k < hd->npred; k++) { + Blk* p = hd->pred[k]; + if (blk_in(p, L.body)) { + if (latch) { + unique = false; + break; + } + latch = p; + } else { + if (phdr) { + unique = false; + break; + } + phdr = p; + } + } + if (!unique || !phdr || !latch) + continue; + L.preheader = phdr; + L.latch = latch; + + Blk* exit_blk = nullptr; + bool exit_unique = true; + for (Blk* b : L.body) { + Blk* succs[2] = {b->s1, b->s2}; + for (Blk* s : succs) { + if (!s || blk_in(s, L.body)) + continue; + if (exit_blk && exit_blk != s) { + exit_unique = false; + break; + } + exit_blk = s; + } + if (!exit_unique) + break; + } + if (!exit_unique || !exit_blk) + continue; + int from_loop = 0; + Blk* exit_pred = nullptr; + for (uint k = 0; k < exit_blk->npred; k++) + if (blk_in(exit_blk->pred[k], L.body)) { + from_loop++; + exit_pred = exit_blk->pred[k]; + } + if (from_loop != 1) + continue; + L.exit_blk = exit_blk; + L.exit_pred = exit_pred; + + out.push_back(std::move(L)); + } + return !out.empty(); +} + +// Recursively check address invariance. +bool is_pure_arith(int op) { + switch (op) { + case Oadd: + case Osub: + case Omul: + case Oand: + case Oor: + case Oxor: + case Oshl: + case Oshr: + case Osar: + case Oextsw: + case Oextuw: + case Oextsh: + case Oextuh: + case Oextsb: + case Oextub: + case Ocopy: + return true; + default: + return false; + } +} + +// Find phi node for t in a blk (t->def doesn't point to phi, must scan). +Phi* find_phi(Fn* fn, Ref r) { + if (rtype(r) != RTmp) + return nullptr; + Tmp* t = &fn->tmp[r.val]; + if (t->bid == ~0u) + return nullptr; + Blk* b = fn->rpo[t->bid]; + for (Phi* p = b->phi; p; p = p->link) + if (req(p->to, r)) + return p; + return nullptr; +} + +// Each incoming phi arg must be invariant. Cannot do optimistic cycle back-edge. +bool phi_invariant(Fn* fn, Phi* p, const LoopInfo& L) { + for (uint k = 0; k < p->narg; k++) { + Ref a = p->arg[k]; + if (req(a, p->to)) + continue; + if (rtype(a) == RCon) + continue; + if (rtype(a) != RTmp) + return false; + Tmp* t = &fn->tmp[a.val]; + if (t->bid == ~0u) + continue; + if (blk_in(fn->rpo[t->bid], L.body)) + return false; + } + return true; +} + +bool addr_invariant_rec(Fn* fn, Ref addr, const LoopInfo& L, int depth) { + if (rtype(addr) == RCon) + return true; + if (rtype(addr) != RTmp) + return false; + Tmp* t = &fn->tmp[addr.val]; + if (t->bid == ~0u) + return true; + if (!blk_in(fn->rpo[t->bid], L.body)) + return true; + if (depth <= 0) + return false; + if (Phi* p = find_phi(fn, addr)) + return phi_invariant(fn, p, L); + Ins* d = t->def; + if (!d || !is_pure_arith(static_cast<int>(d->op))) + return false; + if (!addr_invariant_rec(fn, d->arg[0], L, depth - 1)) + return false; + if (!req(d->arg[1], Ref::none()) && !addr_invariant_rec(fn, d->arg[1], L, depth - 1)) + return false; + return true; +} + +bool addr_invariant(Fn* fn, Ref addr, const LoopInfo& L) { + return addr_invariant_rec(fn, addr, L, 8); +} + +// Structural equivalence: two Ref chains compute the same value. +bool addr_same_value(Fn* fn, Ref a, Ref b, int depth = 8) { + if (req(a, b)) + return true; + if (rtype(a) == RCon && rtype(b) == RCon) { + Con* ca = &fn->con[a.val]; + Con* cb = &fn->con[b.val]; + if (ca->type != cb->type) + return false; + if (ca->type == Con::CAddr && !symeq(ca->sym, cb->sym)) + return false; + return ca->bits.i == cb->bits.i; + } + if (rtype(a) != RTmp || rtype(b) != RTmp) + return false; + if (depth <= 0) + return false; + Tmp* ta = &fn->tmp[a.val]; + Tmp* tb = &fn->tmp[b.val]; + Ins* da = ta->def; + Ins* db = tb->def; + if (!da || !db) + return false; + if (da->op != db->op) + return false; + if (!is_pure_arith(static_cast<int>(da->op))) + return false; + if (!addr_same_value(fn, da->arg[0], db->arg[0], depth - 1)) + return false; + if (req(da->arg[1], Ref::none()) && req(db->arg[1], Ref::none())) + return true; + return addr_same_value(fn, da->arg[1], db->arg[1], depth - 1); +} + +// Clone invariant address chain to preheader. +Ref clone_invariant_to_preheader(Ctx* ctx, Fn* fn, Ref r, const LoopInfo& L, + std::vector<std::pair<uint, Ref>>& cache, int depth = 8) { + if (rtype(r) == RCon) + return r; + if (rtype(r) != RTmp) + return Ref::none(); + Tmp* t = &fn->tmp[r.val]; + if (t->bid == ~0u || !blk_in(fn->rpo[t->bid], L.body)) + return r; // Already defined outside loop, reuse directly + if (depth <= 0) + return Ref::none(); + for (auto& kv : cache) + if (kv.first == r.val) + return kv.second; + + Phi* p = find_phi(fn, r); + if (p) { + // Look for preheader-side arg in phi.blk[] + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == L.preheader) { + Ref v = clone_invariant_to_preheader(ctx, fn, p->arg[k], L, cache, depth - 1); + if (rtype(v) == -1) + return Ref::none(); + cache.push_back(std::make_pair(static_cast<uint>(r.val), v)); + return v; + } + return Ref::none(); + } + Ins* d = t->def; + if (!d || !is_pure_arith(static_cast<int>(d->op))) + return Ref::none(); + Ref a0 = clone_invariant_to_preheader(ctx, fn, d->arg[0], L, cache, depth - 1); + if (rtype(a0) == -1) + return Ref::none(); + Ref a1 = Ref::none(); + if (!req(d->arg[1], Ref::none())) { + a1 = clone_invariant_to_preheader(ctx, fn, d->arg[1], L, cache, depth - 1); + if (rtype(a1) == -1) + return Ref::none(); + } + Ref nt = newtmp(ctx, "sr.addr", static_cast<int>(d->cls), fn); + Ins ni{}; + ni.op = d->op; + ni.cls = d->cls; + ni.to = nt; + ni.arg[0] = a0; + ni.arg[1] = a1; + addins(ctx, &L.preheader->ins, &L.preheader->nins, &ni); + cache.push_back(std::make_pair(static_cast<uint>(r.val), nt)); + return nt; +} + +// Globally replace all ref==from with to (in phi.arg / ins.arg / jmp.arg / jmp.arg2). +void rewrite_ref(Fn* fn, Ref from, Ref to) { + for (Blk* b = fn->start; b; b = b->link) { + for (Phi* p = b->phi; p; p = p->link) + for (uint k = 0; k < p->narg; k++) + if (req(p->arg[k], from)) + p->arg[k] = to; + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) { + if (req(i->arg[0], from)) + i->arg[0] = to; + if (req(i->arg[1], from)) + i->arg[1] = to; + } + if (req(b->jmp.arg, from)) + b->jmp.arg = to; + if (req(b->jmp.arg2, from)) + b->jmp.arg2 = to; + } +} + +void prepend_ins(Ctx* ctx, Blk* b, const Ins& ins) { + vgrow(ctx, &b->ins, b->nins + 1); + std::memmove(b->ins + 1, b->ins, b->nins * sizeof(Ins)); + b->ins[0] = ins; + b->nins += 1; +} + +// Try one promotion on L. Returns true on hit; outer loop recomputes use/dom/alias. +bool try_promote(Ctx* ctx, Fn* fn, const LoopInfo& L) { + for (Blk* b : L.body) { + for (Ins* iload = b->ins; iload < &b->ins[b->nins]; iload++) { + if (!isload(iload->op)) + continue; + Ref addr = iload->arg[0]; + if (!addr_invariant(fn, addr, L)) + continue; + int sz = loadsz(iload); + + std::vector<Ins*> matched_loads; + Blk* store_blk = nullptr; + Ins* matched_store = nullptr; + int store_count = 0; + bool ok = true; + + // Three-tier classification: structurally identical -> matched, NoAlias -> skip, + // MayAlias -> bail. + auto classify = [&](Ref jaddr, int jsz) -> int { + // ret: 0=matched, 1=skip(NoAlias), 2=bail + if (jsz != sz) + return 2; // Different size cannot be promoted (conservative) + if (addr_same_value(fn, addr, jaddr)) + return 0; + int delta; + int r = alias(addr, 0, sz, jaddr, jsz, &delta, fn); + if (r == NoAlias) + return 1; + if (r == MustAlias && delta == 0) + return 0; + return 2; + }; + + for (Blk* bb : L.body) { + for (Ins* j = bb->ins; j < &bb->ins[bb->nins]; j++) { + if (j->op == Ocall || j->op == Oblit0 || j->op == Oblit1) { + ok = false; + break; + } + if (isload(j->op)) { + int jsz = loadsz(j); + // Different size but NoAlias can still be skipped + if (jsz != sz) { + int delta; + int r = alias(addr, 0, sz, j->arg[0], jsz, &delta, fn); + if (r != NoAlias) { + ok = false; + break; + } + continue; + } + int c = classify(j->arg[0], jsz); + if (c == 0) + matched_loads.push_back(j); + else if (c == 2) { + ok = false; + break; + } + } else if (isstore(j->op)) { + int jsz = storesz(j); + if (jsz != sz) { + int delta; + int r = alias(addr, 0, sz, j->arg[1], jsz, &delta, fn); + if (r != NoAlias) { + ok = false; + break; + } + continue; + } + int c = classify(j->arg[1], jsz); + if (c == 0) { + store_count++; + if (store_count > 1) { + ok = false; + break; + } + matched_store = j; + store_blk = bb; + } else if (c == 2) { + ok = false; + break; + } + } + } + if (!ok) + break; + } + if (!ok || store_count != 1) + continue; + // Store must be must-execute. + if (!dom(store_blk, L.latch)) + continue; + if (!dom(store_blk, L.exit_pred)) + continue; + + int cls = static_cast<int>(iload->cls); + + // Clone address chain to preheader, then issue load. + std::vector<std::pair<uint, Ref>> addr_cache; + Ref pre_addr = clone_invariant_to_preheader(ctx, fn, addr, L, addr_cache); + if (rtype(pre_addr) == -1) + continue; // Clone failed (rare corner case) + Ref init_ref = newtmp(ctx, "sr.init", cls, fn); + Ins pre_ld{}; + pre_ld.op = iload->op; + pre_ld.cls = static_cast<uint>(cls); + pre_ld.to = init_ref; + pre_ld.arg[0] = pre_addr; + pre_ld.arg[1] = Ref::none(); + addins(ctx, &L.preheader->ins, &L.preheader->nins, &pre_ld); + + // Insert phi at header top. + Ref back_val = matched_store->arg[0]; + Ref phi_ref = newtmp(ctx, "sr.phi", cls, fn); + Phi* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + std::memset(p, 0, sizeof *p); + p->to = phi_ref; + p->cls = static_cast<short>(cls); + p->narg = 2; + p->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn)); + p->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn)); + p->arg[0] = init_ref; + p->blk[0] = L.preheader; + p->arg[1] = back_val; + p->blk[1] = L.latch; + p->link = L.header->phi; + L.header->phi = p; + + // Change all matched_loads def tmps to phi_ref; original loads become Onop. + for (Ins* ml : matched_loads) { + rewrite_ref(fn, ml->to, phi_ref); + *ml = make_nop_ins(); + } + + // Sink store at top of exit_blk. + Ins sink{}; + sink.op = matched_store->op; + sink.cls = matched_store->cls; + sink.to = Ref::none(); + sink.arg[0] = phi_ref; + sink.arg[1] = pre_addr; + prepend_ins(ctx, L.exit_blk, sink); + + // Original store becomes Onop. + *matched_store = make_nop_ins(); + + return true; + } + } + return false; +} + +} // namespace + +void scalar_repl(Ctx* ctx, Fn* fn) { + int total = 0; + for (int iter = 0; iter < 16; iter++) { + std::vector<LoopInfo> loops; + if (!find_loops(fn, loops)) + break; + bool changed = false; + for (const LoopInfo& L : loops) { + if (try_promote(ctx, fn, L)) { + changed = true; + total++; + break; + } + } + if (!changed) + break; + filluse(ctx, fn); + filldom(fn); + fillalias(fn); + } + if (debug['L']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "\n> scalar-repl promoted %d location(s) in %s\n", total, fn->name); + } +} + +} // namespace rcc diff --git a/src/mid/sccp.cc b/src/mid/sccp.cc new file mode 100644 index 0000000..537773a --- /dev/null +++ b/src/mid/sccp.cc @@ -0,0 +1,449 @@ +/* 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 sccp.cc +/// @brief Sparse Conditional Constant Propagation (Wegman-Zadeck). +/// +/// See docs/sccp.md for architectural notes. + +#include "rcc/all.h" + +#include <cassert> +#include <cstdio> +#include <set> +#include <utility> +#include <vector> + +namespace rcc { + +namespace { + +enum LatState : uchar { Top = 0, Const = 1, Bot = 2 }; + +struct Lat { + LatState state; + Ref con; // Only valid when state==Const, type==RCon +}; + +constexpr Lat kTop = {Top, {RTmp, 0}}; +constexpr Lat kBot = {Bot, {RTmp, 0}}; + +bool con_eq(Fn* fn, Ref a, Ref b) { + if (req(a, b)) + return true; + if (rtype(a) != RCon || rtype(b) != RCon) + return false; + if (a.val >= static_cast<uint>(fn->ncon) || b.val >= static_cast<uint>(fn->ncon)) + return false; + Con* ca = &fn->con[a.val]; + Con* cb = &fn->con[b.val]; + if (ca->type != cb->type) + return false; + if (ca->type == Con::CAddr && !symeq(ca->sym, cb->sym)) + return false; + return ca->bits.i == cb->bits.i && ca->flt == cb->flt; +} + +bool lat_eq(Fn* fn, Lat a, Lat b) { + if (a.state != b.state) + return false; + if (a.state == Const) + return con_eq(fn, a.con, b.con); + return true; +} + +// Check if unfoldable type. +bool op_is_unfoldable(uint op) { + if (isstore(op)) + return true; // store does not define tmp, but safe + if (isload(op)) + return true; + if (isalloc(op)) + return true; + if (ispar(op)) + return true; + if (isarg(op)) + return true; + if (op == Ocall) + return true; + if (op == Oblit0 || op == Oblit1) + return true; + if (op == Ovastart || op == Ovaarg) + return true; + if (op == Odbgloc) + return true; + if (op == Osel0 || op == Osel1) + return true; + if (op == Onop) + return true; + return false; +} + +struct Sccp { + Ctx* ctx; + Fn* fn; + std::vector<Lat> lat; // ntmp + std::vector<char> blk_reachable; // nblk + // edge_exec pairs. + std::set<std::pair<uint, uint>> edge_exec; + std::vector<Blk*> cfg_wl; + std::vector<uint> ssa_wl; // tmp val queue + + Lat arg_lat(Ref r) { + if (rtype(r) == RCon) + return {Const, r}; + if (rtype(r) == RTmp) { + if (r.val >= lat.size()) + return kBot; + return lat[r.val]; + } + // RSlot / RType / RCall / RMem etc: do not participate in constant propagation + return kBot; + } + + void set_lat(uint t, Lat nv) { + if (t >= lat.size() || t == 0) + return; + if (lat_eq(fn, lat[t], nv)) + return; + // lattice monotonically descends: Top -> Const -> Bot + lat[t] = nv; + ssa_wl.push_back(t); + } + + // Evaluate instruction using lattice. + Lat eval_ins(Ins* i) { + if (rtype(i->to) != RTmp) + return kBot; + if (op_is_unfoldable(i->op)) + return kBot; + + Lat a0 = arg_lat(i->arg[0]); + Lat a1; + if (req(i->arg[1], Ref::none())) + a1 = {Const, Ref::con_z()}; // phantom arg[1] for unary op + else + a1 = arg_lat(i->arg[1]); + + // Ocopy special case: lattice equals arg[0] + if (i->op == Ocopy) + return a0; + + if (a0.state == Top || a1.state == Top) + return kTop; + if (a0.state == Bot || a1.state == Bot) + return kBot; + + // Both sides are Const: try to fold + Ins tmp = *i; + tmp.arg[0] = a0.con; + tmp.arg[1] = a1.con; + Ref r = foldref(ctx, fn, &tmp); + if (req(r, Ref::none())) + return kBot; + return {Const, r}; + } + + Lat eval_phi(Phi* p, Blk* in_blk) { + Lat acc = kTop; + for (uint k = 0; k < p->narg; k++) { + Blk* pred = p->blk[k]; + if (!pred) + continue; + if (!edge_exec.count({pred->id, in_blk->id})) + continue; // Incoming edge unreachable - skip + Lat a = arg_lat(p->arg[k]); + if (a.state == Bot) + return kBot; + if (a.state == Top) + continue; + // Const + if (acc.state == Top) { + acc = a; + } else if (!con_eq(fn, acc.con, a.con)) { + return kBot; + } + } + return acc; + } + + // Activate CFG edge. + void activate_edge(Blk* pred, Blk* succ) { + if (!succ) + return; + auto key = std::make_pair(pred->id, succ->id); + if (!edge_exec.insert(key).second) + return; // Already activated + // Enqueue succ if needed. + cfg_wl.push_back(succ); + if (!blk_reachable[succ->id]) + blk_reachable[succ->id] = 1; + } + + void eval_jmp(Blk* b) { + int t = b->jmp.type; + if (isret(t) || t == Jhlt) + return; + if (t == Jjmp) { + activate_edge(b, b->s1); + return; + } + if (INRANGE(t, Jjf, Jjf1)) { + // Conditional jumps. + activate_edge(b, b->s1); + activate_edge(b, b->s2); + return; + } + if (t == Jjnz) { + Lat c = arg_lat(b->jmp.arg); + if (c.state == Top) + return; + if (c.state == Bot) { + activate_edge(b, b->s1); + activate_edge(b, b->s2); + return; + } + // Const: check if non-zero + if (rtype(c.con) != RCon || c.con.val >= static_cast<uint>(fn->ncon)) { + activate_edge(b, b->s1); + activate_edge(b, b->s2); + return; + } + Con* cc = &fn->con[c.con.val]; + if (cc->type != Con::CBits) { + // CAddr is treated as truthy (non-null address) + activate_edge(b, b->s1); + return; + } + if (cc->bits.i != 0) + activate_edge(b, b->s1); + else + activate_edge(b, b->s2); + return; + } + // Other unknown jmp types: activate both sides + activate_edge(b, b->s1); + activate_edge(b, b->s2); + } + + void visit_block(Blk* b) { + // Re-eval phis. + for (Phi* p = b->phi; p; p = p->link) { + if (rtype(p->to) != RTmp) + continue; + Lat nv = eval_phi(p, b); + set_lat(p->to.val, nv); + } + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + if (rtype(i->to) != RTmp) + continue; + Lat nv = eval_ins(i); + set_lat(i->to.val, nv); + } + eval_jmp(b); + } + + // Process tmp uses. + void visit_uses(uint t) { + if (t >= static_cast<uint>(fn->ntmp)) + return; + Tmp* tt = &fn->tmp[t]; + for (uint u = 0; u < tt->nuse; u++) { + Use& uu = tt->use[u]; + if (uu.bid >= blk_reachable.size()) + continue; + if (!blk_reachable[uu.bid]) + continue; // In unreachable block, ignore for now + Blk* ub = fn->rpo[uu.bid]; + if (uu.type == Use::UPhi) { + Phi* p = uu.u.phi; + if (rtype(p->to) == RTmp) { + Lat nv = eval_phi(p, ub); + set_lat(p->to.val, nv); + } + } else if (uu.type == Use::UIns) { + Ins* i = uu.u.ins; + if (rtype(i->to) == RTmp) { + Lat nv = eval_ins(i); + set_lat(i->to.val, nv); + } + } else if (uu.type == Use::UJmp) { + eval_jmp(ub); + } + } + } + + void solve() { + // Init state. + lat.assign(static_cast<size_t>(fn->ntmp), kTop); + blk_reachable.assign(static_cast<size_t>(fn->nblk), 0); + edge_exec.clear(); + cfg_wl.clear(); + ssa_wl.clear(); + + blk_reachable[fn->start->id] = 1; + cfg_wl.push_back(fn->start); + // Enqueue start. + + while (!cfg_wl.empty() || !ssa_wl.empty()) { + // Process worklists. + while (!cfg_wl.empty()) { + Blk* b = cfg_wl.back(); + cfg_wl.pop_back(); + visit_block(b); + } + while (!ssa_wl.empty()) { + uint t = ssa_wl.back(); + ssa_wl.pop_back(); + visit_uses(t); + } + } + } + + // Apply: Commit lattice to IR. + Ref maybe_const(Ref r) { + if (rtype(r) != RTmp) + return r; + if (r.val >= lat.size()) + return r; + Lat& l = lat[r.val]; + if (l.state == Const) + return l.con; + return r; + } + + int apply() { + int n_ins = 0, n_phi = 0, n_jmp = 0, n_arg = 0; + for (Blk* b = fn->start; b; b = b->link) { + if (b->id >= blk_reachable.size() || !blk_reachable[b->id]) + continue; + + // Phi: Remove incoming edges from unreachable preds; replace arg operands + for (Phi* p = b->phi; p; p = p->link) { + uint w = 0; + for (uint k = 0; k < p->narg; k++) { + Blk* pred = p->blk[k]; + if (!pred || !edge_exec.count({pred->id, b->id})) { + // This incoming edge was not activated during SCCP -> unreachable -> delete + continue; + } + Ref na = maybe_const(p->arg[k]); + if (!req(na, p->arg[k])) + n_arg++; + if (w != k) { + p->arg[w] = na; + p->blk[w] = pred; + } else { + p->arg[w] = na; + } + w++; + } + p->narg = w; + + // Keep constant phi.to for DCE. + if (rtype(p->to) == RTmp && p->to.val < lat.size() && lat[p->to.val].state == Const) + n_phi++; + } + + // Ins: Change Const def to Ocopy; replace arg operands + for (uint k = 0; k < b->nins; k++) { + Ins* i = &b->ins[k]; + bool is_const_def = (rtype(i->to) == RTmp && i->to.val < lat.size() && + lat[i->to.val].state == Const); + if (is_const_def && i->op != Ocopy && !op_is_unfoldable(i->op)) { + Ref cr = lat[i->to.val].con; + i->op = Ocopy; + i->arg[0] = cr; + i->arg[1] = Ref::none(); + n_ins++; + continue; + } + // Otherwise just replace args + Ref na0 = maybe_const(i->arg[0]); + Ref na1 = maybe_const(i->arg[1]); + if (!req(na0, i->arg[0])) { + i->arg[0] = na0; + n_arg++; + } + if (!req(na1, i->arg[1])) { + i->arg[1] = na1; + n_arg++; + } + } + + // jmp: Jjnz cond Const -> Jjmp; replace args + if (b->jmp.type == Jjnz) { + Lat c = arg_lat(b->jmp.arg); + if (c.state == Const && rtype(c.con) == RCon && + c.con.val < static_cast<uint>(fn->ncon)) { + Con* cc = &fn->con[c.con.val]; + if (cc->type == Con::CBits) { + // Choose succ + Blk* taken = (cc->bits.i != 0) ? b->s1 : b->s2; + b->jmp.type = Jjmp; + b->jmp.arg = Ref::none(); + b->s1 = taken; + b->s2 = nullptr; + n_jmp++; + } else if (cc->type == Con::CAddr) { + // Address is always truthy + b->jmp.type = Jjmp; + b->jmp.arg = Ref::none(); + b->s1 = b->s1; + b->s2 = nullptr; + n_jmp++; + } + } else { + Ref na = maybe_const(b->jmp.arg); + if (!req(na, b->jmp.arg)) { + b->jmp.arg = na; + n_arg++; + } + } + } else if (!isret(b->jmp.type) && b->jmp.type != Jhlt && b->jmp.type != Jjmp) { + Ref na = maybe_const(b->jmp.arg); + if (!req(na, b->jmp.arg)) { + b->jmp.arg = na; + n_arg++; + } + } + } + + if (debug['S']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> SCCP %s: const_ins=%d const_phi=%d jjnz_collapse=%d arg_subst=%d\n", + fn->name, n_ins, n_phi, n_jmp, n_arg); + } + return n_ins + n_phi + n_jmp + n_arg; + } +}; + +} // namespace + +void sccp(Ctx* ctx, Fn* fn) { + // Prerequisites: filluse and rpo ready. + Sccp s; + s.ctx = ctx; + s.fn = fn; + s.solve(); + s.apply(); +} + +} // namespace rcc diff --git a/src/mid/ssa.cc b/src/mid/ssa.cc new file mode 100644 index 0000000..75e2773 --- /dev/null +++ b/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/src/mid/storemrg.cc b/src/mid/storemrg.cc new file mode 100644 index 0000000..65b4bd4 --- /dev/null +++ b/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 diff --git a/src/mid/tre.cc b/src/mid/tre.cc new file mode 100644 index 0000000..6345647 --- /dev/null +++ b/src/mid/tre.cc @@ -0,0 +1,214 @@ +/* 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 tre.cc +/// @brief Tail Recursion Elimination (TRE) + +#include "rcc/all.h" + +#include <cstring> +#include <vector> + +namespace rcc { + +namespace { + +struct Site { + Blk* b; + Ins* call; // Points to Ocall in b->ins + Ins* arg_first; // [arg_first..call) is Oarg sequence + std::vector<Ref> args; +}; + +bool is_self_call(Fn* fn, Ins* call, uint32_t self_id) { + if (call->op != Ocall) + return false; + Ref tgt = call->arg[0]; + if (rtype(tgt) != RCon) + return false; + Con* c = &fn->con[tgt.val]; + if (c->type != Con::CAddr) + return false; + if (c->sym.type != Sym::SGlo) + return false; + return c->sym.id == self_id; +} + +// Locate Ocall and arg-group start in b. +bool collect_site(Fn* fn, Blk* b, int npar, uint32_t self_id, Site& out) { + if (b->nins == 0) + return false; + Ins* last = &b->ins[b->nins - 1]; + if (!is_self_call(fn, last, self_id)) + return false; + + // ret form matches call.to + if (b->jmp.type == Jret0) { + if (!req(last->to, Ref::none())) + return false; + } else if (b->jmp.type == Jretw || b->jmp.type == Jretl || b->jmp.type == Jrets || + b->jmp.type == Jretd) { + if (!req(b->jmp.arg, last->to)) + return false; + } else { + return false; + } + + // Verify all instructions before call are Oarg and count matches. + Ins *i0 = nullptr, *i1 = nullptr; + igroup(b, last, &i0, &i1); + if (i1 != last + 1) + return false; + int argc = static_cast<int>(last - i0); + if (argc != npar) + return false; + out.b = b; + out.call = last; + out.arg_first = i0; + out.args.clear(); + out.args.reserve(static_cast<size_t>(npar)); + for (Ins* j = i0; j < last; j++) { + if (!isarg(j->op)) + return false; + out.args.push_back(j->arg[0]); + } + return true; +} + +} // namespace + +void tre(Ctx* ctx, Fn* fn) { + if (fn->vararg) + return; + if (fn->name && std::strcmp(fn->name, "main") == 0) + return; + + // exit if any block contains isalloc + for (Blk* b = fn->start; b; b = b->link) + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (isalloc(i->op)) + return; + + // fn->start cannot already have phi + if (fn->start->phi) + return; + + // Collect Opar from start block + std::vector<Ins*> par_inss; + Ins* ie0 = fn->start->ins + fn->start->nins; + for (Ins* i = fn->start->ins; i < ie0 && i->op == Opar; i++) + par_inss.push_back(i); + int npar = static_cast<int>(par_inss.size()); + + // Reject theoretically allowed Opar variants (struct by-value) not used by SysY + for (Ins* i = fn->start->ins; i < ie0; i++) { + if (i->op == Oparc || i->op == Opare) + return; + } + + uint32_t self_id = intern(fn->name); + + // Scan qualified sites + std::vector<Site> sites; + for (Blk* b = fn->start; b; b = b->link) { + if (!isret(b->jmp.type)) + continue; + Site s; + if (collect_site(fn, b, npar, self_id, s)) + sites.push_back(std::move(s)); + } + if (sites.empty()) + return; + + // Transformation + Blk* loop_header = fn->start; + + // Prepare %p_init_i for each Opar, record original %p_i and cls + auto upar = static_cast<size_t>(npar); + std::vector<Ref> par_init_refs(upar); + std::vector<Ref> par_orig_refs(upar); + std::vector<int> par_clses(upar); + for (int i = 0; i < npar; i++) { + size_t ui = static_cast<size_t>(i); + par_orig_refs[ui] = par_inss[ui]->to; + par_clses[ui] = static_cast<int>(par_inss[ui]->cls); + par_init_refs[ui] = newtmp(ctx, "p_init", par_clses[ui], fn); + } + + // Create @tre_entry + Blk* tre_entry = newblk(ctx); + tre_entry->name = strf(ctx, PFn, "tre_entry"); + for (int i = 0; i < npar; i++) { + size_t ui = static_cast<size_t>(i); + Ins ni = *par_inss[ui]; + ni.to = par_init_refs[ui]; + addins(ctx, &tre_entry->ins, &tre_entry->nins, &ni); + } + tre_entry->jmp.type = Jjmp; + tre_entry->jmp.arg = Ref::none(); + tre_entry->s1 = loop_header; + tre_entry->s2 = nullptr; + tre_entry->link = loop_header; + fn->start = tre_entry; + + // Remove head npar Opars from loop_header + int remaining = static_cast<int>(loop_header->nins) - npar; + if (remaining > 0) + std::memmove(loop_header->ins, loop_header->ins + npar, + static_cast<size_t>(remaining) * sizeof(Ins)); + loop_header->nins = static_cast<uint>(remaining); + + // Insert npar phis at loop_header head + int nphi_args = 1 + static_cast<int>(sites.size()); + for (int i = npar - 1; i >= 0; i--) { + size_t ui = static_cast<size_t>(i); + auto* p = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + std::memset(p, 0, sizeof(Phi)); + p->to = par_orig_refs[ui]; + p->cls = static_cast<short>(par_clses[ui]); + p->narg = static_cast<uint>(nphi_args); + p->arg = static_cast<Ref*>(vnew(ctx, static_cast<ulong>(nphi_args), sizeof(Ref), PFn)); + p->blk = static_cast<Blk**>(vnew(ctx, static_cast<ulong>(nphi_args), sizeof(Blk*), PFn)); + p->arg[0] = par_init_refs[ui]; + p->blk[0] = tre_entry; + for (size_t k = 0; k < sites.size(); k++) { + p->arg[1 + k] = sites[k].args[ui]; + p->blk[1 + k] = sites[k].b; + } + p->link = loop_header->phi; + loop_header->phi = p; + } + + // Truncate block at each site and redirect jmp to loop_header + for (auto& s : sites) { + int new_nins = static_cast<int>(s.arg_first - s.b->ins); + s.b->nins = static_cast<uint>(new_nins); + s.b->jmp.type = Jjmp; + s.b->jmp.arg = Ref::none(); + s.b->s1 = loop_header; + s.b->s2 = nullptr; + } + + if (debug['T']) { + fprintf(stderr, "\n>>> after tre (%s) — %zu site(s):\n", fn->name, sites.size()); + printfn(fn, stderr); + } +} + +} // namespace rcc diff --git a/src/mid/unroll.cc b/src/mid/unroll.cc new file mode 100644 index 0000000..4e5462c --- /dev/null +++ b/src/mid/unroll.cc @@ -0,0 +1,847 @@ +/* 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 unroll.cc +/// @brief Loop Unrolling. +/// +/// See docs/unroll.md for architectural notes. + +#include "rcc/all.h" + +#include <cassert> +#include <cstdio> +#include <cstring> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +constexpr int kFullUnrollTripMax = 16; +constexpr int kFullUnrollBudget = 256; +constexpr int kUnrollFixpointIters = 4; +constexpr int kPartialFactor = 4; +constexpr int kPartialMaxBodyIns = 64; // F * body_ins <= 256 total instruction budget + +// Loop Identification + +struct LoopShape { + Blk* header; + Blk* preheader; + Blk* latch; + Blk* exit_bb; + uint preheader_idx; // index in header->pred where preheader sits + uint latch_idx; // index in header->pred where latch sits + std::vector<Blk*> body; // includes header, latch +}; + +// Helper: find_simple_loops (see docs/unroll.md) +std::vector<LoopShape> find_simple_loops(Fn* fn) { + std::vector<LoopShape> out; + + for (Blk* b = fn->start; b; b = b->link) + b->visit = ~0u; + + for (uint i = 0; i < fn->nblk; i++) { + Blk* hd = fn->rpo[i]; + + // single back-edge + int back_cnt = 0; + uint latch_pred_idx = 0; + uint pre_pred_idx = 0; + int pre_cnt = 0; + for (uint k = 0; k < hd->npred; k++) { + if (hd->pred[k]->id >= i && hd->pred[k]->id != ~0u) { + back_cnt++; + latch_pred_idx = k; + } else { + pre_cnt++; + pre_pred_idx = k; + } + } + if (back_cnt != 1 || pre_cnt != 1) + continue; // Require single latch + single preheader + + Blk* latch = hd->pred[latch_pred_idx]; + Blk* preheader = hd->pred[pre_pred_idx]; + + // Reverse DFS to collect body + std::vector<Blk*> body = {hd}; + std::vector<Blk*> stack = {latch}; + hd->visit = hd->id; + while (!stack.empty()) { + Blk* n = stack.back(); + stack.pop_back(); + if (n->visit == hd->id) + continue; + n->visit = hd->id; + body.push_back(n); + for (uint k = 0; k < n->npred; k++) + stack.push_back(n->pred[k]); + } + + // single exit (see docs/unroll.md) + if (hd->jmp.type != Jjnz) + continue; + Blk* exit_bb = hd->s2; + if (!exit_bb) + continue; + auto in_body = [&](Blk* x) -> bool { + for (Blk* bb : body) + if (bb == x) + return true; + return false; + }; + if (in_body(exit_bb)) + continue; + if (!in_body(hd->s1)) + continue; + bool ok = true; + for (Blk* bb : body) { + if (bb == hd) + continue; // header already checked + auto check_succ = [&](Blk* s) -> bool { + if (!s) + return true; + return in_body(s); + }; + if (!check_succ(bb->s1) || !check_succ(bb->s2)) { + ok = false; + break; + } + // Forbid Ocall / nested ret jmp in body + if (isret(bb->jmp.type)) { + ok = false; + break; + } + for (Ins* ii = bb->ins; ii < &bb->ins[bb->nins]; ii++) { + if (ii->op == Ocall) { + ok = false; + break; + } + } + if (!ok) + break; + } + if (!ok) + continue; + // header itself cannot have Ocall either (although Jjnz is not ret) + for (Ins* ii = hd->ins; ii < &hd->ins[hd->nins]; ii++) { + if (ii->op == Ocall) { + ok = false; + break; + } + } + if (!ok) + continue; + + LoopShape L; + L.header = hd; + L.preheader = preheader; + L.latch = latch; + L.exit_bb = exit_bb; + L.preheader_idx = pre_pred_idx; + L.latch_idx = latch_pred_idx; + L.body = std::move(body); + out.push_back(std::move(L)); + } + + return out; +} + +// Induction Variable Detection + Trip Count + +struct Induction { + int64_t init; + int64_t step; // > 0 + int64_t bound; // Valid only when bound_const is true + Ref bound_ref; // cmp's arg[1]: RCon or loop-invariant RTmp + bool bound_const; // bound_ref is compile-time constant + int cmp_op; // Ocsltw/Ocsltl/Ocslew/Ocslel + int cmp_cls; // cls of cmp ins (for copying guard in partial unroll) + Ref iv_ref; // induction variable header phi.to (used to find corresponding new phi in + // main.head) + int64_t trip; // Calculated when bound_const is true; -1 for runtime bounds +}; + +bool ref_is_const(Fn* fn, Ref r, int64_t& out_val) { + if (rtype(r) != RCon) + return false; + if (r.val >= fn->ncon) + return false; + Con* c = &fn->con[r.val]; + if (c->type != Con::CBits) + return false; + if (c->flt) + return false; + out_val = c->bits.i; + return true; +} + +bool is_one_of(int op, std::initializer_list<int> opts) { + for (int o : opts) + if (op == o) + return true; + return false; +} + +// Find def of cmp. Returns Ins* or nullptr. +Ins* find_def_ins(Fn* fn, Ref r) { + if (rtype(r) != RTmp) + return nullptr; + if (r.val >= fn->ntmp) + return nullptr; + return fn->tmp[r.val].def; +} + +// Check if ref is defined outside the loop (including constants). Uses Tmp.bid (set by filluse). +bool def_outside_body(Fn* fn, const LoopShape& L, Ref r) { + if (rtype(r) != RTmp) + return true; // Constants / non-tmps are loop-invariant + uint bid = fn->tmp[r.val].bid; + for (Blk* bb : L.body) + if (bb->id == bid) + return false; + return true; +} + +// Resolve self-loop phis to their loop-invariant preheader value. +Ref resolve_loop_invariant(Fn* fn, const LoopShape& L, Ref r) { + for (int guard = 0; guard < 8; guard++) { + if (def_outside_body(fn, L, r)) + return r; + // Defined inside loop: can only resolve further if it's a self-loop phi in header + if (rtype(r) != RTmp || fn->tmp[r.val].bid != L.header->id) + return Ref::none(); + Phi* p = nullptr; + for (Phi* q = L.header->phi; q; q = q->link) + if (req(q->to, r)) { + p = q; + break; + } + if (!p) + return Ref::none(); // Inside header but not a phi (it's a regular ins) -> true + // variable + Ref pre_arg = Ref::none(); + bool selfish = true; + for (uint k = 0; k < p->narg; k++) { + if (p->blk[k] == L.preheader) + pre_arg = p->arg[k]; + else if (!req(p->arg[k], p->to)) + selfish = false; // Non-self-loop incoming edge -> actually varying + } + if (!selfish || req(pre_arg, Ref::none())) + return Ref::none(); + r = pre_arg; // Resolved one level, continue (preheader value might also be a self-loop + // phi) + } + return Ref::none(); +} + +bool detect_induction(Fn* fn, const LoopShape& L, Induction& out) { + // Header must end with Jjnz, arg = cmp tmp + if (rtype(L.header->jmp.arg) != RTmp) + return false; + Ins* cmp = find_def_ins(fn, L.header->jmp.arg); + if (!cmp) + return false; + if (!is_one_of(cmp->op, {Ocsltw, Ocsltl, Ocslew, Ocslel})) + return false; + + // Parse bound (see docs/unroll.md) + if (rtype(cmp->arg[0]) != RTmp) + return false; + Ref bound_ref = resolve_loop_invariant(fn, L, cmp->arg[1]); + if (req(bound_ref, Ref::none())) + return false; + int64_t bound_v = 0; + bool bound_const = ref_is_const(fn, bound_ref, bound_v); + + // Find matching header phi + Phi* ind_phi = nullptr; + for (Phi* p = L.header->phi; p; p = p->link) { + if (req(p->to, cmp->arg[0])) { + ind_phi = p; + break; + } + } + if (!ind_phi) + return false; + + // Get init (preheader side) and i_next (latch side) + if (ind_phi->narg != L.header->npred) + return false; // Defensive check + Ref init_ref = Ref::none(), i_next_ref = Ref::none(); + for (uint k = 0; k < ind_phi->narg; k++) { + if (ind_phi->blk[k] == L.preheader) + init_ref = ind_phi->arg[k]; + else if (ind_phi->blk[k] == L.latch) + i_next_ref = ind_phi->arg[k]; + } + int64_t init_v; + if (!ref_is_const(fn, init_ref, init_v)) + return false; + if (rtype(i_next_ref) != RTmp) + return false; + + // i_next def must be Oadd(i, step_const) or Oadd(step_const, i) + Ins* next_def = find_def_ins(fn, i_next_ref); + if (!next_def) + return false; + if (next_def->op != Oadd) + return false; + int64_t step_v = 0; + if (req(next_def->arg[0], ind_phi->to) && ref_is_const(fn, next_def->arg[1], step_v)) { + // i_next = i + step + } else if (req(next_def->arg[1], ind_phi->to) && ref_is_const(fn, next_def->arg[0], step_v)) { + // i_next = step + i (commutative) + } else { + return false; + } + if (step_v <= 0) + return false; // Only positive step supported for now + + out.init = init_v; + out.step = step_v; + out.bound = bound_v; + out.bound_ref = bound_ref; + out.bound_const = bound_const; + out.cmp_op = cmp->op; + out.cmp_cls = cmp->cls; + out.iv_ref = ind_phi->to; + + if (!bound_const) { + out.trip = -1; // Runtime bound: only usable for partial unroll + return true; + } + + // trip count depends on cmp_op + int64_t span = bound_v - init_v; + if (cmp->op == Ocslew || cmp->op == Ocslel) + span += 1; + if (span <= 0) { + out.trip = 0; + return true; + } + out.trip = (span + step_v - 1) / step_v; + return true; +} + +int count_body_ins(const LoopShape& L) { + int n = 0; + for (Blk* b : L.body) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (i->op != Onop) + n++; + for (Phi* p = b->phi; p; p = p->link) + (void)p, n++; + } + return n; +} + +// Clone / Unroll + +struct CopyState { + std::unordered_map<uint, Blk*> blk_map; // body blk.id -> clone + std::unordered_map<uint, Ref> tmp_map; // body-defined tmp.val -> clone Ref + Blk* first_blk; // = blk_map[L.header.id] + Blk* last_blk; // = blk_map[L.latch.id] +}; + +Ref remap_tmp(Ref r, const std::unordered_map<uint, Ref>& m) { + if (rtype(r) != RTmp) + return r; + auto it = m.find(r.val); + if (it == m.end()) + return r; + return it->second; +} + +// Deep copy a body block into dst. Discard header phis, map header Jjnz to Jjmp. +void clone_body_blk(Ctx* ctx, const CopyState& st, Blk* src, Blk* dst, bool is_header, + bool is_latch) { + // ── phi ── + dst->phi = nullptr; + if (!is_header) { + Phi** tail = &dst->phi; + for (Phi* sp = src->phi; sp; sp = sp->link) { + Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + np->to = remap_tmp(sp->to, st.tmp_map); + np->cls = sp->cls; + np->visit = 0; + np->narg = sp->narg; + np->arg = static_cast<Ref*>(vnew(ctx, sp->narg, sizeof(Ref), PFn)); + np->blk = static_cast<Blk**>(vnew(ctx, sp->narg, sizeof(Blk*), PFn)); + for (uint k = 0; k < sp->narg; k++) { + np->arg[k] = remap_tmp(sp->arg[k], st.tmp_map); + auto it = st.blk_map.find(sp->blk[k]->id); + np->blk[k] = (it != st.blk_map.end()) ? it->second : sp->blk[k]; + } + np->link = nullptr; + *tail = np; + tail = &np->link; + } + } + + // ── ins ── + dst->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn)); + dst->nins = 0; + for (Ins* si = src->ins; si < &src->ins[src->nins]; si++) { + Ins ni; + ni.op = si->op; + ni.cls = si->cls; + ni.to = remap_tmp(si->to, st.tmp_map); + ni.arg[0] = remap_tmp(si->arg[0], st.tmp_map); + ni.arg[1] = remap_tmp(si->arg[1], st.tmp_map); + addins(ctx, &dst->ins, &dst->nins, &ni); + } + + // ── jmp ── + if (is_header) { + // Jjnz cmp, body_first, exit -> Jjmp blk_map[body_first.id] + dst->jmp.type = Jjmp; + dst->jmp.arg = Ref::none(); + auto it = st.blk_map.find(src->s1->id); + assert(it != st.blk_map.end()); + dst->s1 = it->second; + dst->s2 = nullptr; + } else if (is_latch) { + // Kept for now, caller rewrites when linking across copies + dst->jmp.type = Jjmp; + dst->jmp.arg = Ref::none(); + dst->s1 = nullptr; + dst->s2 = nullptr; + } else { + dst->jmp.type = src->jmp.type; + dst->jmp.arg = remap_tmp(src->jmp.arg, st.tmp_map); + dst->s1 = nullptr; + dst->s2 = nullptr; + if (src->s1) { + auto it = st.blk_map.find(src->s1->id); + dst->s1 = (it != st.blk_map.end()) ? it->second : src->s1; + } + if (src->s2) { + auto it = st.blk_map.find(src->s2->id); + dst->s2 = (it != st.blk_map.end()) ? it->second : src->s2; + } + } +} + +// Clone body N times sequentially to replace original loop. Matching by Blk* pointer is the only +// robust way. +Ref phi_arg_for_pred(Phi* p, Blk* pred) { + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == pred) + return p->arg[k]; + die("phi_arg_for_pred: pred not found in phi.blk[]"); +} + +// Prerequisites: detect_induction successful, N = induction.trip. +void full_unroll(Ctx* ctx, Fn* fn, const LoopShape& L, int64_t N) { + // Collect body-defined tmps + std::unordered_set<uint> body_defined; + for (Blk* b : L.body) { + for (Phi* p = b->phi; p; p = p->link) + if (rtype(p->to) == RTmp) + body_defined.insert(p->to.val); + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (rtype(i->to) == RTmp && i->to.val != 0) + body_defined.insert(i->to.val); + } + + // Collect header phi list (in linked list order, to re-define them later) + std::vector<Phi*> header_phis; + for (Phi* p = L.header->phi; p; p = p->link) + header_phis.push_back(p); + + // Clone for each iteration + std::vector<CopyState> copies(static_cast<size_t>(N)); + + // Map previous latch tmps + std::unordered_map<uint, Ref> prev_latch_tmp_map; + + static int unroll_seq = 0; + int my_seq = unroll_seq++; + + for (int64_t k = 0; k < N; k++) { + CopyState& st = copies[static_cast<size_t>(k)]; + + // Allocate new blk for each body block + for (Blk* b : L.body) { + Blk* nb = newblk(ctx); + char buf[128]; + std::snprintf(buf, sizeof(buf), "u%d.%lld.%s", my_seq, static_cast<long long>(k), + b->name ? b->name : "blk"); + nb->name = strf(ctx, PFn, "%s", buf); + nb->id = fn->nblk++; + st.blk_map[b->id] = nb; + } + st.first_blk = st.blk_map[L.header->id]; + st.last_blk = st.blk_map[L.latch->id]; + + // Assign fresh ids to body-defined tmps; then populate header phi.tos with entry values + for (uint tv : body_defined) { + const char* prfx = fn->tmp[tv].name ? fn->tmp[tv].name : "u"; + st.tmp_map[tv] = newtmp(ctx, prfx, fn->tmp[tv].cls, fn); + } + for (Phi* p : header_phis) { + Ref entry_val; + if (k == 0) { + entry_val = phi_arg_for_pred(p, L.preheader); + } else { + Ref prev_latch_ref = phi_arg_for_pred(p, L.latch); + entry_val = remap_tmp(prev_latch_ref, prev_latch_tmp_map); + } + if (rtype(p->to) == RTmp) + st.tmp_map[p->to.val] = entry_val; + } + + // Clone each body block + for (Blk* b : L.body) { + Blk* dst = st.blk_map[b->id]; + bool is_h = (b == L.header); + bool is_l = (b == L.latch); + // Single-block loop specialization + clone_body_blk(ctx, st, b, dst, is_h, is_l && !is_h); + } + + // Save current tmp_map for next round + prev_latch_tmp_map = st.tmp_map; + } + + // Link copies: copy_k's latch_clone.jmp = Jjmp copy_{k+1}.first + for (int64_t k = 0; k < N - 1; k++) { + Blk* lc = copies[static_cast<size_t>(k)].last_blk; + Blk* nh = copies[static_cast<size_t>(k + 1)].first_blk; + // Single-block loop self-loop fix: rewrite last_blk's Jjmp to point to next copy + lc->jmp.type = Jjmp; + lc->jmp.arg = Ref::none(); + lc->s1 = nh; + lc->s2 = nullptr; + } + + // Last round's latch_clone: append Ocopy to re-def header phi.tos, jmp points to exit_bb + { + CopyState& last = copies[static_cast<size_t>(N - 1)]; + Blk* lc = last.last_blk; + for (Phi* p : header_phis) { + Ref final_val = remap_tmp(phi_arg_for_pred(p, L.latch), last.tmp_map); + Ins re_def; + re_def.op = Ocopy; + re_def.cls = static_cast<uint>(p->cls) & 3u; + re_def.to = p->to; // Original header phi.to (now dead, SSA def monopolized) + re_def.arg[0] = final_val; + re_def.arg[1] = Ref::none(); + addins(ctx, &lc->ins, &lc->nins, &re_def); + } + lc->jmp.type = Jjmp; + lc->jmp.arg = Ref::none(); + lc->s1 = L.exit_bb; + lc->s2 = nullptr; + } + + // Point preheader to copy_0.first; clear original header's pred references (leave for fillcfg + // to rebuild) + Blk* new_entry = copies[0].first_blk; + if (L.preheader->s1 == L.header) + L.preheader->s1 = new_entry; + if (L.preheader->s2 == L.header) + L.preheader->s2 = new_entry; + + // Fix exit_bb phi incoming edges: replace header with last copy's latch_clone + Blk* new_last = copies[static_cast<size_t>(N - 1)].last_blk; + for (Phi* p = L.exit_bb->phi; p; p = p->link) { + for (uint kk = 0; kk < p->narg; kk++) { + if (p->blk[kk] == L.header) + p->blk[kk] = new_last; + } + } + + // Link chain: preheader -> copy_0_blocks -> ... -> copy_{N-1}_blocks -> original preheader.link + Blk* saved_link = L.preheader->link; + Blk** plink = &L.preheader->link; + for (int64_t k = 0; k < N; k++) { + const CopyState& st = copies[static_cast<size_t>(k)]; + // Link in L.body order + for (Blk* b : L.body) { + Blk* nb = st.blk_map.at(b->id); + *plink = nb; + plink = &nb->link; + } + } + *plink = saved_link; + + if (debug['U']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> unroll full N=%lld in %s (header b%u, body=%zu, ins=%d)\n", + static_cast<long long>(N), fn->name, L.header->id, L.body.size(), + count_body_ins(L)); + } +} + +// Partial Unroll + +Blk* partial_unroll(Ctx* ctx, Fn* fn, const LoopShape& L, const Induction& ind, int F) { + // body-defined tmps + std::unordered_set<uint> body_defined; + for (Blk* b : L.body) { + for (Phi* p = b->phi; p; p = p->link) + if (rtype(p->to) == RTmp) + body_defined.insert(p->to.val); + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (rtype(i->to) == RTmp && i->to.val != 0) + body_defined.insert(i->to.val); + } + + // header phi list + std::vector<Phi*> header_phis; + for (Phi* p = L.header->phi; p; p = p->link) + header_phis.push_back(p); + + static int unroll_seq_p = 0; + int my_seq = unroll_seq_p++; + + // Create main_head, build its phi list first (loopback edge filled later) + Blk* main_head = newblk(ctx); + main_head->name = strf(ctx, PFn, "u%d.main.head", my_seq); + main_head->id = fn->nblk++; + + std::unordered_map<uint, Ref> head_to_main; // original phi.to.val -> main_head phi.to + std::vector<Phi*> main_phis; + Phi** ptail = &main_head->phi; + for (Phi* p : header_phis) { + Phi* pm = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + pm->cls = p->cls; + pm->visit = 0; + const char* prfx = + (rtype(p->to) == RTmp && fn->tmp[p->to.val].name) ? fn->tmp[p->to.val].name : "u"; + pm->to = newtmp(ctx, prfx, p->cls, fn); + pm->narg = 2; + pm->arg = static_cast<Ref*>(vnew(ctx, 2, sizeof(Ref), PFn)); + pm->blk = static_cast<Blk**>(vnew(ctx, 2, sizeof(Blk*), PFn)); + pm->arg[0] = phi_arg_for_pred(p, L.preheader); + pm->blk[0] = L.preheader; + pm->arg[1] = Ref::none(); + pm->blk[1] = nullptr; + pm->link = nullptr; + *ptail = pm; + ptail = &pm->link; + main_phis.push_back(pm); + if (rtype(p->to) == RTmp) + head_to_main[p->to.val] = pm->to; + } + + // Clone body F times, same as full_unroll (except clone0 entry uses main_head phi.to) + std::vector<CopyState> copies(static_cast<size_t>(F)); + std::unordered_map<uint, Ref> prev_latch_tmp_map; + for (int k = 0; k < F; k++) { + CopyState& st = copies[static_cast<size_t>(k)]; + for (Blk* b : L.body) { + Blk* nb = newblk(ctx); + nb->name = strf(ctx, PFn, "u%d.%d.%s", my_seq, k, b->name ? b->name : "blk"); + nb->id = fn->nblk++; + st.blk_map[b->id] = nb; + } + st.first_blk = st.blk_map[L.header->id]; + st.last_blk = st.blk_map[L.latch->id]; + for (uint tv : body_defined) { + const char* prfx = fn->tmp[tv].name ? fn->tmp[tv].name : "u"; + st.tmp_map[tv] = newtmp(ctx, prfx, fn->tmp[tv].cls, fn); + } + for (Phi* p : header_phis) { + Ref entry_val; + if (k == 0) { + auto it = head_to_main.find(p->to.val); + entry_val = + (it != head_to_main.end()) ? it->second : phi_arg_for_pred(p, L.preheader); + } else { + entry_val = remap_tmp(phi_arg_for_pred(p, L.latch), prev_latch_tmp_map); + } + if (rtype(p->to) == RTmp) + st.tmp_map[p->to.val] = entry_val; + } + for (Blk* b : L.body) { + Blk* dst = st.blk_map[b->id]; + bool is_h = (b == L.header); + bool is_l = (b == L.latch); + clone_body_blk(ctx, st, b, dst, is_h, is_l && !is_h); + } + prev_latch_tmp_map = st.tmp_map; + } + + // Link: copy_k.last -> copy_{k+1}.first + for (int k = 0; k < F - 1; k++) { + Blk* lc = copies[static_cast<size_t>(k)].last_blk; + lc->jmp.type = Jjmp; + lc->jmp.arg = Ref::none(); + lc->s1 = copies[static_cast<size_t>(k + 1)].first_blk; + lc->s2 = nullptr; + } + // Last latch -> main_head (back-edge) + Blk* last_latch = copies[static_cast<size_t>(F - 1)].last_blk; + last_latch->jmp.type = Jjmp; + last_latch->jmp.arg = Ref::none(); + last_latch->s1 = main_head; + last_latch->s2 = nullptr; + + // Fill main_head phi's loopback incoming edge + for (size_t idx = 0; idx < header_phis.size(); idx++) { + Phi* p = header_phis[idx]; + Phi* pm = main_phis[idx]; + pm->arg[1] = + remap_tmp(phi_arg_for_pred(p, L.latch), copies[static_cast<size_t>(F - 1)].tmp_map); + pm->blk[1] = last_latch; + } + + // main_head guard: adj_bound = bound - (F-1)*step; cmp(i_main, adj) + main_head->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn)); + main_head->nins = 0; + int kls = (ind.cmp_op == Ocsltl || ind.cmp_op == Ocslel) ? Kl : Kw; + int64_t C = static_cast<int64_t>(F - 1) * ind.step; + Ref adj_ref; + if (ind.bound_const) { + adj_ref = getcon(ctx, ind.bound - C, fn); + } else { + Ref adj_tmp = newtmp(ctx, "u", kls, fn); + Ins sub; + sub.op = Osub; + sub.cls = static_cast<uint>(kls) & 3u; + sub.to = adj_tmp; + sub.arg[0] = ind.bound_ref; + sub.arg[1] = getcon(ctx, C, fn); + addins(ctx, &main_head->ins, &main_head->nins, &sub); + adj_ref = adj_tmp; + } + Ref iv_main = head_to_main.at(ind.iv_ref.val); + Ref gcmp = newtmp(ctx, "u", Kw, fn); + Ins cmpi; + cmpi.op = static_cast<uint>(ind.cmp_op); + cmpi.cls = static_cast<uint>(ind.cmp_cls) & 3u; + cmpi.to = gcmp; + cmpi.arg[0] = iv_main; + cmpi.arg[1] = adj_ref; + addins(ctx, &main_head->ins, &main_head->nins, &cmpi); + main_head->jmp.type = Jjnz; + main_head->jmp.arg = gcmp; + main_head->s1 = copies[0].first_blk; + main_head->s2 = L.header; // remainder loop + + // preheader -> main_head + if (L.preheader->s1 == L.header) + L.preheader->s1 = main_head; + if (L.preheader->s2 == L.header) + L.preheader->s2 = main_head; + + // Remainder header phi's preheader edge: replace with main_head, value is pm.to + for (size_t idx = 0; idx < header_phis.size(); idx++) { + Phi* p = header_phis[idx]; + Phi* pm = main_phis[idx]; + for (uint kk = 0; kk < p->narg; kk++) { + if (p->blk[kk] == L.preheader) { + p->blk[kk] = main_head; + p->arg[kk] = pm->to; + } + } + } + + // link chain: preheader -> main_head -> clones -> original preheader.link + Blk* saved_link = L.preheader->link; + L.preheader->link = main_head; + Blk** plink = &main_head->link; + for (int k = 0; k < F; k++) { + for (Blk* b : L.body) { + Blk* nb = copies[static_cast<size_t>(k)].blk_map.at(b->id); + *plink = nb; + plink = &nb->link; + } + } + *plink = saved_link; + + if (debug['U']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> unroll partial F=%d in %s (header b%u, body=%zu, runtime_bound=%d)\n", F, + fn->name, L.header->id, L.body.size(), ind.bound_const ? 0 : 1); + } + return main_head; +} + +// Pass driver + +bool unroll_pass(Ctx* ctx, Fn* fn, std::unordered_set<Blk*>& unrolled) { + auto loops = find_simple_loops(fn); + bool any = false; + + for (auto& L : loops) { + // Prevent infinite partial unrolling by skipping already processed main_heads. + if (unrolled.count(L.header)) + continue; + + Induction ind; + if (!detect_induction(fn, L, ind)) + continue; + int64_t N = ind.trip; + int body_ins = count_body_ins(L); + + // Prioritize full unroll: trip known and <= kFullUnrollTripMax, and N*body within budget + if (N >= 1 && N <= kFullUnrollTripMax && + static_cast<int64_t>(body_ins) * N <= kFullUnrollBudget) { + full_unroll(ctx, fn, L, N); + any = true; + break; + } + + if (N == 0) + continue; // Loop doesn't execute, leave to simplcfg + + // Otherwise try partial unroll: N=-1 (runtime bound) or N > kFullUnrollTripMax + // Skip if body is too large (F*body limit kPartialMaxBodyIns*F = 256 ins). + if (body_ins <= 0 || body_ins > kPartialMaxBodyIns) + continue; + Blk* mh = partial_unroll(ctx, fn, L, ind, kPartialFactor); + unrolled.insert(mh); + any = true; + break; + } + return any; +} + +} // namespace + +void unroll(Ctx* ctx, Fn* fn) { + // Caller guarantees fillcfg + filldom + filluse are ready. + std::unordered_set<Blk*> + unrolled; // main_heads already partial-unrolled, persistent across fixpoints + int iter = 0; + for (; iter < kUnrollFixpointIters; iter++) { + if (!unroll_pass(ctx, fn, unrolled)) + break; + // CFG modified, rebuild for next round. + fillcfg(ctx, fn); + filluse(ctx, fn); + filldom(fn); + } + if (debug['U'] && iter > 0) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> unroll %s: %d round%s\n", fn->name, iter, iter == 1 ? "" : "s"); + } +} + +} // namespace rcc diff --git a/src/mid/unswitch.cc b/src/mid/unswitch.cc new file mode 100644 index 0000000..fa8bf73 --- /dev/null +++ b/src/mid/unswitch.cc @@ -0,0 +1,447 @@ +/* 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 unswitch.cc +/// @brief Loop Unswitching. +/// +/// See docs/unswitch.md for architectural notes. + +#include "rcc/all.h" + +#include <cassert> +#include <cstdio> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +namespace rcc { + +namespace { + +constexpr int kUnswitchMaxBodyIns = 64; +constexpr int kUnswitchFixpointIters = 3; + +// Loop Identification +struct LoopShape { + Blk* header; + Blk* preheader; + Blk* latch; + Blk* exit_bb; + std::vector<Blk*> body; // includes header / latch +}; + +// Find simple loops (derived from unroll.cc). +std::vector<LoopShape> find_simple_loops(Fn* fn) { + std::vector<LoopShape> out; + for (Blk* b = fn->start; b; b = b->link) + b->visit = ~0u; + + for (uint i = 0; i < fn->nblk; i++) { + Blk* hd = fn->rpo[i]; + if (hd->id == ~0u) + continue; + + int back_cnt = 0, pre_cnt = 0; + uint latch_idx = 0, pre_idx = 0; + for (uint k = 0; k < hd->npred; k++) { + Blk* p = hd->pred[k]; + if (p->id == ~0u) + continue; + if (p->id >= i) { + back_cnt++; + latch_idx = k; + } else { + pre_cnt++; + pre_idx = k; + } + } + if (back_cnt != 1 || pre_cnt != 1) + continue; + + Blk* latch = hd->pred[latch_idx]; + Blk* preheader = hd->pred[pre_idx]; + + // Reverse DFS to collect body + std::vector<Blk*> body = {hd}; + std::vector<Blk*> stack = {latch}; + hd->visit = hd->id; + while (!stack.empty()) { + Blk* n = stack.back(); + stack.pop_back(); + if (n->visit == hd->id) + continue; + n->visit = hd->id; + body.push_back(n); + for (uint k = 0; k < n->npred; k++) + stack.push_back(n->pred[k]); + } + + // single-exit gate + if (hd->jmp.type != Jjnz) + continue; + Blk* exit_bb = hd->s2; + if (!exit_bb || !hd->s1) + continue; + + auto in_body = [&](Blk* x) -> bool { + for (Blk* bb : body) + if (bb == x) + return true; + return false; + }; + if (in_body(exit_bb)) + continue; + if (!in_body(hd->s1)) + continue; + + bool ok = true; + for (Blk* bb : body) { + if (bb == hd) + continue; + if (isret(bb->jmp.type)) { + ok = false; + break; + } + auto check = [&](Blk* s) -> bool { return !s || in_body(s); }; + if (!check(bb->s1) || !check(bb->s2)) { + ok = false; + break; + } + } + if (!ok) + continue; + + LoopShape L; + L.header = hd; + L.preheader = preheader; + L.latch = latch; + L.exit_bb = exit_bb; + L.body = std::move(body); + out.push_back(std::move(L)); + } + return out; +} + +// Loop Invariance +bool ref_is_invariant(Fn* fn, const LoopShape& L, Ref r) { + if (rtype(r) == RCon) + return true; + if (rtype(r) != RTmp) + return false; + if (r.val >= static_cast<uint>(fn->ntmp)) + return false; + uint bid = fn->tmp[r.val].bid; + if (bid == ~0u) + return true; + for (Blk* b : L.body) + if (b->id == bid) + return false; + return true; +} + +// Find a hoistable Jjnz branch. Returns nullptr if no candidate. +Blk* find_unswitch_blk(Fn* fn, const LoopShape& L) { + for (Blk* b : L.body) { + if (b == L.header) + continue; // header.Jjnz is loop exit, cannot hoist + if (b == L.latch) + continue; // Jjnz on latch is usually back-edge directed, skip to simplify analysis + if (b->jmp.type != Jjnz) + continue; + if (rtype(b->jmp.arg) != RTmp) + continue; // RCon left to simplcfg + if (!ref_is_invariant(fn, L, b->jmp.arg)) + continue; + if (!b->s1 || !b->s2 || b->s1 == b->s2) + continue; + bool s1_in = false, s2_in = false; + for (Blk* bb : L.body) { + if (bb == b->s1) + s1_in = true; + if (bb == b->s2) + s2_in = true; + } + if (!s1_in || !s2_in) + continue; + return b; + } + return nullptr; +} + +int count_body_ins(const LoopShape& L) { + int n = 0; + for (Blk* b : L.body) { + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (i->op != Onop) + n++; + for (Phi* p = b->phi; p; p = p->link) + (void)p, n++; + } + return n; +} + +// Clone Infrastructure (derived from unroll.cc with minor adaptations) +struct CopyState { + std::unordered_map<uint, Blk*> blk_map; // body blk.id -> clone + std::unordered_map<uint, Ref> tmp_map; // body-defined tmp.val -> clone Ref +}; + +Ref remap_tmp(Ref r, const std::unordered_map<uint, Ref>& m) { + if (rtype(r) != RTmp) + return r; + auto it = m.find(r.val); + return (it != m.end()) ? it->second : r; +} + +// Generic block clone. +void clone_blk(Ctx* ctx, const CopyState& st, Blk* src, Blk* dst) { + dst->phi = nullptr; + Phi** tail = &dst->phi; + for (Phi* sp = src->phi; sp; sp = sp->link) { + Phi* np = static_cast<Phi*>(alloc(ctx, sizeof(Phi))); + np->to = remap_tmp(sp->to, st.tmp_map); + np->cls = sp->cls; + np->visit = 0; + np->narg = sp->narg; + np->arg = static_cast<Ref*>(vnew(ctx, sp->narg, sizeof(Ref), PFn)); + np->blk = static_cast<Blk**>(vnew(ctx, sp->narg, sizeof(Blk*), PFn)); + for (uint k = 0; k < sp->narg; k++) { + np->arg[k] = remap_tmp(sp->arg[k], st.tmp_map); + auto it = st.blk_map.find(sp->blk[k]->id); + np->blk[k] = (it != st.blk_map.end()) ? it->second : sp->blk[k]; + } + np->link = nullptr; + *tail = np; + tail = &np->link; + } + + dst->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn)); + dst->nins = 0; + for (Ins* si = src->ins; si < &src->ins[src->nins]; si++) { + Ins ni; + ni.op = si->op; + ni.cls = si->cls; + ni.to = remap_tmp(si->to, st.tmp_map); + ni.arg[0] = remap_tmp(si->arg[0], st.tmp_map); + ni.arg[1] = remap_tmp(si->arg[1], st.tmp_map); + addins(ctx, &dst->ins, &dst->nins, &ni); + } + + auto map_succ = [&](Blk* s) -> Blk* { + if (!s) + return nullptr; + auto it = st.blk_map.find(s->id); + return (it != st.blk_map.end()) ? it->second : s; + }; + dst->jmp.type = src->jmp.type; + dst->jmp.arg = remap_tmp(src->jmp.arg, st.tmp_map); + dst->s1 = map_succ(src->s1); + dst->s2 = map_succ(src->s2); +} + +CopyState clone_loop(Ctx* ctx, Fn* fn, const LoopShape& L, const char* tag) { + CopyState st; + + std::unordered_set<uint> body_defined; + for (Blk* b : L.body) { + for (Phi* p = b->phi; p; p = p->link) + if (rtype(p->to) == RTmp) + body_defined.insert(p->to.val); + for (Ins* i = b->ins; i < &b->ins[b->nins]; i++) + if (rtype(i->to) == RTmp && i->to.val != 0) + body_defined.insert(i->to.val); + } + + for (Blk* b : L.body) { + Blk* nb = newblk(ctx); + nb->name = strf(ctx, PFn, "%s.%s", tag, b->name ? b->name : "blk"); + nb->id = fn->nblk++; + st.blk_map[b->id] = nb; + } + + for (uint tv : body_defined) { + const char* prfx = fn->tmp[tv].name ? fn->tmp[tv].name : "u"; + st.tmp_map[tv] = newtmp(ctx, prfx, fn->tmp[tv].cls, fn); + } + + for (Blk* b : L.body) + clone_blk(ctx, st, b, st.blk_map[b->id]); + + return st; +} + +// Remove dangling phi incoming edges from deactivated successors. +void remove_phi_entry(Blk* succ, Blk* old_pred) { + for (Phi* p = succ->phi; p; p = p->link) { + uint w = 0; + for (uint k = 0; k < p->narg; k++) { + if (p->blk[k] == old_pred) + continue; + if (w != k) { + p->arg[w] = p->arg[k]; + p->blk[w] = p->blk[k]; + } + w++; + } + p->narg = w; + } +} + +void do_unswitch(Ctx* ctx, Fn* fn, const LoopShape& L, Blk* unsw_blk) { + Ref cond = unsw_blk->jmp.arg; + Blk* orig_s1 = unsw_blk->s1; + Blk* orig_s2 = unsw_blk->s2; + + CopyState A = clone_loop(ctx, fn, L, "uA"); + CopyState B = clone_loop(ctx, fn, L, "uB"); + + Blk* header_A = A.blk_map.at(L.header->id); + Blk* header_B = B.blk_map.at(L.header->id); + Blk* unsw_A = A.blk_map.at(unsw_blk->id); + Blk* unsw_B = B.blk_map.at(unsw_blk->id); + + // In A, collapse to true branch (orig.s1) + unsw_A->jmp.type = Jjmp; + unsw_A->jmp.arg = Ref::none(); + unsw_A->s1 = A.blk_map.at(orig_s1->id); + unsw_A->s2 = nullptr; + // orig.s2 clone in A is no longer reached by unsw_A; must remove phi edges from unsw_A + remove_phi_entry(A.blk_map.at(orig_s2->id), unsw_A); + + // In B, collapse to false branch (orig.s2) + unsw_B->jmp.type = Jjmp; + unsw_B->jmp.arg = Ref::none(); + unsw_B->s1 = B.blk_map.at(orig_s2->id); + unsw_B->s2 = nullptr; + remove_phi_entry(B.blk_map.at(orig_s1->id), unsw_B); + + // guard block: Jjnz cond, header_A, header_B + Blk* guard = newblk(ctx); + guard->name = strf(ctx, PFn, "uG.%s", L.header->name ? L.header->name : ""); + guard->id = fn->nblk++; + guard->phi = nullptr; + guard->ins = static_cast<Ins*>(vnew(ctx, 0, sizeof(Ins), PFn)); + guard->nins = 0; + guard->jmp.type = Jjnz; + guard->jmp.arg = cond; + guard->s1 = header_A; + guard->s2 = header_B; + + // preheader -> guard (replaces original preheader -> header) + if (L.preheader->s1 == L.header) + L.preheader->s1 = guard; + if (L.preheader->s2 == L.header) + L.preheader->s2 = guard; + + // Update header_A / header_B phi.blk[k]==preheader to guard + auto fix_hdr_pred = [&](Blk* hdr_clone) { + for (Phi* p = hdr_clone->phi; p; p = p->link) + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == L.preheader) + p->blk[k] = guard; + }; + fix_hdr_pred(header_A); + fix_hdr_pred(header_B); + + // exit_bb phi: split blk==L.header incoming edges into two + Blk* exit = L.exit_bb; + for (Phi* p = exit->phi; p; p = p->link) { + uint header_entries = 0; + for (uint k = 0; k < p->narg; k++) + if (p->blk[k] == L.header) + header_entries++; + if (header_entries == 0) + continue; + + uint new_narg = p->narg + header_entries; // Expand each into 2 = +1 + Ref* new_arg = static_cast<Ref*>(vnew(ctx, new_narg, sizeof(Ref), PFn)); + Blk** new_blk = static_cast<Blk**>(vnew(ctx, new_narg, sizeof(Blk*), PFn)); + uint w = 0; + for (uint k = 0; k < p->narg; k++) { + if (p->blk[k] == L.header) { + new_arg[w] = remap_tmp(p->arg[k], A.tmp_map); + new_blk[w] = header_A; + w++; + new_arg[w] = remap_tmp(p->arg[k], B.tmp_map); + new_blk[w] = header_B; + w++; + } else { + new_arg[w] = p->arg[k]; + new_blk[w] = p->blk[k]; + w++; + } + } + p->narg = new_narg; + p->arg = new_arg; + p->blk = new_blk; + } + + // link chain: preheader -> guard -> A.body -> B.body -> original preheader.link + Blk* saved = L.preheader->link; + L.preheader->link = guard; + Blk** plink = &guard->link; + for (Blk* b : L.body) { + *plink = A.blk_map.at(b->id); + plink = &(*plink)->link; + } + for (Blk* b : L.body) { + *plink = B.blk_map.at(b->id); + plink = &(*plink)->link; + } + *plink = saved; + + if (debug['W']) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> unswitch %s: hdr b%u, unsw b%u, body=%zu\n", fn->name, L.header->id, + unsw_blk->id, L.body.size()); + } +} + +bool unswitch_pass(Ctx* ctx, Fn* fn) { + auto loops = find_simple_loops(fn); + for (auto& L : loops) { + Blk* u = find_unswitch_blk(fn, L); + if (!u) + continue; + if (count_body_ins(L) > kUnswitchMaxBodyIns) + continue; + do_unswitch(ctx, fn, L, u); + return true; // Process one per pass to avoid scanning mutated CFG + } + return false; +} + +} // namespace + +void unswitch(Ctx* ctx, Fn* fn) { + // Caller should have done fillcfg + filluse; filldom not needed (only Tmp.bid is used). + int iter = 0; + for (; iter < kUnswitchFixpointIters; iter++) { + if (!unswitch_pass(ctx, fn)) + break; + fillcfg(ctx, fn); + filluse(ctx, fn); + } + if (debug['W'] && iter > 0) { + FILE* out = ctx->dbg_buf ? ctx->dbg_buf : stderr; + fprintf(out, "> unswitch %s: %d round%s\n", fn->name, iter, iter == 1 ? "" : "s"); + } +} + +} // namespace rcc diff --git a/src/util.cc b/src/util.cc new file mode 100644 index 0000000..8c5ce13 --- /dev/null +++ b/src/util.cc @@ -0,0 +1,778 @@ +/* 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 util.cc +/// @brief Infrastructure: Memory management, growable arrays. + +#include "rcc/all.h" + +#include <cstdarg> +#include <vector> + +namespace rcc { + +// Global Read-Only Resources + +Typ* typ; +Target T = {}; + +char debug['Z' + 1] = {}; +Op optab[NOp]; + +namespace { +struct OptabInit { + OptabInit() { + for (size_t i = 0; i < NOp; ++i) { + const OpInfo& info = kops[i]; + Op& o = optab[i]; + o.name = const_cast<char*>(info.name); + for (int a = 0; a < 2; ++a) + for (int b = 0; b < 4; ++b) + o.argcls[a][b] = info.argcls[a][b]; + o.canfold = (info.flags & Fold) != 0; + o.hasid = (info.flags & Id) != 0; + o.idval = (info.flags & IdV) != 0; + o.commutes = (info.flags & Comm) != 0; + o.assoc = (info.flags & Assoc) != 0; + o.idemp = (info.flags & Idemp) != 0; + o.cmpeqwl = (info.flags & CmpEW) != 0; + o.cmplgtewl = (info.flags & CmpLW) != 0; + o.eqval = (info.flags & EqV) != 0; + o.pinned = (info.flags & Pin) != 0; + } + } +} optab_init_; +} // namespace + +// SubPool: per-Fn Memory Pool + +void* subpool_alloc(SubPool* sp, size_t n) { + if (n == 0) + return nullptr; + void* p = emalloc(n); + sp->chunks.push_back(p); + return p; +} + +void subpool_free(SubPool* sp) { + for (void* p : sp->chunks) + free(p); + sp->chunks.clear(); +} + +// Ctx Lifecycle + +void* emalloc(size_t n) { + void* p = calloc(1, n); + if (!p) + die("emalloc, out of memory"); + return p; +} + +void Ctx::init() { + assert(!insb && "Ctx::init called twice"); + insb = static_cast<Ins*>(emalloc(NIns * sizeof(Ins))); + curi = insb + NIns; +} + +void Ctx::bind(Fn* f) { + assert(f && f->_subpool && "bind requires fn with attached SubPool"); + subpool = f->_subpool; + curi = insb + NIns; + load_curf = nullptr; + load_inum = 0; + load_ilog = nullptr; + load_nlog = 0; + ssa_namel = nullptr; + gvn_con01[0] = Ref::none(); + gvn_con01[1] = Ref::none(); + gvn_tbl = nullptr; + gvn_tbln = 0; + rega_regu = 0; + rega_tmp = nullptr; + rega_npm = 0; + rega_loop = 0; + rega_stmov = 0; + rega_stblk = 0; + emit_stash = nullptr; + spill_fp_live_peak = 0; + fn = f; +} + +void Ctx::release() { + if (subpool) { + subpool_free(subpool); + delete subpool; + subpool = nullptr; + } + fn = nullptr; +} + +Ctx::~Ctx() { + if (subpool) { + subpool_free(subpool); + delete subpool; + } + if (insb) + free(insb); + if (out_buf) + fclose(out_buf); + if (dbg_buf) + fclose(dbg_buf); + // ssa.cc Name pool is allocated via emalloc and reused across functions, ~Ctx must drain it. + ssa_drain_namel(this); +} + +// alloc / freeall + +void* alloc(Ctx* ctx, size_t n) { + assert(ctx && ctx->subpool); + return subpool_alloc(ctx->subpool, n); +} + +void freeall(Ctx* ctx) { + assert(ctx && ctx->subpool); + subpool_free(ctx->subpool); +} + +// Growable Arrays (vnew / vgrow / vfree) + +struct ArrayHdr { + ulong magic; + ulong cap; + size_t esz; + Pool pool; +}; + +static constexpr ulong AHDR_MAGIC = 0xA77A7007u; +static constexpr ulong VMIN_CAP = 2; + +/// Retreat from user pointer to header +static ArrayHdr* hdr_of(void* p) { + auto* h = static_cast<ArrayHdr*>(p) - 1; + assert(h->magic == AHDR_MAGIC); + return h; +} + +void* vnew(Ctx* ctx, ulong len, size_t esz, Pool pl) { + ulong cap = VMIN_CAP; + while (cap < len) + cap *= 2; + + size_t total = sizeof(ArrayHdr) + cap * esz; + void* raw = (pl == PHeap) ? emalloc(total) : alloc(ctx, total); + + auto* h = static_cast<ArrayHdr*>(raw); + h->magic = AHDR_MAGIC; + h->cap = cap; + h->esz = esz; + h->pool = pl; + return h + 1; +} + +void vfree(void* p) { + ArrayHdr* h = hdr_of(p); + if (h->pool == PHeap) { + h->magic = 0; + free(h); + } +} + +void vgrow(Ctx* ctx, void* vp, ulong len) { + void** pp = static_cast<void**>(vp); + ArrayHdr* h = hdr_of(*pp); + + if (h->cap >= len) + return; + + void* nv = vnew(ctx, len, h->esz, h->pool); + memcpy(nv, *pp, h->cap * h->esz); + vfree(*pp); + *pp = nv; +} + +// String Pool (intern / str) + +uint32_t hash(const char* s) { + uint32_t h = 5381; + for (; *s; s++) + h = ((h << 5) + h) ^ static_cast<uchar>(*s); // djb2 xor variant + return h; +} + +/// Hash table slot +struct StrSlot { + uint32_t hash; + uint32_t id; +}; + +static constexpr uint32_t SLOT_EMPTY = UINT32_MAX; +static constexpr size_t HTAB_INIT = 1024; + +static std::vector<char> char_buf; // Continuous character storage +static std::vector<uint32_t> id_to_off; // ID -> char_buf offset +static std::vector<StrSlot> htab; // Open addressing hash table +static uint32_t htab_used; // Number of occupied slots + +/// Initialize hash table (triggered automatically on first intern call) +static void htab_init() { + htab.assign(HTAB_INIT, {SLOT_EMPTY, 0}); + htab_used = 0; +} + +/// Grow and rehash when load factor exceeds 70% +static void htab_grow() { + size_t new_sz = htab.size() * 2; + std::vector<StrSlot> old; + old.swap(htab); + htab.assign(new_sz, {SLOT_EMPTY, 0}); + + uint32_t mask = static_cast<uint32_t>(new_sz - 1); + for (auto& s : old) { + if (s.hash == SLOT_EMPTY) + continue; + uint32_t pos = s.hash & mask; + while (htab[pos].hash != SLOT_EMPTY) + pos = (pos + 1) & mask; + htab[pos] = s; + } +} + +uint32_t intern(const char* s) { + if (htab.empty()) + htab_init(); + + uint32_t h = hash(s); + uint32_t mask = static_cast<uint32_t>(htab.size() - 1); + uint32_t pos = h & mask; + + // Linear probing search + while (htab[pos].hash != SLOT_EMPTY) { + if (htab[pos].hash == h) { + // hash identical, compare actual string + uint32_t off = id_to_off[htab[pos].id]; + if (strcmp(s, char_buf.data() + off) == 0) + return htab[pos].id; + } + pos = (pos + 1) & mask; + } + + // Not found, insert new string + uint32_t new_id = static_cast<uint32_t>(id_to_off.size()); + uint32_t off = static_cast<uint32_t>(char_buf.size()); + size_t len = strlen(s); + char_buf.insert(char_buf.end(), s, s + len + 1); // Includes '\0' + id_to_off.push_back(off); + + htab[pos] = {h, new_id}; + htab_used++; + + // Load factor check + if (htab_used * 10 > htab.size() * 7) + htab_grow(); + + return new_id; +} + +char* str(uint32_t id) { + assert(id < id_to_off.size()); + return char_buf.data() + id_to_off[id]; +} + +// strf: Format string and allocate to specified memory pool + +char* strf(Ctx* ctx, Pool pl, const char* fmt, ...) { + va_list ap, ap2; + va_start(ap, fmt); + va_copy(ap2, ap); + int n = vsnprintf(nullptr, 0, fmt, ap); + va_end(ap); + + char* p = static_cast<char*>((pl == PFn) ? alloc(ctx, static_cast<size_t>(n + 1)) + : emalloc(static_cast<size_t>(n + 1))); + vsnprintf(p, static_cast<size_t>(n + 1), fmt, ap2); + va_end(ap2); + return p; +} + +// die + +// die context handover (emit phase has no explicit Ctx*, bypassed via thread_local) +static thread_local Ctx* tl_die_ctx = nullptr; + +void set_die_ctx(Ctx* c) { + tl_die_ctx = c; +} + +const char* die_ctx_str(Ctx* ctx, Ins* cur) { + static thread_local char buf[2048]; + char* p = buf; + char* end = buf + sizeof(buf); + if (!ctx) + ctx = tl_die_ctx; + if (!ctx || !ctx->fn) { + snprintf(buf, sizeof(buf), "no_fn"); + return buf; + } + Fn* fn = ctx->fn; + + int fp_par = 0, gp_par = 0; + if (fn->start) + for (uint j = 0; j < fn->start->nins; ++j) { + Ins* ii = &fn->start->ins[j]; + if (ii->op != Ocopy || !isreg(ii->arg[0])) + break; + if (KBASE(ii->cls) == 1) + ++fp_par; + else + ++gp_par; + } + int ncall = 0, max_loop = 0; + for (Blk* b = fn->start; b; b = b->link) { + if (b->loop > max_loop) + max_loop = b->loop; + for (uint j = 0; j < b->nins; ++j) + if (b->ins[j].op == Ocall) + ++ncall; + } + p += snprintf(p, static_cast<size_t>(end - p), + "nblk=%u ntmp=%d ncon=%d slot=%d fpar=%d gpar=%d ncall=%d mxloop=%d fplive=%d", + fn->nblk, fn->ntmp, fn->ncon, fn->slot, fp_par, gp_par, ncall, max_loop, + ctx->spill_fp_live_peak); + + if (!cur) + return buf; + Blk* host = nullptr; + uint pos = 0; + for (Blk* b = fn->start; b; b = b->link) { + if (cur >= b->ins && cur < b->ins + b->nins) { + host = b; + pos = static_cast<uint>(cur - b->ins); + break; + } + } + if (!host) { + snprintf(p, static_cast<size_t>(end - p), " host=none"); + return buf; + } + p += snprintf(p, static_cast<size_t>(end - p), " blk%u/L%d nins=%u idx=%u ", host->id, + host->loop, host->nins, pos); + uint lo = pos > 10 ? pos - 10 : 0; + uint hi = pos + 10 < host->nins ? pos + 10 : host->nins - 1; + for (uint j = lo; j <= hi && p < end - 32; ++j) { + Ins* ii = &host->ins[j]; + p += snprintf(p, static_cast<size_t>(end - p), "%s%d/%c,", j == pos ? "*" : "", ii->op, + "wlsd"[ii->cls]); + } + return buf; +} + +[[noreturn]] void die_(const char* file, const char* s, ...) { + va_list ap; + fprintf(stderr, "%s: dying: ", file); + va_start(ap, s); + vfprintf(stderr, s, ap); + va_end(ap); + fputc('\n', stderr); + abort(); +} + +[[noreturn]] void err(char* s, ...) { + va_list ap; + fprintf(stderr, "rcc: error: "); + va_start(ap, s); + vfprintf(stderr, s, ap); + va_end(ap); + fputc('\n', stderr); + abort(); +} + +// BSet Bitset Operations + +void bsinit(Ctx* ctx, BSet* bs, uint n) { + n = (n + NBit - 1) / NBit; + bs->nt = n; + bs->t = static_cast<bits*>(alloc(ctx, n * sizeof(bits))); +} + +void bszero(BSet* bs) { + memset(bs->t, 0, bs->nt * sizeof(bits)); +} + +uint bscount(BSet* bs) { + uint c = 0; + for (uint i = 0; i < bs->nt; i++) { + bits b = bs->t[i]; + // Bitwise count: Hamming Weight (divide and conquer) + b = b - ((b >> 1) & 0x5555555555555555ULL); + b = (b & 0x3333333333333333ULL) + ((b >> 2) & 0x3333333333333333ULL); + b = (b + (b >> 4)) & 0x0f0f0f0f0f0f0f0fULL; + c += static_cast<uint>((b * 0x0101010101010101ULL) >> 56); + } + return c; +} + +void bsset(BSet* bs, uint elt) { + assert(elt < bs->nt * NBit); + bs->t[elt / NBit] |= BIT(elt % NBit); +} + +void bsclr(BSet* bs, uint elt) { + assert(elt < bs->nt * NBit); + bs->t[elt / NBit] &= ~BIT(elt % NBit); +} + +void bscopy(BSet* a, BSet* b) { + assert(a->nt == b->nt); + memcpy(a->t, b->t, a->nt * sizeof(bits)); +} + +void bsunion(BSet* a, BSet* b) { + assert(a->nt == b->nt); + for (uint i = 0; i < a->nt; i++) + a->t[i] |= b->t[i]; +} + +void bsinter(BSet* a, BSet* b) { + assert(a->nt == b->nt); + for (uint i = 0; i < a->nt; i++) + a->t[i] &= b->t[i]; +} + +void bsdiff(BSet* a, BSet* b) { + assert(a->nt == b->nt); + for (uint i = 0; i < a->nt; i++) + a->t[i] &= ~b->t[i]; +} + +int bsequal(BSet* a, BSet* b) { + assert(a->nt == b->nt); + for (uint i = 0; i < a->nt; i++) + if (a->t[i] != b->t[i]) + return 0; + return 1; +} + +/// Find index of lowest set bit in 64-bit word. For single bit (pow2) input, equivalent to log2. +int lowest_bit(bits b) { + // De Bruijn sequence method + static const int debruijn[64] = { + 0, 1, 2, 53, 3, 7, 54, 27, 4, 38, 41, 8, 34, 55, 48, 28, 62, 5, 39, 46, 44, 42, + 22, 9, 24, 35, 59, 56, 49, 18, 29, 11, 63, 52, 6, 26, 37, 40, 33, 47, 61, 45, 43, 21, + 23, 58, 17, 10, 51, 25, 36, 32, 60, 20, 57, 16, 50, 31, 19, 15, 30, 14, 13, 12, + }; + return debruijn[((b & (~b + 1)) * 0x022fdd63cc95386dULL) >> 58]; +} + +int bsiter(BSet* bs, int* elt) { + uint i = static_cast<uint>(*elt); + uint t = i / NBit; + if (t >= bs->nt) + return 0; + + bits b = bs->t[t] & ~(BIT(i % NBit) - 1); + while (!b) { + if (++t >= bs->nt) + return 0; + b = bs->t[t]; + } + *elt = static_cast<int>(NBit * t + static_cast<uint>(lowest_bit(b))); + return 1; +} + +// Instruction Operation Helpers + +void addins(Ctx* ctx, Ins** pi, uint* ni, Ins* ins) { + if (ins->op == Onop) + return; + vgrow(ctx, pi, *ni + 1); + (*pi)[*ni] = *ins; + (*ni)++; +} + +void addbins(Ctx* ctx, Ins** pi, uint* ni, Blk* b) { + for (uint j = 0; j < b->nins; j++) + addins(ctx, pi, ni, &b->ins[j]); +} + +void emit(Ctx* ctx, int op, int k, Ref to, Ref a0, Ref a1) { + if (ctx->curi == ctx->insb) + die("emit, too many instructions (NIns=%d) op=%d cls=%d " + "to=t%d/v%u a0=t%d/v%u a1=t%d/v%u || %s", + NIns, op, k, rtype(to), to.val, rtype(a0), a0.val, rtype(a1), a1.val, + die_ctx_str(ctx, nullptr)); + --ctx->curi; + ctx->curi->op = static_cast<uint>(op); + ctx->curi->cls = static_cast<uint>(k); + ctx->curi->to = to; + ctx->curi->arg[0] = a0; + ctx->curi->arg[1] = a1; +} + +void emiti(Ctx* ctx, Ins i) { + emit(ctx, i.op, i.cls, i.to, i.arg[0], i.arg[1]); +} + +Ins* icpy(Ins* dst, Ins* src, ulong n) { + if (n) + memmove(dst, src, n * sizeof(Ins)); + return dst + n; +} + +void idup(Ctx* ctx, Blk* b, Ins* src, ulong n) { + vgrow(ctx, &b->ins, n); + icpy(b->ins, src, n); + b->nins = static_cast<uint>(n); +} + +void igroup(Blk* b, Ins* i, Ins** i0, Ins** i1) { + Ins* ib = b->ins; + Ins* ie = ib + b->nins; + + switch (i->op) { + case Oblit0: + *i0 = i; + *i1 = i + 2; + return; + case Oblit1: + *i0 = i - 1; + *i1 = i + 1; + return; + case Osel0: + case Osel1: + // Backtrack to the leading sel0 from anywhere in the group, then consume consecutive + // sel1s forward + while (i->op == Osel1) + --i; + assert(i->op == Osel0); + *i0 = i; + for (++i; i < ie && i->op == Osel1; ++i) + ; + *i1 = i; + return; + } + + if (ispar(i->op)) { + while (i > ib && ispar((i - 1)->op)) + --i; + *i0 = i; + while (i < ie && ispar(i->op)) + ++i; + *i1 = i; + } else if (i->op == Ocall || isarg(i->op)) { + while (i > ib && isarg((i - 1)->op)) + --i; + *i0 = i; + while (i < ie && i->op != Ocall) + ++i; + assert(i < ie); + *i1 = i + 1; + } else { + *i0 = i; + *i1 = i + 1; + } +} + +int argcls(Ins* i, int n) { + return optab[i->op].argcls[n][i->cls]; +} + +// Tmp / Con / Comparison Helpers + +Ref newtmp(Ctx* ctx, const char* prfx, int k, Fn* fn) { + static int cnt; + int t = fn->ntmp++; + vgrow(ctx, &fn->tmp, static_cast<ulong>(fn->ntmp)); + memset(&fn->tmp[t], 0, sizeof(Tmp)); + if (prfx) + fn->tmp[t].name = strf(ctx, PFn, "%s.%d", prfx, ++cnt); + fn->tmp[t].cls = static_cast<short>(k); + fn->tmp[t].slot = -1; + fn->tmp[t].nuse = 1; + fn->tmp[t].ndef = 1; + return Ref::tmp(static_cast<uint>(t)); +} + +void chuse(Ctx*, Ref r, int du, Fn* fn) { + if (rtype(r) == RTmp) + fn->tmp[r.val].nuse += static_cast<uint>(du); +} + +int symeq(Sym a, Sym b) { + return a.type == b.type && a.id == b.id; +} + +Ref newcon(Ctx* ctx, Con* c0, Fn* fn) { + for (int i = 1; i < fn->ncon; i++) { + Con* c1 = &fn->con[i]; + if (c0->type == c1->type && symeq(c0->sym, c1->sym) && c0->bits.i == c1->bits.i) + return Ref::con(static_cast<uint>(i)); + } + int i = fn->ncon++; + vgrow(ctx, &fn->con, static_cast<ulong>(fn->ncon)); + fn->con[i] = *c0; + return Ref::con(static_cast<uint>(i)); +} + +Ref getcon(Ctx* ctx, int64_t val, Fn* fn) { + for (int i = 1; i < fn->ncon; i++) + if (fn->con[i].type == Con::CBits && fn->con[i].bits.i == val) + return Ref::con(static_cast<uint>(i)); + int i = fn->ncon++; + vgrow(ctx, &fn->con, static_cast<ulong>(fn->ncon)); + memset(&fn->con[i], 0, sizeof(Con)); + fn->con[i].type = Con::CBits; + fn->con[i].bits.i = val; + return Ref::con(static_cast<uint>(i)); +} + +int addcon(Con* c0, Con* c1, int m) { + if (m != 1 && c1->type == Con::CAddr) + return 0; + if (c0->type == Con::CUndef) { + *c0 = *c1; + c0->bits.i *= m; + } else { + if (c1->type == Con::CAddr) { + if (c0->type == Con::CAddr) + return 0; + c0->type = Con::CAddr; + c0->sym = c1->sym; + } + c0->bits.i += c1->bits.i * m; + } + return 1; +} + +int isconbits(Fn* fn, Ref r, int64_t* v) { + if (rtype(r) == RCon) { + Con* c = &fn->con[r.val]; + if (c->type == Con::CBits) { + *v = c->bits.i; + return 1; + } + } + return 0; +} + +int iscmp(int op, int* pk, int* pc) { + if (Ocmpw <= op && op <= Ocmpw1) { + *pc = op - Ocmpw; + *pk = Kw; + } else if (Ocmpl <= op && op <= Ocmpl1) { + *pc = op - Ocmpl; + *pk = Kl; + } else if (Ocmps <= op && op <= Ocmps1) { + *pc = NCmpI + op - Ocmps; + *pk = Ks; + } else if (Ocmpd <= op && op <= Ocmpd1) { + *pc = NCmpI + op - Ocmpd; + *pk = Kd; + } else + return 0; + return 1; +} + +/// Negation / Swap table for comparison operations +static const int cmptab[][2] = { + /* negate swap */ + /* Cieq */ {Cine, Cieq}, + /* Cine */ {Cieq, Cine}, + /* Cisge */ {Cislt, Cisle}, + /* Cisgt */ {Cisle, Cislt}, + /* Cisle */ {Cisgt, Cisge}, + /* Cislt */ {Cisge, Cisgt}, + /* Ciuge */ {Ciult, Ciule}, + /* Ciugt */ {Ciule, Ciult}, + /* Ciule */ {Ciugt, Ciuge}, + /* Ciult */ {Ciuge, Ciugt}, +}; + +static const int fcmptab[][2] = { + /* negate swap */ + /* Cfeq */ {-1, Cfeq}, + /* Cfge */ {-1, Cfle}, + /* Cfgt */ {-1, Cflt}, + /* Cfle */ {-1, Cfge}, + /* Cflt */ {-1, Cfgt}, + /* Cfne */ {-1, Cfne}, + /* Cfo */ {-1, Cfo}, + /* Cfuo */ {-1, Cfuo}, +}; + +int cmpop(int c) { + assert(0 <= c && c < NCmp); + if (c < NCmpI) + return cmptab[c][1]; + return NCmpI + fcmptab[c - NCmpI][1]; +} + +int cmpwlneg(int op) { + if (INRANGE(op, Ocmpw, Ocmpw1)) + return cmptab[op - Ocmpw][0] + Ocmpw; + if (INRANGE(op, Ocmpl, Ocmpl1)) + return cmptab[op - Ocmpl][0] + Ocmpl; + die("not a wl comparison"); +} + +int clsmerge(short* pk, short k) { + short k1 = *pk; + if (k1 == Kx) { + *pk = k; + return 0; + } + if ((k1 == Kw && k == Kl) || (k1 == Kl && k == Kw)) { + *pk = Kw; + return 0; + } + return k1 != k; +} + +int phicls(int t, Tmp* tmp) { + int t1 = tmp[t].phi; + if (!t1) + return t; + t1 = phicls(t1, tmp); + tmp[t].phi = t1; + return t1; +} + +uint phiargn(Phi* p, Blk* b) { + if (p) + for (uint n = 0; n < p->narg; n++) + if (p->blk[n] == b) + return n; + return ~0u; +} + +Ref phiarg(Phi* p, Blk* b) { + uint n = phiargn(p, b); + assert(n != ~0u && "block not found"); + return p->arg[n]; +} + +void dumpts(BSet* bs, Tmp* tmp, FILE* f) { + int t; + fprintf(f, "["); + for (t = Tmp0; bsiter(bs, &t); t++) + fprintf(f, " %s", tmp[t].name); + fprintf(f, " ]\n"); +} + +} // namespace rcc |
