use std::collections::{HashMap, HashSet};
use rucc_base::Symbol;
use rucc_cost::heuristics::{INLINE_CALLED_ONCE_INSNS, INLINE_CALLED_ONCE_LOOP_DEPTH};
use rucc_ir::{
Abi, AsmInfo, AttrSet, Block, BlockCall, BlockCallList, CallInfo, Datum, Def, Drains, Extra,
Float, Func, FuncId, Imm, Inst, InstData, Linkage, MemInfo, MemOrder, Module, Opcode, Restrict,
Signature, SwitchInfo, Type, VaInfo, Value, ValueList,
};
use rucc_target::Isa;
use rucc_tuple::{Arch, Os};
use crate::Stats;
use crate::cfg::Cfg;
use crate::dom::Dominators;
use crate::loops::Loops;
pub const NAME: &str = "inline";
pub const ONCE: &str = "inline-functions-called-once";
const INLINED: &str = "always_inline call inlined";
const HINT_INLINED: &str = "inline call inlined";
const ONCE_INLINED: &str = "call to a static function called once inlined";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Always,
Hinted,
Once,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InlineFailure {
Recursive,
Mismatch,
ByValue,
VaStart,
ComputedGoto,
Setjmp,
ApplyArgs,
MemorySsa,
Pack,
Alloca,
TooLarge,
TooDeep,
Unwinds,
Target,
}
impl InlineFailure {
#[must_use]
pub const fn why(self) -> &'static str {
match self {
Self::Recursive => "always_inline call not inlined: recursive",
Self::Mismatch => "always_inline call not inlined: arguments do not match",
Self::ByValue => "always_inline call not inlined: structure passed by value",
Self::VaStart => "always_inline call not inlined: callee uses va_start",
Self::ComputedGoto => "always_inline call not inlined: callee has a computed goto",
Self::Setjmp => "always_inline call not inlined: callee calls setjmp",
Self::ApplyArgs => "always_inline call not inlined: callee uses __builtin_apply_args",
Self::MemorySsa => "always_inline call not inlined: memory SSA present",
Self::Pack => "always_inline call not inlined: va_arg_pack cannot be forwarded",
Self::Alloca => "always_inline call not inlined: callee calls alloca",
Self::TooLarge => "always_inline call not inlined: callee too large",
Self::TooDeep => "always_inline call not inlined: call inside too many loops",
Self::Unwinds => "always_inline call not inlined: call has a landing pad",
Self::Target => "always_inline call not inlined: target specific option mismatch",
}
}
#[must_use]
pub const fn hint(self) -> &'static str {
match self {
Self::Recursive => "inline call not inlined: recursive",
Self::Mismatch => "inline call not inlined: arguments do not match",
Self::ByValue => "inline call not inlined: structure passed by value",
Self::VaStart => "inline call not inlined: callee uses va_start",
Self::ComputedGoto => "inline call not inlined: callee has a computed goto",
Self::Setjmp => "inline call not inlined: callee calls setjmp",
Self::ApplyArgs => "inline call not inlined: callee uses __builtin_apply_args",
Self::MemorySsa => "inline call not inlined: memory SSA present",
Self::Pack => "inline call not inlined: va_arg_pack cannot be forwarded",
Self::Alloca => "inline call not inlined: callee calls alloca",
Self::TooLarge => "inline call not inlined: callee too large",
Self::TooDeep => "inline call not inlined: call inside too many loops",
Self::Unwinds => "inline call not inlined: call has a landing pad",
Self::Target => "inline call not inlined: target specific option mismatch",
}
}
#[must_use]
pub const fn once(self) -> &'static str {
match self {
Self::Recursive => "call to a function called once not inlined: recursive",
Self::Mismatch => "call to a function called once not inlined: arguments do not match",
Self::ByValue => {
"call to a function called once not inlined: structure passed by value"
}
Self::VaStart => "call to a function called once not inlined: callee uses va_start",
Self::ComputedGoto => {
"call to a function called once not inlined: callee has a computed goto"
}
Self::Setjmp => "call to a function called once not inlined: callee calls setjmp",
Self::ApplyArgs => {
"call to a function called once not inlined: callee uses __builtin_apply_args"
}
Self::MemorySsa => "call to a function called once not inlined: memory SSA present",
Self::Pack => {
"call to a function called once not inlined: va_arg_pack cannot be forwarded"
}
Self::Alloca => "call to a function called once not inlined: callee calls alloca",
Self::TooLarge => "call to a function called once not inlined: callee too large",
Self::TooDeep => {
"call to a function called once not inlined: call inside too many loops"
}
Self::Unwinds => "call to a function called once not inlined: call has a landing pad",
Self::Target => {
"call to a function called once not inlined: target specific option mismatch"
}
}
}
}
pub fn run(module: &mut Module, limit: Option<u32>, once: bool, isa: Isa) -> Vec<(FuncId, Stats)> {
let once = if limit.is_some() && once { called_once(module) } else { HashSet::new() };
let wanted: HashMap<Symbol, (FuncId, Kind)> = module
.funcs()
.filter(|&id| !module[id].is_declaration())
.filter_map(|id| {
let func = &module[id];
let set = func.attrs.set;
let kind = if set.contains(AttrSet::ALWAYS_INLINE) {
Kind::Always
} else if limit.is_none()
|| set.without(AttrSet::NOINLINE | AttrSet::OPTNONE | AttrSet::NAKED) != set
{
return None;
} else if func.linkage == Linkage::Internal
&& !set.contains(AttrSet::USED)
&& once.contains(&func.name)
{
Kind::Once
} else if set.contains(AttrSet::INLINE_HINT) {
Kind::Hinted
} else {
return None;
};
Some((func.name, (id, kind)))
})
.collect();
let mut done = Vec::new();
if !wanted.is_empty() {
let convention = Convention::of(module);
let mut state = HashMap::new();
let limit = limit.map_or(0, |limit| usize::try_from(limit).unwrap_or(usize::MAX));
let how = How { wanted: &wanted, convention, limit, isa };
for id in module.funcs().collect::<Vec<FuncId>>() {
settle(module, id, &how, &mut state, &mut done);
}
let (calls, elsewhere) = references(module);
for &(id, kind) in wanted.values() {
let func = &module[id];
let name = func.name;
let gone = match kind {
Kind::Once => true,
Kind::Always => {
func.linkage == Linkage::Internal && !func.attrs.set.contains(AttrSet::USED)
}
Kind::Hinted => false,
};
if gone && !calls.contains_key(&name) && !elsewhere.contains(&name) {
module[id] = declaration(&module[id]);
}
}
for &(id, _) in &done {
settle_operands(&mut module[id]);
}
}
withdraw(module);
done
}
fn settle_operands(func: &mut Func) {
let mut found = Vec::new();
for block in func.blocks() {
for inst in func.insts(block) {
if func[inst].opcode != Opcode::InlineAsm {
continue;
}
for &arg in &func[func[inst].args] {
let Def::Result { inst: def, .. } = func[arg].def else { continue };
if matches!(func[def].opcode, Opcode::IConst | Opcode::FConst) {
continue;
}
if let Some((imm, _)) = crate::fold::evaluated(func, arg, 8) {
found.push((def, imm));
}
}
}
}
for (def, imm) in found {
let at = func.add_imm(imm);
let data = &mut func[def];
data.opcode = Opcode::IConst;
data.flags = rucc_ir::Flags::NONE;
data.args = ValueList::EMPTY;
data.extra = Extra::Imm(at);
}
}
fn called_once(module: &Module) -> HashSet<Symbol> {
let (calls, elsewhere) = references(module);
calls
.into_iter()
.filter(|&(name, count)| count == 1 && !elsewhere.contains(&name))
.map(|(name, _)| name)
.collect()
}
fn references(module: &Module) -> (HashMap<Symbol, usize>, HashSet<Symbol>) {
let mut calls: HashMap<Symbol, usize> = HashMap::new();
let mut elsewhere = HashSet::new();
for id in module.funcs() {
let func = &module[id];
for inst in func.blocks().flat_map(|block| func.insts(block)) {
match func[inst].extra {
Extra::Call(info) if func[inst].opcode == Opcode::Call => {
if let Some(callee) = func[info].callee {
*calls.entry(callee).or_default() += 1;
}
}
Extra::Call(info) => elsewhere.extend(func[info].callee),
Extra::Symbol(name) => {
elsewhere.insert(name);
}
_ => {}
}
}
}
for id in module.globals() {
let init = module[id].init.map(|list| &module[list]).unwrap_or_default();
for datum in init {
if let Datum::Addr(reloc) | Datum::Away(reloc) | Datum::Apart { to: reloc, .. } = *datum
{
elsewhere.insert(module[reloc].symbol);
}
}
}
for id in module.aliases() {
elsewhere.insert(module[id].target);
}
(calls, elsewhere)
}
struct How<'a> {
wanted: &'a HashMap<Symbol, (FuncId, Kind)>,
convention: Convention,
limit: usize,
isa: Isa,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Settling,
Settled,
}
fn settle(
module: &mut Module,
id: FuncId,
how: &How<'_>,
state: &mut HashMap<FuncId, State>,
done: &mut Vec<(FuncId, Stats)>,
) {
if state.contains_key(&id) || module[id].is_declaration() {
return;
}
state.insert(id, State::Settling);
let optnone = module[id].attrs.set.contains(AttrSet::OPTNONE);
let calls: Vec<(Block, Inst, FuncId, Kind)> = {
let func = &module[id];
func.blocks()
.flat_map(|block| func.insts(block).map(move |inst| (block, inst)))
.filter_map(|(block, inst)| {
let Extra::Call(info) = func[inst].extra else { return None };
if func[inst].opcode != Opcode::Call {
return None;
}
let callee = func[info].callee?;
let &(callee, kind) = how.wanted.get(&callee)?;
(kind == Kind::Always || !optnone).then_some((block, inst, callee, kind))
})
.collect()
};
let deep: HashSet<Inst> = if calls.iter().any(|&(.., kind)| kind == Kind::Once) {
let func = &module[id];
let cfg = Cfg::new(func);
let loops = Loops::new(&cfg, &Dominators::new(&cfg));
let depth = |block| loops.innermost(block).map_or(0, |inner| loops.depth(inner) + 1);
calls
.iter()
.filter(|&&(block, _, _, kind)| {
kind == Kind::Once && depth(block) > INLINE_CALLED_ONCE_LOOP_DEPTH
})
.map(|&(_, inst, ..)| inst)
.collect()
} else {
HashSet::new()
};
let mut stats = Stats::new();
for (_, call, callee, kind) in calls {
let why = |failure: InlineFailure| match kind {
Kind::Always => failure.why(),
Kind::Hinted => failure.hint(),
Kind::Once => failure.once(),
};
if callee == id || state.get(&callee) == Some(&State::Settling) {
stats.missed(why(InlineFailure::Recursive));
continue;
}
if deep.contains(&call) {
stats.missed(why(InlineFailure::TooDeep));
continue;
}
if let Some(wanted) = module[callee].target {
if !module[id].target.unwrap_or(how.isa).covers(wanted) {
stats.missed(why(InlineFailure::Target));
continue;
}
}
settle(module, callee, how, state, done);
let most = match kind {
Kind::Always => usize::MAX,
Kind::Hinted => how.limit,
Kind::Once => INLINE_CALLED_ONCE_INSNS as usize,
};
if size(&module[callee]) > most {
stats.missed(why(InlineFailure::TooLarge));
continue;
}
match splice(module, id, call, callee, how.convention, kind) {
Ok(()) => stats.optimized(match kind {
Kind::Always => INLINED,
Kind::Hinted => HINT_INLINED,
Kind::Once => ONCE_INLINED,
}),
Err(failure) => stats.missed(why(failure)),
}
}
state.insert(id, State::Settled);
if !stats.is_empty() {
done.push((id, stats));
}
}
fn size(func: &Func) -> usize {
func.blocks().map(|block| func.insts(block).count()).sum()
}
fn splice(
module: &mut Module,
caller: FuncId,
call: Inst,
callee: FuncId,
convention: Convention,
kind: Kind,
) -> Result<(), InlineFailure> {
let stand_in = Func::new(module[caller].name, Signature::new());
let mut func = std::mem::replace(&mut module[caller], stand_in);
let result = check(&func, call, &module[callee], convention, kind)
.map(|plan| copy(&mut func, call, &module[callee], &plan));
module[caller] = func;
result
}
struct Plan {
fixed: usize,
extras: Vec<Value>,
abis: Vec<Abi>,
groups: Option<Vec<u32>>,
spills: HashMap<Inst, Vec<usize>>,
}
fn check(
func: &Func,
call: Inst,
callee: &Func,
convention: Convention,
kind: Kind,
) -> Result<Plan, InlineFailure> {
if func.unwinds_to_pad(call) {
return Err(InlineFailure::Unwinds);
}
let entry = callee.entry().ok_or(InlineFailure::Mismatch)?;
let params = &callee[entry].params;
let args = &func[func[call].args];
let Extra::Call(info) = func[call].extra else { return Err(InlineFailure::Mismatch) };
let signature = &func[func[info].signature];
if args.len() < params.len()
|| (args.len() > params.len() && !callee.signature().variadic)
|| args.iter().zip(params).any(|(&arg, ¶m)| func[arg].ty != callee[param].ty)
{
return Err(InlineFailure::Mismatch);
}
let returns: Vec<Type> = callee.signature().return_types().collect();
let results: Vec<Type> = func[call].results().map(|value| func[value].ty).collect();
if results.len() > returns.len() || results.iter().zip(&returns).any(|(a, b)| a != b) {
return Err(InlineFailure::Mismatch);
}
if kind != Kind::Always
&& callee.signature().params.iter().any(|param| matches!(param.abi, Abi::ByVal { .. }))
{
return Err(InlineFailure::ByValue);
}
let fixed = params.len();
let extras = args[fixed..].to_vec();
let abis = expand(&func[func[info].varargs], extras.len());
let groups = func.arg_groups(call).and_then(|groups| past(groups, fixed));
let mut plan = Plan { fixed, extras, abis, groups, spills: HashMap::new() };
let outer: Vec<(Type, Abi)> = args[..fixed]
.iter()
.enumerate()
.map(|(at, &arg)| (func[arg].ty, signature.params.get(at).map_or(Abi::Plain, |p| p.abi)))
.collect();
if callee.named_blocks().next().is_some() {
return Err(InlineFailure::ComputedGoto);
}
let mut packs = HashSet::new();
let mut counted = false;
for block in callee.blocks() {
for inst in callee.insts(block) {
match callee[inst].opcode {
Opcode::VaStart => return Err(InlineFailure::VaStart),
Opcode::IndirectBr => return Err(InlineFailure::ComputedGoto),
Opcode::Alloca if kind != Kind::Always && !callee[inst].args.is_empty() => {
return Err(InlineFailure::Alloca);
}
Opcode::SetjmpMarker => return Err(InlineFailure::Setjmp),
Opcode::ApplyArgs => return Err(InlineFailure::ApplyArgs),
Opcode::MemEntry => return Err(InlineFailure::MemorySsa),
Opcode::VaArgPack => packs.extend(callee[inst].results()),
Opcode::VaArgPackLen => counted = true,
_ => {}
}
}
}
if (counted || !packs.is_empty()) && plan.extras.iter().any(|&value| is_pack(func, value)) {
return Err(InlineFailure::Pack);
}
if packs.is_empty() {
return Ok(plan);
}
for block in callee.blocks() {
for inst in callee.insts(block) {
let data = &callee[inst];
let used = callee[data.args].iter().position(|value| packs.contains(value));
let passed = callee
.successors(inst)
.any(|to| callee[to.args].iter().any(|value| packs.contains(value)));
if passed {
return Err(InlineFailure::Pack);
}
let Some(at) = used else { continue };
let args = &callee[data.args];
let Extra::Call(inner) = data.extra else { return Err(InlineFailure::Pack) };
if at + 1 != args.len() || !matches!(data.opcode, Opcode::Call | Opcode::CallIndirect) {
return Err(InlineFailure::Pack);
}
let skip = usize::from(data.opcode == Opcode::CallIndirect);
let named = &callee[callee[inner].signature].params;
let written = &args[skip..at];
let anonymous = expand(&callee[callee[inner].varargs], written.len() + 1 - named.len());
let before: Vec<(Type, Abi)> = written
.iter()
.enumerate()
.map(|(index, &value)| {
let abi = match named.get(index) {
Some(param) => param.abi,
None => anonymous[index - named.len()],
};
(callee[value].ty, abi)
})
.collect();
let forwarded: Vec<(Type, Abi)> = plan
.extras
.iter()
.zip(&plan.abis)
.map(|(&value, &abi)| (func[value].ty, abi))
.collect();
let spills =
forwardable(convention, &outer, &before, &forwarded, plan.groups.as_deref())
.ok_or(InlineFailure::Pack)?;
if !spills.is_empty() {
plan.spills.insert(inst, spills);
}
}
}
Ok(plan)
}
fn is_pack(func: &Func, value: Value) -> bool {
matches!(func[value].def, Def::Result { inst, .. } if func[inst].opcode == Opcode::VaArgPack)
}
fn expand(abis: &[Abi], count: usize) -> Vec<Abi> {
if abis.is_empty() { vec![Abi::Plain; count] } else { abis.to_vec() }
}
fn past(groups: &[u32], fixed: usize) -> Option<Vec<u32>> {
let mut seen = 0;
let mut rest = groups.iter();
while seen < fixed {
seen += usize::try_from(*rest.next()?).ok()?;
}
(seen == fixed).then(|| rest.copied().collect())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Convention {
SysV,
Slots,
Other,
}
impl Convention {
fn of(module: &Module) -> Self {
match (module.tuple.arch(), module.tuple.os()) {
(Arch::X86_64, Os::Windows) => Self::Slots,
(Arch::X86_64, _) => Self::SysV,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Class {
Gpr(u32),
Sse,
Memory,
}
fn class(ty: Type, abi: Abi) -> Class {
if abi.indirect() && !matches!(abi, Abi::Sret { .. }) {
Class::Memory
} else if ty.is_vector() {
Class::Sse
} else if ty.is_float() {
if ty.format() == Some(Float::F80) { Class::Memory } else { Class::Sse }
} else if ty.is_int() && ty.bits() > 64 {
Class::Gpr(2)
} else {
Class::Gpr(1)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct Regs {
gpr: u32,
sse: u32,
}
impl Regs {
const GPR: u32 = 6;
const SSE: u32 = 8;
fn after(values: &[(Type, Abi)]) -> Self {
let mut regs = Self::default();
for &(ty, abi) in values {
regs.take(class(ty, abi));
}
regs
}
fn fits(self, gpr: u32, sse: u32) -> bool {
self.gpr + gpr <= Self::GPR && self.sse + sse <= Self::SSE
}
fn take(&mut self, class: Class) -> bool {
let (gpr, sse) = match class {
Class::Gpr(count) => (count, 0),
Class::Sse => (0, 1),
Class::Memory => return false,
};
let room = self.fits(gpr, sse);
if room {
self.gpr += gpr;
self.sse += sse;
}
room
}
}
fn forwardable(
convention: Convention,
outer: &[(Type, Abi)],
before: &[(Type, Abi)],
forwarded: &[(Type, Abi)],
groups: Option<&[u32]>,
) -> Option<Vec<usize>> {
match convention {
Convention::Slots => Some(Vec::new()),
Convention::Other => {
let count = |values: &[(Type, Abi)]| {
let mut ints = 0;
let mut floats = 0;
for &(ty, abi) in values {
if abi.indirect() {
return None;
}
if ty.is_float() || ty.is_vector() { floats += 1 } else { ints += 1 }
}
Some((ints, floats))
};
(count(outer).is_some() && count(outer) == count(before)).then(Vec::new)
}
Convention::SysV => {
let mut first = Regs::after(outer);
let mut second = Regs::after(before);
if first == second {
return Some(Vec::new());
}
let mut spills = Vec::new();
let mut at = 0;
for (index, &count) in groups?.iter().enumerate() {
let group = usize::try_from(count).ok().and_then(|n| forwarded.get(at..at + n))?;
at += group.len();
match *group {
[] => {}
[(ty, abi)] => {
if let Abi::ByVal { size, .. } = abi {
let small = size <= 16; if small && (second.gpr < first.gpr || second.sse < first.sse) {
return None;
}
continue;
}
first.take(class(ty, abi));
second.take(class(ty, abi));
}
_ => {
let mut gpr = 0;
let mut sse = 0;
for &(ty, abi) in group {
match class(ty, abi) {
Class::Gpr(count) => gpr += count,
Class::Sse => sse += 1,
Class::Memory => return None,
}
}
if !first.fits(gpr, sse) {
return None;
}
first.gpr += gpr;
first.sse += sse;
if second.fits(gpr, sse) {
second.gpr += gpr;
second.sse += sse;
} else {
spills.push(index);
}
}
}
}
(at == forwarded.len()).then_some(spills)
}
}
}
fn copy(func: &mut Func, call: Inst, callee: &Func, plan: &Plan) {
let block = func.block_of(call).expect("a call being inlined is in a block");
let entry = func.entry().expect("a function with a call in it has a body");
let after = func.create_block();
let mut forward = HashMap::new();
for result in func[call].results().collect::<Vec<Value>>() {
let ty = func[result].ty;
forward.insert(result, func.append_param(after, ty));
}
let moving: Vec<Inst> = func.insts(block).skip_while(|&inst| inst != call).skip(1).collect();
for inst in moving {
func.remove_inst(inst);
func.append_inst(after, inst);
}
let start = callee.entry().expect("checked to have a body");
let mut passed = func[func[call].args][..plan.fixed].to_vec();
for (at, param) in callee.signature().params.iter().enumerate().take(plan.fixed) {
if let Abi::ByVal { size, align, .. } = param.abi {
passed[at] = by_value(func, entry, call, passed[at], size, align);
}
}
let mut blocks = HashMap::new();
let mut values = HashMap::new();
for from in callee.blocks() {
let to = func.create_block();
if from == start {
values.extend(callee[from].params.iter().copied().zip(passed.iter().copied()));
} else {
for ¶m in &callee[from].params {
values.insert(param, func.append_param(to, callee[param].ty));
}
}
blocks.insert(from, to);
}
let at = func.span(call);
let body = callee.declared;
let spliced = |inst: Inst| {
let span = callee.span(inst);
let prologue = span == body || !body.contains(span.lo);
if span.is_dummy() || body.is_dummy() || !prologue { span } else { at }
};
let mut made = Vec::new();
for from in callee.blocks() {
for inst in callee.insts(from) {
let data = &callee[inst];
if data.opcode == Opcode::VaArgPack {
continue;
}
let opcode = match data.opcode {
Opcode::Return => Opcode::Jump,
Opcode::VaArgPackLen => Opcode::IConst,
opcode => opcode,
};
let types: Vec<Type> = data.results().map(|value| callee[value].ty).collect();
let shell = InstData { flags: data.flags, ..InstData::new(opcode) };
let fixed = opcode == Opcode::Alloca && data.args.is_empty();
let first = func.insts(entry).next().expect("an entry block ends in something");
let span = if fixed {
func.span(first)
} else {
spliced(inst)
};
let new = func.create_inst(shell, &types, span);
for (old, value) in data.results().zip(func[new].results().collect::<Vec<Value>>()) {
values.insert(old, value);
}
if fixed {
func.insert_before(new, first);
} else {
func.append_inst(blocks[&from], new);
}
made.push((inst, new));
}
}
let keep = func[call].results().count();
for (inst, new) in made {
let data = &callee[inst];
let mut args: Vec<Value> = callee[data.args]
.iter()
.filter(|value| values.contains_key(value))
.map(|value| values[value])
.collect();
let packed = args.len() != data.args.len();
let extra = if data.opcode == Opcode::Return {
args.truncate(keep);
let to = func.push_values(&args);
args.clear();
Extra::Targets(func.push_block_calls(&[BlockCall::new(after, to)]))
} else if data.opcode == Opcode::VaArgPackLen {
let count = plan.groups.as_ref().map_or(plan.extras.len(), Vec::len);
let count = i128::try_from(count).expect("fewer arguments than that");
Extra::Imm(func.add_imm(Imm::int(count, Type::int(32))))
} else {
match data.extra {
Extra::Imm(imm) => Extra::Imm(func.add_imm(callee[imm])),
Extra::Mem(mem) => Extra::Mem(func.add_mem(unscoped(callee[mem]))),
Extra::Rmw(op, mem) => Extra::Rmw(op, func.add_mem(unscoped(callee[mem]))),
Extra::Targets(list) => {
Extra::Targets(targets(func, callee, list, &blocks, &values))
}
Extra::Call(info) => {
let info = callee[info];
let mut forwarded = None;
let signature = callee[info.signature].clone();
let mut abis = callee[info.varargs].to_vec();
if packed {
let skip = usize::from(data.opcode == Opcode::CallIndirect);
let written = args.len() - skip - signature.params.len();
abis = expand(&abis, written + 1);
abis.truncate(written);
let spills = plan.spills.get(&inst).map_or(&[][..], Vec::as_slice);
forwarded = pass_on(func, entry, new, spills, plan, &mut args, &mut abis);
if abis.iter().all(|&abi| abi == Abi::Plain) {
abis.clear();
}
}
let signature = func.add_signature(signature);
let varargs = func.push_abis(&abis);
if let Some(groups) = callee.arg_groups(inst) {
let mut groups = groups.to_vec();
let known = if packed {
groups.pop();
forwarded.as_ref().map(|outer| groups.extend_from_slice(outer))
} else {
Some(())
};
if known.is_some() {
func.set_arg_groups(new, groups);
}
}
Extra::Call(func.add_call(CallInfo { callee: info.callee, signature, varargs }))
}
Extra::Switch(info) => {
let info = callee[info];
let cases = func.push_imms(&callee[info.cases]);
let targets = targets(func, callee, info.targets, &blocks, &values);
Extra::Switch(func.add_switch(SwitchInfo { targets, cases }))
}
Extra::Asm(info) => {
let info = callee[info];
let targets = targets(func, callee, info.targets, &blocks, &values);
Extra::Asm(func.add_asm(AsmInfo { targets, ..info }))
}
Extra::VaObject(info) => {
let info = callee[info];
let mem = func.add_mem(unscoped(callee[info.mem]));
let slots = func.push_slots(&callee[info.slots]);
Extra::VaObject(func.add_va_object(VaInfo { mem, slots }))
}
other => other,
}
};
func[new].args = if args.is_empty() { ValueList::EMPTY } else { func.push_values(&args) };
func[new].extra = extra;
}
let to = ValueList::EMPTY;
let targets = func.push_block_calls(&[BlockCall::new(blocks[&start], to)]);
let span = func.span(call);
let jump = func.create_inst(
InstData { extra: Extra::Targets(targets), ..InstData::new(Opcode::Jump) },
&[],
span,
);
crate::uses::substitute(func, &forward);
func.remove_inst(call);
func.append_inst(block, jump);
}
fn pass_on(
func: &mut Func,
entry: Block,
call: Inst,
spills: &[usize],
plan: &Plan,
args: &mut Vec<Value>,
abis: &mut Vec<Abi>,
) -> Option<Vec<u32>> {
let Some(groups) = plan.groups.as_deref().filter(|_| !spills.is_empty()) else {
args.extend_from_slice(&plan.extras);
abis.extend_from_slice(&plan.abis);
return plan.groups.clone();
};
let mut now = Vec::with_capacity(groups.len());
let mut at = 0;
for (index, &count) in groups.iter().enumerate() {
let end = at + count as usize;
if spills.contains(&index) {
let (slot, size) = spill(func, entry, call, &plan.extras[at..end]);
args.push(slot);
abis.push(Abi::ByVal { size, align: 8, drains: Drains::Nothing });
now.push(1);
} else {
args.extend_from_slice(&plan.extras[at..end]);
abis.extend_from_slice(&plan.abis[at..end]);
now.push(count);
}
at = end;
}
Some(now)
}
fn by_value(
func: &mut Func,
entry: Block,
call: Inst,
from: Value,
size: u64,
align: u32,
) -> Value {
let span = func.span(call);
let info = MemInfo {
size,
align,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let mem = func.add_mem(info);
let alloca = InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) };
let alloca = func.create_inst(alloca, &[Type::PTR], span);
let first = func.insts(entry).next().expect("an entry block ends in something");
func.insert_before(alloca, first);
let slot = func[alloca].results().next().expect("an alloca has a result");
let copy = InstData {
args: func.push_values(&[slot, from]),
extra: Extra::Mem(func.add_mem(info)),
..InstData::new(Opcode::Memcpy)
};
let copy = func.create_inst(copy, &[], span);
func.insert_before(copy, call);
slot
}
fn spill(func: &mut Func, entry: Block, call: Inst, pieces: &[Value]) -> (Value, u64) {
let span = func.span(call);
let bytes = |ty: Type| {
if ty == Type::PTR { 8 } else { u64::from(ty.bits() * ty.lanes()).div_ceil(8) }
};
let size: u64 = pieces.iter().map(|&piece| bytes(func[piece].ty).next_multiple_of(8)).sum();
let info = MemInfo {
size,
align: 8,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
};
let mem = func.add_mem(info);
let alloca = InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) };
let alloca = func.create_inst(alloca, &[Type::PTR], span);
let first = func.insts(entry).next().expect("an entry block ends in something");
func.insert_before(alloca, first);
let slot = func[alloca].results().next().expect("an alloca has a result");
let mut offset = 0;
for &piece in pieces {
let ty = func[piece].ty;
let width = bytes(ty);
let mut address = slot;
if offset != 0 {
let imm = func.add_imm(Imm::int(i128::from(offset), Type::int(64)));
let amount = InstData { extra: Extra::Imm(imm), ..InstData::new(Opcode::IConst) };
let amount = func.create_inst(amount, &[Type::int(64)], span);
func.insert_before(amount, call);
let amount = func[amount].results().next().expect("a constant has a result");
let add = InstData {
args: func.push_values(&[slot, amount]),
..InstData::new(Opcode::PtrAdd)
};
let add = func.create_inst(add, &[Type::PTR], span);
func.insert_before(add, call);
address = func[add].results().next().expect("an address has a result");
}
let info = MemInfo { size: width, ..info };
let store = InstData {
args: func.push_values(&[piece, address]),
extra: Extra::Mem(func.add_mem(info)),
..InstData::new(Opcode::Store)
};
let store = func.create_inst(store, &[], span);
func.insert_before(store, call);
offset += width.next_multiple_of(8);
}
(slot, size)
}
fn unscoped(info: MemInfo) -> MemInfo {
MemInfo { restrict: Restrict::NONE, ..info }
}
fn targets(
func: &mut Func,
callee: &Func,
list: BlockCallList,
blocks: &HashMap<Block, Block>,
values: &HashMap<Value, Value>,
) -> BlockCallList {
let calls: Vec<BlockCall> = callee[list]
.iter()
.map(|call| {
let args: Vec<Value> = callee[call.args].iter().map(|value| values[value]).collect();
let args = func.push_values(&args);
BlockCall { block: blocks[&call.block], args, hint: call.hint }
})
.collect();
func.push_block_calls(&calls)
}
fn withdraw(module: &mut Module) {
for id in module.funcs().collect::<Vec<FuncId>>() {
let func = &module[id];
if func.is_declaration() {
continue;
}
let holds = func
.blocks()
.flat_map(|block| func.insts(block))
.any(|inst| matches!(func[inst].opcode, Opcode::VaArgPack | Opcode::VaArgPackLen));
if !holds && !func.attrs.set.contains(AttrSet::INLINE_ONLY) {
continue;
}
module[id] = declaration(func);
}
}
fn declaration(func: &Func) -> Func {
let mut declared = Func::new(func.name, func.signature().clone());
declared.spelled = func.spelled;
declared.visibility = func.visibility;
declared.attrs = func.attrs;
declared.target = func.target;
declared.attrs.set = declared.attrs.set.without(AttrSet::INLINE_ONLY);
declared.declared = func.declared;
declared.linkage = Linkage::External;
declared
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_diag::Span;
use super::*;
const HEAD: &str = r#"; ModuleID = 't.c'
; format 0
target triple = "x86_64-unknown-linux-gnu"
target datalayout = "e-p:64:64-i64:64-f80:128-S128"
"#;
fn inlined(body: &str) -> String {
inlined_under(body, None)
}
fn inlined_under(body: &str, limit: Option<u32>) -> String {
inlined_with(body, limit, true).0
}
fn inlined_with(body: &str, limit: Option<u32>, once: bool) -> (String, String) {
let mut names = Interner::new();
let text = format!("{HEAD}{body}");
let mut module = rucc_ir::parse(&text, &mut names).expect("the fixture parses");
let said = format!("{:?}", run(&mut module, limit, once, Isa::baseline()));
if let Err(errors) = rucc_ir::verify(&module, &names) {
panic!("the inliner left invalid IR, {errors:?}\n{}", rucc_ir::print(&module, &names));
}
(rucc_ir::print(&module, &names), said)
}
#[test]
fn a_callee_built_for_more_than_its_caller_stays_a_call() {
let sse42 = "mmx,sse,sse2,sse3,ssse3,sse4.1,sse4.2,popcnt,crc32,fxsr";
for attrs in ["always_inline", "inline_hint"] {
let body = format!(
r#"
func @step(i32) -> i32, linkage(internal), attrs({attrs}), target "{sse42}" {{
block0(%0: i32):
%1 = add.i32 %0, %0
return %1
}}
func @plain(i32) -> i32, linkage(external) {{
block0(%0: i32):
%1 = call @step(%0) : (i32) -> i32
return %1
}}
func @fast(i32) -> i32, linkage(external), target "{sse42}" {{
block0(%0: i32):
%1 = call @step(%0) : (i32) -> i32
return %1
}}
"#
);
let (out, said) = inlined_with(&body, Some(100), false);
let plain = &out
[out.find("func @plain").expect("plain")..out.find("func @fast").expect("fast")];
let fast = &out[out.find("func @fast").expect("fast")..];
assert!(plain.contains("call @step"), "{attrs}: {out}");
assert!(!fast.contains("call @step"), "{attrs}: {out}");
assert!(said.contains("not inlined: target specific option mismatch"), "{said}");
}
}
#[test]
fn a_call_to_an_always_inline_function_is_replaced_by_its_body() {
let out = inlined(
r#"
func @twice(i32) -> i32, linkage(linkonce), attrs(always_inline) {
block0(%0: i32):
%1 = alloca, size 4, align 4
%2 = add.i32 %0, %0
return %2
}
func @g(i32) -> i32, linkage(external) {
block0(%0: i32):
%1 = call @twice(%0) : (i32) -> i32
%2 = add.i32 %1, %1
return %2
}
"#,
);
let g = &out[out.find("func @g").expect("g is there")..];
assert!(!g.contains("call @twice"), "{out}");
assert!(g.contains("alloca"), "{out}");
}
#[test]
fn a_static_always_inline_function_is_not_kept_once_every_call_is_inlined() {
let body = r#"
func @twice(i32) -> i32, linkage(internal), attrs(always_inline) {
block0(%0: i32):
%1 = mul.i32 %0, %0
return %1
}
func @g(i32) -> i32, linkage(external) {
block0(%0: i32):
%1 = call @twice(%0) : (i32) -> i32
return %1
}
"#;
let out = inlined(body);
assert_eq!(out.matches("mul ").count(), 1, "{out}");
assert!(!out.contains("linkage(internal)"), "{out}");
let kept = inlined(&body.replace("attrs(always_inline)", "attrs(always_inline, used)"));
assert_eq!(kept.matches("mul ").count(), 2, "{kept}");
}
#[test]
fn an_asm_operand_passed_as_constant_arithmetic_is_the_constant_once_inlined() {
let out = inlined(
r#"
func @round(i32), linkage(internal), attrs(always_inline) {
block0(%0: i32):
inline_asm.volatile "roundps %0, %%xmm0, %%xmm0", "i", "xmm0"(%0)
return
}
func @g(), linkage(external) {
block0:
%0 = iconst.i32 8
%1 = iconst.i32 1
%2 = or.i32 %0, %1
call @round(%2) : (i32)
return
}
"#,
);
assert!(out.contains("iconst.i32 9"), "{out}");
assert!(!out.contains("or."), "{out}");
}
fn respan(func: &mut Func, spans: &[Span]) {
let insts: Vec<Inst> = func.blocks().flat_map(|block| func.insts(block)).collect();
assert_eq!(insts.len(), spans.len(), "one span for each instruction");
let mut forward = HashMap::new();
for (inst, &span) in insts.into_iter().zip(spans) {
let data = func[inst];
let types: Vec<Type> = data.results().map(|value| func[value].ty).collect();
let new = func.create_inst(data, &types, span);
let old: Vec<Value> = func[inst].results().collect();
forward.extend(old.into_iter().zip(func[new].results().collect::<Vec<Value>>()));
func.insert_before(new, inst);
func.remove_inst(inst);
}
crate::uses::substitute(func, &forward);
}
#[test]
fn an_inlined_prologue_names_no_line_outside_the_caller() {
let text = format!(
r#"{HEAD}
func @twice(i32) -> i32, linkage(internal), attrs(always_inline) {{
block0(%0: i32):
%1 = alloca, size 4, align 4
store %0 -> %1, align 4
%2 = load.i32 %1, align 4
%3 = add.i32 %2, %2
return %3
}}
func @g(i32) -> i32, linkage(external) {{
block0(%0: i32):
%1 = alloca, size 4, align 4
%2 = call @twice(%0) : (i32) -> i32
return %2
}}
"#
);
let mut names = Interner::new();
let mut module = rucc_ir::parse(&text, &mut names).expect("the fixture parses");
let (brace, statement) = (Span::new(10, 50), Span::new(20, 30));
let (frame, call) = (Span::new(60, 100), Span::new(70, 80));
let g = names.intern("g");
for id in module.funcs().collect::<Vec<FuncId>>() {
let func = &mut module[id];
if func.name == g {
func.declared = frame;
respan(func, &[frame, call, call]);
} else {
func.declared = brace;
respan(func, &[brace, brace, statement, statement, statement]);
}
}
run(&mut module, None, true, Isa::baseline());
let id = module.funcs().find(|&id| module[id].name == g).expect("g is there");
let func = &module[id];
let mut seen = Vec::new();
for block in func.blocks() {
for inst in func.insts(block) {
let span = func.span(inst);
assert_ne!(span, brace, "{:?} kept the callee's brace", func[inst].opcode);
seen.push((func[inst].opcode, span));
}
}
assert!(seen.iter().all(|&(opcode, span)| opcode != Opcode::Alloca || span == frame));
assert!(seen.contains(&(Opcode::Store, call)), "{seen:?}");
assert!(seen.contains(&(Opcode::Load, statement)), "{seen:?}");
}
#[test]
fn a_pack_is_the_anonymous_arguments_of_the_call_inlined() {
let out = inlined(
r#"
func @inner(i32, ...) -> i32, linkage(external);
func @wrap(i32, ...) -> i32, linkage(linkonce), attrs(always_inline) {
block0(%0: i32):
%1 = va_arg_pack.i32
%2 = call @inner(%0, %1) : (i32, ...) -> i32
return %2
}
func @g(i64, f64) -> i32, linkage(external) {
block0(%0: i64, %1: f64):
%2 = iconst.i32 7
%3 = call @wrap(%2, %0, %1) : (i32, ...) -> i32
return %3
}
"#,
);
assert!(
out.contains("func @wrap(i32, ...) -> i32, linkage(external), attrs(always_inline);"),
"{out}"
);
assert!(!out.contains("va_arg_pack"), "{out}");
assert!(out.contains("call @inner(%"), "{out}");
}
#[test]
fn a_pack_length_is_the_count_of_the_anonymous_arguments() {
let out = inlined(
r#"
func @wrap(i32, ...) -> i32, linkage(linkonce), attrs(always_inline) {
block0(%0: i32):
%1 = va_arg_pack_len.i32
return %1
}
func @g(i64, f64) -> i32, linkage(external) {
block0(%0: i64, %1: f64):
%2 = iconst.i32 7
%3 = call @wrap(%2, %0, %1) : (i32, ...) -> i32
return %3
}
"#,
);
let g = &out[out.find("func @g").expect("g is there")..];
assert!(g.contains("iconst.i32 2"), "{out}");
assert!(!g.contains("call @wrap"), "{out}");
}
const HINTED: &str = r#"
func @bump(i32) -> i32, linkage(external), attrs(inline_hint) {
block0(%0: i32):
%1 = iconst.i32 1
%2 = add.i32 %0, %1
return %2
}
func @g(i32) -> i32, linkage(external) {
block0(%0: i32):
%1 = call @bump(%0) : (i32) -> i32
return %1
}
"#;
#[test]
fn a_small_function_declared_inline_is_inlined_above_o0() {
let out = inlined_under(HINTED, Some(70));
let g = &out[out.find("func @g").expect("g is there")..];
assert!(!g.contains("call @bump"), "{out}");
let out = inlined_under(HINTED, None);
assert!(out.contains("call @bump"), "{out}");
}
#[test]
fn a_function_declared_inline_over_the_limit_is_left_alone() {
let out = inlined_under(HINTED, Some(2));
assert!(out.contains("call @bump"), "{out}");
}
const ONCE: &str = r#"
func @scale(i32) -> i32, linkage(internal) {
block0(%0: i32):
%1 = iconst.i32 3
%2 = mul.i32 %0, %1
%3 = iconst.i32 1
%4 = add.i32 %2, %3
return %4
}
func @g(i32) -> i32, linkage(external) {
block0(%0: i32):
%1 = call @scale(%0) : (i32) -> i32
return %1
}
"#;
#[test]
fn a_static_function_called_once_is_inlined_above_o0() {
let out = inlined_under(ONCE, Some(2));
let g = &out[out.find("func @g").expect("g is there")..];
assert!(!g.contains("call @scale"), "{out}");
assert!(g.contains("mul %0"), "{out}");
let out = inlined_under(ONCE, None);
assert!(out.contains("call @scale"), "{out}");
}
#[test]
fn a_static_function_called_once_is_not_kept_once_inlined() {
let out = inlined_under(ONCE, Some(2));
assert_eq!(out.matches("mul ").count(), 1, "{out}");
assert!(!out.contains("linkage(internal)"), "{out}");
}
#[test]
fn a_static_function_called_twice_stays_a_call() {
let twice = ONCE.replace(
" %1 = call @scale(%0) : (i32) -> i32\n return %1",
" %1 = call @scale(%0) : (i32) -> i32\n %2 = call @scale(%1) : (i32) -> i32\n \
return %2",
);
assert_ne!(twice, ONCE);
let out = inlined_under(&twice, Some(70));
assert_eq!(out.matches("call @scale").count(), 2, "{out}");
}
#[test]
fn a_function_other_objects_can_call_stays_a_call_when_called_once() {
let external = ONCE.replace(
"@scale(i32) -> i32, linkage(internal)",
"@scale(i32) -> i32, linkage(external)",
);
assert_ne!(external, ONCE);
let out = inlined_under(&external, Some(70));
assert!(out.contains("call @scale"), "{out}");
}
#[test]
fn a_static_function_whose_address_is_taken_stays_a_call() {
let taken = ONCE.replace(
" %1 = call @scale(%0) : (i32) -> i32\n return %1",
" %1 = call @scale(%0) : (i32) -> i32\n %2 = global_addr @scale\n return %1",
);
assert_ne!(taken, ONCE);
let out = inlined_under(&taken, Some(70));
assert!(out.contains("call @scale"), "{out}");
}
fn nested(depth: usize) -> String {
let scale = &ONCE[..ONCE.find("func @g").expect("g is there")];
let (body, out) = (depth + 1, depth + 2);
let mut g = String::from(
"func @g(i32, i1) -> i32, linkage(external) {\nblock0(%0: i32, %1: i1):\n jump block1\n",
);
for header in 1..=depth {
let back = if header == 1 { out } else { header - 1 };
g += &format!("block{header}:\n br_if %1, block{}, block{back}\n", header + 1);
}
g += &format!(
"block{body}:\n %2 = call @scale(%0) : (i32) -> i32\n jump block{depth}\n"
);
g += &format!("block{out}:\n return %0\n}}\n");
format!("{scale}{g}")
}
#[test]
fn a_static_function_called_once_is_inlined_no_more_than_six_loops_deep() {
let (out, said) = inlined_with(&nested(6), Some(70), true);
assert!(!out.contains("call @scale"), "{out}");
assert!(!said.contains("too many loops"), "{said}");
let (out, said) = inlined_with(&nested(7), Some(70), true);
assert!(out.contains("call @scale"), "{out}");
assert!(said.contains("call inside too many loops"), "{said}");
}
#[test]
fn the_called_once_half_can_be_turned_off_alone() {
let (out, _) = inlined_with(ONCE, Some(70), false);
assert!(out.contains("call @scale"), "{out}");
let hinted = ONCE.replace("linkage(internal) {", "linkage(internal), attrs(inline_hint) {");
assert_ne!(hinted, ONCE);
let (out, _) = inlined_with(&hinted, Some(70), false);
assert!(!out.contains("call @scale"), "{out}");
}
#[test]
fn a_static_function_called_once_and_marked_noinline_stays_a_call() {
let kept = ONCE.replace("linkage(internal) {", "linkage(internal), attrs(noinline) {");
assert_ne!(kept, ONCE);
let out = inlined_under(&kept, Some(70));
assert!(out.contains("call @scale"), "{out}");
}
#[test]
fn each_copy_of_a_label_address_is_a_label_of_its_own() {
let out = inlined_under(
r#"
func @here() -> ptr, linkage(internal), attrs(inline_hint) {
block0:
jump block1
block1:
%0 = block_addr block1
return %0
}
func @g() -> i1, linkage(external) {
block0:
%0 = call @here() : () -> ptr
%1 = call @here() : () -> ptr
%2 = icmp eq %0, %1
return %2
}
"#,
Some(70),
);
let g = &out[out.find("func @g").expect("g is there")..];
assert!(!g.contains("call @here"), "{out}");
assert_eq!(g.matches("block_addr").count(), 2, "{out}");
}
#[test]
fn a_computed_goto_is_not_inlined() {
let out = inlined_under(
r#"
func @jump(ptr) -> i32, linkage(internal), attrs(inline_hint) {
block0(%0: ptr):
indirect_br %0, block1
block1:
%1 = iconst.i32 1
return %1
}
func @g(ptr) -> i32, linkage(external) {
block0(%0: ptr):
%1 = call @jump(%0) : (ptr) -> i32
return %1
}
"#,
Some(70),
);
assert!(out.contains("call @jump"), "{out}");
}
#[test]
fn a_recursive_always_inline_function_is_left_alone() {
let out = inlined(
r#"
func @r(i32) -> i32, linkage(linkonce), attrs(always_inline) {
block0(%0: i32):
%1 = call @r(%0) : (i32) -> i32
return %1
}
"#,
);
assert!(out.contains("call @r("), "{out}");
}
#[test]
fn a_structure_that_would_straddle_the_registers_goes_to_memory() {
let int = (Type::int(64), Abi::Plain);
let outer = [int];
let before = [int, int, int, int, int];
let forwarded = [int, int];
let sysv = |before: &[(Type, Abi)], groups| {
forwardable(Convention::SysV, &outer, before, &forwarded, groups)
};
assert_eq!(sysv(&before, Some(&[2])), Some(vec![0]));
assert_eq!(sysv(&before, Some(&[1, 1])), Some(Vec::new()));
assert_eq!(sysv(&before, None), None);
assert_eq!(sysv(&outer, None), Some(Vec::new()));
}
}