/* Source code for RCC-Legacy.
Copyright (C) 2026 Richard Qin
This file is part of RCC.
RCC is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3, or (at your option) any later
version.
RCC is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with RCC; see the file LICENSE. If not see
. */
/// @file print.cc
/// @brief IR text output: Fn / Blk / Phi / Ins / Jmp / Con / Ref
#include "rcc/all.h"
#include "rcc/irgen.h"
#include
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 a{};
#define X(n) a[J##n] = #n;
JMPS(X)
#undef X
return a;
}();
if (j <= Jxxx || j >= NJmp)
return "??";
return tab[static_cast(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(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(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(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(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(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(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& v, FILE* f) {
if (elem_cls == Ks) {
// QBE floating-point constant literal s_
float fv = std::holds_alternative(v) ? std::get(v)
: static_cast(std::get(v));
fprintf(f, "s s_%f", static_cast(fv));
} else {
int32_t iv = std::holds_alternative(v) ? std::get(v)
: static_cast(std::get(v));
fprintf(f, "w %d", iv);
}
}
} // namespace
void print_globals(const std::vector& 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