use rucc_base::{Interner, Symbol};
use rucc_ir::{Abi, AttrSet, Extra, Func, Inst, Opcode, Value};
use rucc_mir as mir;
use rucc_target::{FrameInsts, RegClass};
const TWICE: &[&str] = &["setjmp", "sigsetjmp", "savectx", "vfork", "getcontext"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tail {
pub call: mir::Inst,
pub returns: Vec<mir::Inst>,
}
#[must_use]
pub fn refusal(func: &Func, names: &Interner) -> Option<&'static str> {
if func.attrs.set.contains(AttrSet::NAKED) {
return Some("the function is naked and writes its own ending");
}
let sret =
func.signature().params.first().is_some_and(|param| matches!(param.abi, Abi::Sret { .. }));
if sret {
return Some("the function gives its answer back through memory it was handed");
}
for block in func.blocks() {
for inst in func.insts(block) {
match func[inst].opcode {
Opcode::Alloca => return Some("a local lives in the frame"),
Opcode::VaStart => return Some("the function reads its own variable arguments"),
Opcode::ApplyArgs => return Some("the function keeps its arguments in the frame"),
Opcode::SetjmpMarker => return Some("the function saves a place to come back to"),
Opcode::Call if twice(func, inst, names) => {
return Some("the function calls something that comes back twice");
}
_ => (),
}
}
}
None
}
fn twice(func: &Func, inst: Inst, names: &Interner) -> bool {
let Extra::Call(info) = func[inst].extra else { return false };
let Some(callee) = func[info].callee else { return false };
TWICE.contains(&names.resolve(callee).trim_start_matches('_'))
}
pub fn mark(func: &mut Func, names: &Interner) -> usize {
if refusal(func, names).is_some() {
return 0;
}
let blocks: Vec<_> = func.blocks().collect();
let mut marked = 0;
for block in blocks {
let insts: Vec<Inst> = func.insts(block).collect();
let [.., call, ret] = insts[..] else { continue };
if !in_tail_position(func, call, ret) {
continue;
}
func.remove_inst(ret);
func[call].opcode = Opcode::TailCall;
marked += 1;
}
marked
}
fn in_tail_position(func: &Func, call: Inst, ret: Inst) -> bool {
if func[ret].opcode != Opcode::Return || func[call].opcode != Opcode::Call {
return false;
}
let Extra::Call(info) = func[call].extra else { return false };
let info = func[info];
if info.callee.is_none() {
return false;
}
let results: Vec<Value> = func[call].results().collect();
if func[func[ret].args] != results[..] {
return false;
}
func[info.signature].returns == func.signature().returns
}
pub fn jumps(
func: &mut mir::Func,
tails: &[Tail],
insts: &FrameInsts,
names: &mut Interner,
) -> usize {
let Some(away) = insts.away else { return 0 };
let ret = mir::Opcode::new(names.intern(&format!("{}{}", insts.prefix, insts.ret)));
let away = mir::Opcode::new(names.intern(&format!("{}{away}", insts.prefix)));
let mut jumped = 0;
for tail in tails {
let Some((last, callee)) = ending(func, tail, ret) else { continue };
let span = func.span(tail.call);
func.remove_inst(tail.call);
for &pseudo in &tail.returns {
func.remove_inst(pseudo);
}
func[last].opcode = away;
func[last].symbol = Some(callee);
func.set_span(last, span);
jumped += 1;
}
jumped
}
fn ending(func: &mir::Func, tail: &Tail, ret: mir::Opcode) -> Option<(mir::Inst, Symbol)> {
let block = func.block_of(tail.call)?;
let callee = func[tail.call].symbol?;
if !func[block].succs.is_empty() {
return None;
}
let last = func.insts(block).last()?;
if func[last].opcode != ret {
return None;
}
let named: Vec<(mir::Reg, RegClass)> =
func[func[tail.call].operands].iter().map(|operand| (operand.reg, operand.class)).collect();
let mut at = func.next_inst(tail.call)?;
while at != last {
if !tail.returns.contains(&at) {
let data = &func[at];
let touches = func[data.operands]
.iter()
.any(|operand| named.contains(&(operand.reg, operand.class)));
if touches || data.symbol.is_some() {
return None;
}
}
at = func.next_inst(at)?;
}
Some((last, callee))
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Block, Builder, Flags, Func, InstData, Opcode, Signature, Type, Value};
use super::{mark, refusal};
fn caller(
names: &mut Interner,
between: impl FnOnce(&mut Func, Block, Value) -> Value,
) -> Func {
let i32 = Type::int(32);
let mut func =
Func::new(names.intern("f"), Signature::new().with_params(&[i32]).with_returns(&[i32]));
let block = func.create_block();
let arg = func.append_param(block, i32);
let sig = func.add_signature(Signature::new().with_params(&[i32]).with_returns(&[i32]));
let callee = names.intern("g");
let call = Builder::new(&mut func, block).call(callee, sig, &[arg]);
let got = func[call].first_result.expect("an integer comes back");
let answer = between(&mut func, block, got);
Builder::new(&mut func, block).ret(&[answer]);
func
}
fn opcodes(func: &Func) -> Vec<Opcode> {
func.blocks()
.flat_map(|block| func.insts(block).map(|inst| func[inst].opcode).collect::<Vec<_>>())
.collect()
}
#[test]
fn a_call_whose_answer_is_returned_becomes_a_tail_call() {
let mut names = Interner::new();
let mut func = caller(&mut names, |_, _, got| got);
assert_eq!(mark(&mut func, &names), 1);
assert_eq!(opcodes(&func), [Opcode::TailCall]);
}
#[test]
fn a_call_with_work_after_it_is_not_in_tail_position() {
let mut names = Interner::new();
let mut func = caller(&mut names, |func, block, got| {
Builder::new(func, block).binary(Opcode::Add, got, got, Flags::default())
});
assert_eq!(mark(&mut func, &names), 0);
assert_eq!(opcodes(&func), [Opcode::Call, Opcode::Add, Opcode::Return]);
}
#[test]
fn a_local_in_the_frame_turns_down_the_whole_function() {
let mut names = Interner::new();
let mut func = caller(&mut names, |func, block, got| {
Builder::new(func, block).value(InstData::new(Opcode::Alloca), Type::PTR);
got
});
assert_eq!(refusal(&func, &names), Some("a local lives in the frame"));
assert_eq!(mark(&mut func, &names), 0);
}
#[test]
fn a_call_to_setjmp_anywhere_turns_down_the_whole_function() {
let mut names = Interner::new();
let setjmp = names.intern("_setjmp");
let mut func = caller(&mut names, |func, block, got| {
let sig = func.add_signature(Signature::new().with_returns(&[Type::int(32)]));
Builder::new(func, block).call(setjmp, sig, &[]);
got
});
assert_eq!(
refusal(&func, &names),
Some("the function calls something that comes back twice")
);
assert_eq!(mark(&mut func, &names), 0);
}
}