use std::collections::{HashMap, HashSet};
use rucc_base::{Interner, Symbol};
use rucc_ir::{
AbiList, AttrSet, Block, CallInfo, Datum, Def, Extra, Float, Func, FuncId, Global, Imm, Inst,
InstData, IntPred, Linkage, MemInfo, MemOrder, Module, Opcode, Pic, Restrict, Signature,
SymbolRef, Type, Value,
};
use crate::extents::vouched;
use crate::{Cfg, Fuel, Stats, uses};
pub const NAME: &str = "libcall";
const DEPTH: u32 = 4;
const CHAIN: u32 = 12;
const ROUNDS: u32 = 8;
const REPLACEMENTS: [&str; 16] = [
"__memcpy_chk",
"ceilf",
"floorf",
"fputc",
"fputs",
"fwrite",
"memcpy",
"nearbyintf",
"putchar",
"puts",
"rintf",
"roundf",
"strchr",
"strcpy",
"strlen",
"truncf",
];
const UNCHECKED: [&str; 18] = [
"__memcpy_chk",
"__strcat_chk",
"__strcpy_chk",
"__strncpy_chk",
"memcpy",
"memmove",
"mempcpy",
"memset",
"snprintf",
"sprintf",
"stpcpy",
"stpncpy",
"strcat",
"strcpy",
"strncat",
"strncpy",
"vsnprintf",
"vsprintf",
];
const SOURCES: [&str; 55] = [
"__fprintf_chk",
"__memcpy_chk",
"__memmove_chk",
"__mempcpy_chk",
"__memset_chk",
"__printf_chk",
"__snprintf_chk",
"__sprintf_chk",
"__stpcpy_chk",
"__stpncpy_chk",
"__strcat_chk",
"__strcpy_chk",
"__strncat_chk",
"__strncpy_chk",
"__vfprintf_chk",
"__vprintf_chk",
"__vsnprintf_chk",
"__vsprintf_chk",
"bcopy",
"ceil",
"floor",
"fprintf",
"fprintf_unlocked",
"fputs",
"fputs_unlocked",
"index",
"memchr",
"memcmp",
"memmove",
"mempcpy",
"nearbyint",
"printf",
"printf_unlocked",
"rindex",
"rint",
"round",
"sprintf",
"stpcpy",
"strcat",
"strchr",
"strcmp",
"strcpy",
"strcspn",
"strlen",
"strncat",
"strncmp",
"strncpy",
"strnlen",
"strpbrk",
"strrchr",
"strspn",
"strstr",
"trunc",
"vfprintf",
"vprintf",
];
#[derive(Debug, Clone, PartialEq, Eq)]
enum Plan {
Drop,
Answer(Answer),
Swap {
callee: Symbol,
signature: Signature,
args: Vec<Argument>,
answer: Option<Answer>,
},
Unchecked {
callee: Symbol,
drop: &'static [usize],
},
Narrow {
callee: Symbol,
signature: Signature,
arg: Value,
},
}
impl Plan {
fn rename(&mut self, renamed: &HashMap<Value, Value>) {
if renamed.is_empty() {
return;
}
let answer = match self {
Plan::Drop | Plan::Unchecked { .. } => None,
Plan::Narrow { arg, .. } => {
*arg = renamed.get(arg).copied().unwrap_or(*arg);
None
}
Plan::Answer(answer) => Some(answer),
Plan::Swap { args, answer, .. } => {
for arg in args {
if let Argument::Have(value) | Argument::At(value, _) = arg {
*value = renamed.get(value).copied().unwrap_or(*value);
}
}
answer.as_mut()
}
};
let value = match answer {
Some(Answer::Along(value, _) | Answer::Least { count: value, .. }) => value,
Some(Answer::Byte { of, .. } | Answer::Less { step: of, .. }) => of,
Some(Answer::Nowhere | Answer::Number(_)) | None => return,
};
*value = renamed.get(value).copied().unwrap_or(*value);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Answer {
Along(Value, u64),
Nowhere,
Number(i128),
Least {
count: Value,
len: u64,
},
Less {
len: u64,
step: Value,
},
Byte {
of: Value,
against: u8,
leading: bool,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Side {
First,
Last,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Set {
Inside,
Outside,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Argument {
Have(Value),
Char(u8),
Count(u64),
Text(Vec<u8>),
At(Value, u64),
}
struct Shapes {
held: HashMap<&'static str, Option<(Symbol, Signature)>>,
named: HashMap<&'static str, Option<(Symbol, Option<Signature>)>>,
}
impl Shapes {
fn of(module: &Module, names: &mut Interner) -> Self {
let mut held: HashMap<&'static str, Option<(Symbol, Signature)>> = REPLACEMENTS
.iter()
.map(|&name| (name, Some((names.intern(name), canonical(module, name)))))
.collect();
let mut named: HashMap<&'static str, Option<(Symbol, Option<Signature>)>> =
UNCHECKED.iter().map(|&name| (name, Some((names.intern(name), None)))).collect();
for id in module.funcs() {
let func = &module[id];
let spelled = func.spelled.unwrap_or(func.name);
if let Some(slot) = named.get_mut(names.resolve(spelled)) {
*slot = Some((func.name, Some(func.signature().clone())));
}
let Some(slot) = held.get_mut(names.resolve(spelled)) else { continue };
let declared = func.signature();
let agrees = slot.as_ref().is_some_and(|(_, want)| {
!declared.variadic
&& declared.param_types().eq(want.param_types())
&& declared.return_types().eq(want.return_types())
});
*slot = agrees.then(|| (func.name, declared.clone()));
}
for id in module.globals() {
let name = names.resolve(module[id].name);
if let Some(slot) = held.get_mut(name) {
*slot = None;
}
if let Some(slot) = named.get_mut(name) {
*slot = None;
}
}
for id in module.aliases() {
let name = names.resolve(module[id].name);
if let Some(slot) = held.get_mut(name) {
*slot = None;
}
if let Some(slot) = named.get_mut(name) {
*slot = None;
}
}
Self { held, named }
}
fn get(&self, name: &'static str) -> Option<(Symbol, Signature)> {
self.held.get(name)?.clone()
}
fn unchecked(&self, name: &str, want: &Signature) -> Option<Symbol> {
let (symbol, declared) = self.named.get(name)?.as_ref()?;
let agrees = declared.as_ref().is_none_or(|declared| {
declared.variadic == want.variadic
&& declared.param_types().eq(want.param_types())
&& declared.return_types().eq(want.return_types())
});
agrees.then_some(*symbol)
}
}
fn canonical(module: &Module, name: &str) -> Signature {
let int = int();
let size = size(module);
match name {
"puts" => Signature::new().with_params(&[Type::PTR]).with_returns(&[int]),
"putchar" => Signature::new().with_params(&[int]).with_returns(&[int]),
"fputc" => Signature::new().with_params(&[int, Type::PTR]).with_returns(&[int]),
"fputs" => Signature::new().with_params(&[Type::PTR, Type::PTR]).with_returns(&[int]),
"strchr" => Signature::new().with_params(&[Type::PTR, int]).with_returns(&[Type::PTR]),
"strlen" => Signature::new().with_params(&[Type::PTR]).with_returns(&[size]),
"strcpy" => {
Signature::new().with_params(&[Type::PTR, Type::PTR]).with_returns(&[Type::PTR])
}
"memcpy" => {
Signature::new().with_params(&[Type::PTR, Type::PTR, size]).with_returns(&[Type::PTR])
}
"ceilf" | "floorf" | "nearbyintf" | "rintf" | "roundf" | "truncf" => {
let float = Type::float(Float::F32);
Signature::new().with_params(&[float]).with_returns(&[float])
}
"__memcpy_chk" => Signature::new()
.with_params(&[Type::PTR, Type::PTR, size, size])
.with_returns(&[Type::PTR]),
_ => {
Signature::new().with_params(&[Type::PTR, size, size, Type::PTR]).with_returns(&[size])
}
}
}
const fn int() -> Type {
Type::int(32)
}
fn size(module: &Module) -> Type {
Type::int(module.datalayout.pointer_bits)
}
pub fn fold(
module: &mut Module,
names: &mut Interner,
no_builtin: &[String],
pic: Pic,
fuel: &mut Fuel,
) -> Vec<(FuncId, Stats)> {
let shapes = Shapes::of(module, names);
let standard: HashMap<Symbol, Symbol> =
module.funcs().filter_map(|id| Some((module[id].name, module[id].spelled?))).collect();
let library: HashSet<Symbol> = module
.funcs()
.filter(|&id| !module[id].is_declaration())
.map(|id| module[id].name)
.filter(|&name| {
let name = names.resolve(standard.get(&name).copied().unwrap_or(name));
SOURCES.contains(&name) || REPLACEMENTS.contains(&name)
})
.collect();
let mut texts: HashMap<Vec<u8>, Symbol> = HashMap::new();
let mut done = Vec::new();
for id in module.funcs().collect::<Vec<FuncId>>() {
if module[id].is_declaration()
|| library.contains(&module[id].name)
|| !mentions(&module[id], names, &standard)
{
continue;
}
let mut stats = Stats::new();
for _ in 0..ROUNDS {
let plans = {
let func = &module[id];
let site = Site {
module,
func,
cfg: &Cfg::new(func),
shapes: &shapes,
counts: &uses::count(func),
standard: &standard,
names,
no_builtin,
pic,
};
site.survey(fuel, &mut stats)
};
if plans.is_empty() {
break;
}
let mut renamed: HashMap<Value, Value> = HashMap::new();
for (inst, mut plan) in plans {
plan.rename(&renamed);
let made = apply(module, id, names, &mut texts, inst, plan);
for value in renamed.values_mut() {
if let Some(&to) = made.get(value) {
*value = to;
}
}
renamed.extend(made);
}
}
if stats.changed() {
done.push((id, stats));
}
}
done
}
fn mentions(func: &Func, names: &Interner, standard: &HashMap<Symbol, Symbol>) -> bool {
func.blocks().flat_map(|block| func.insts(block)).any(|inst| {
let data = &func[inst];
let Extra::Call(at) = data.extra else { return false };
data.opcode == Opcode::Call
&& func[at].callee.is_some_and(|callee| {
let spelled = standard.get(&callee).copied().unwrap_or(callee);
SOURCES.contains(&names.resolve(spelled))
})
})
}
struct Site<'a> {
module: &'a Module,
func: &'a Func,
cfg: &'a Cfg,
shapes: &'a Shapes,
counts: &'a [u32],
standard: &'a HashMap<Symbol, Symbol>,
names: &'a Interner,
no_builtin: &'a [String],
pic: Pic,
}
impl Site<'_> {
fn survey(&self, fuel: &mut Fuel, stats: &mut Stats) -> Vec<(Inst, Plan)> {
let mut plans = Vec::new();
for block in self.func.blocks().collect::<Vec<_>>() {
for inst in self.func.insts(block).collect::<Vec<Inst>>() {
let Some(plan) = self.plan(inst) else { continue };
if !fuel.take() {
stats.missed("call to the library folded");
continue;
}
stats.optimized(match &plan {
Plan::Drop => "call to the library that writes nothing removed",
Plan::Answer(_) => "call to the library whose answer is known folded",
Plan::Swap { .. } => "call to the library folded",
Plan::Unchecked { .. } => "checking call whose check cannot fail made plain",
Plan::Narrow { .. } => "rounding of a widened float done in float",
});
plans.push((inst, plan));
}
}
plans
}
fn plan(&self, inst: Inst) -> Option<Plan> {
let data = &self.func[inst];
if data.opcode != Opcode::Call || self.func.mem_in(inst).is_some() {
return None;
}
let ignored = data.results().all(|result| self.counts[result.index()] == 0);
let name = self.called(inst)?;
let args: Vec<Value> = self.func[data.args].to_vec();
match name {
"printf" if ignored => self.printf(&args, false),
"printf_unlocked" if ignored => self.printf(&args, true),
"fprintf" if ignored => self.fprintf(&args, false),
"fprintf_unlocked" if ignored => self.fprintf(&args, true),
"fputs" if ignored => self.fputs(&args, false),
"fputs_unlocked" if ignored => self.fputs(&args, true),
"__printf_chk" if ignored => self.printf(args.get(1..)?, false),
"vprintf" | "__vprintf_chk" if ignored => {
let format = if name == "vprintf" { 0 } else { 1 };
self.printf(&[*args.get(format)?], false)
}
"__fprintf_chk" if ignored => {
let mut rest = vec![*args.first()?];
rest.extend_from_slice(args.get(2..)?);
self.fprintf(&rest, false)
}
"vfprintf" | "__vfprintf_chk" if ignored => {
let format = if name == "vfprintf" { 1 } else { 2 };
self.fprintf(&[*args.first()?, *args.get(format)?], false)
}
"strstr" => self.strstr(data, &args),
"strchr" | "index" => self.strchr(data, &args, Side::First),
"strrchr" | "rindex" => self.strchr(data, &args, Side::Last),
"memchr" => self.memchr(data, &args),
"memcmp" => self.memcmp(inst, data, &args),
"strlen" => self.strlen(inst, data, &args),
"strnlen" => self.strnlen(data, &args),
"strcmp" => self.strcmp(data, &args),
"strncmp" => self.strncmp(data, &args),
"strcspn" => self.span(data, &args, Set::Outside),
"strspn" => self.span(data, &args, Set::Inside),
"strpbrk" => self.strpbrk(data, &args),
"strcpy" | "stpcpy" => self.strcpy(data, name, &args, ignored),
"strcat" => self.strcat(inst, data, &args),
"strncat" => self.strncat(data, &args),
"mempcpy" => self.mempcpy(data, &args, ignored),
"memmove" => self.memmove(data, &args),
"strncpy" => self.strncpy(data, &args),
"bcopy" => {
let [source, dest, count] = *args else { return None };
if data.results().next().is_some() {
return None;
}
self.moved(dest, source, count).map(|plan| match plan {
Plan::Answer(_) => Plan::Drop,
plan => plan,
})
}
"sprintf" => self.sprintf(data, &args, ignored),
"ceil" => self.narrow(data, &args, "ceilf"),
"floor" => self.narrow(data, &args, "floorf"),
"nearbyint" => self.narrow(data, &args, "nearbyintf"),
"rint" => self.narrow(data, &args, "rintf"),
"round" => self.narrow(data, &args, "roundf"),
"trunc" => self.narrow(data, &args, "truncf"),
"__memcpy_chk" | "__memmove_chk" | "__mempcpy_chk" | "__memset_chk" => {
self.memory_chk(data, name, &args, ignored)
}
"__strcpy_chk" | "__stpcpy_chk" => self.strcpy_chk(data, name, &args, ignored),
"__strncpy_chk" | "__stpncpy_chk" => self.strncpy_chk(data, name, &args, ignored),
"__strcat_chk" => self.strcat_chk(data, &args),
"__strncat_chk" => self.strncat_chk(data, &args),
"__sprintf_chk" | "__vsprintf_chk" => self.sprintf_chk(data, name, &args),
"__snprintf_chk" | "__vsnprintf_chk" => self.snprintf_chk(data, name, &args),
_ => None,
}
}
fn answers(&self, data: &InstData) -> Option<Type> {
let mut results = data.results();
let ty = self.func[results.next()?].ty;
(results.next().is_none() && ty.is_int() && !ty.is_vector()).then_some(ty)
}
fn places(&self, data: &InstData) -> bool {
let mut results = data.results();
results.next().is_some_and(|result| self.func[result].ty == Type::PTR)
&& results.next().is_none()
}
fn character(&self, value: Value) -> Option<u8> {
let (imm, ty) = crate::fold::evaluated(self.func, value, DEPTH)?;
u8::try_from(imm.signed(ty).rem_euclid(256)).ok()
}
fn count(&self, value: Value) -> Option<usize> {
let narrow = self.widened(value);
let (imm, ty) = crate::fold::evaluated(self.func, narrow, DEPTH)?;
(narrow == value || imm.signed(ty) >= 0).then_some(())?;
usize::try_from(imm.unsigned()).ok()
}
fn widened(&self, value: Value) -> Value {
let Def::Result { inst, .. } = self.func[value].def else { return value };
if !matches!(self.func[inst].opcode, Opcode::SExt | Opcode::ZExt) {
return value;
}
self.func[self.func[inst].args].first().copied().unwrap_or(value)
}
fn strchr(&self, data: &InstData, args: &[Value], side: Side) -> Option<Plan> {
if args.len() != 2 || self.func[args[0]].ty != Type::PTR || !self.places(data) {
return None;
}
let wanted = self.character(args[1])?;
let Some(text) = self.one(args[0]) else {
return match (wanted, side) {
(0, Side::Last) => {
self.call("strchr", vec![Argument::Have(args[0]), Argument::Char(0)])
}
_ => None,
};
};
let found = match (wanted, side) {
(0, _) => Some(text.len()),
(_, Side::First) => text.iter().position(|&byte| byte == wanted),
(_, Side::Last) => text.iter().rposition(|&byte| byte == wanted),
};
Some(Plan::Answer(match found {
Some(at) => Answer::Along(args[0], u64::try_from(at).ok()?),
None => Answer::Nowhere,
}))
}
fn memchr(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
(args.len() == 3).then_some(())?; if self.func[args[0]].ty != Type::PTR || !self.places(data) {
return None;
}
let wanted = self.character(args[1])?;
let count = self.count(args[2])?;
let bytes = self.raw(args[0])?;
let window = bytes.get(..count)?;
Some(Plan::Answer(match window.iter().position(|&byte| byte == wanted) {
Some(at) => Answer::Along(args[0], u64::try_from(at).ok()?),
None => Answer::Nowhere,
}))
}
fn strlen(&self, inst: Inst, data: &InstData, args: &[Value]) -> Option<Plan> {
if args.len() != 1 || self.func[args[0]].ty != Type::PTR {
return None;
}
self.answers(data)?;
if let Some(len) = self.length(args[0]) {
return Some(Plan::Answer(Answer::Number(i128::try_from(len).ok()?)));
}
if let Some(text) = self.stored(inst, args[0]) {
return Some(Plan::Answer(Answer::Number(i128::try_from(text.len()).ok()?)));
}
let Def::Result { inst, .. } = self.func[args[0]].def else { return None };
if self.func[inst].opcode != Opcode::PtrAdd {
return None;
}
let &[base, step] = &self.func[self.func[inst].args] else { return None };
let len = u64::try_from(self.literal(base)?.len()).ok()?;
if self.largest(step)? > u128::from(len) {
return None;
}
Some(Plan::Answer(Answer::Less { len, step }))
}
fn stored(&self, call: Inst, value: Value) -> Option<Vec<u8>> {
let (base, offset) = self.address(value)?;
let bytes = self.before(call, base)?;
let mut text = Vec::new();
for at in (offset..).take(bytes.len()) {
match *bytes.get(&at)? {
0 => return Some(text),
byte => text.push(byte),
}
}
None
}
fn before(&self, call: Inst, base: Value) -> Option<HashMap<i128, u8>> {
let size = self.extent(base)?;
let mut bytes: HashMap<i128, u8> = HashMap::new();
let mut block = self.func.block_of(call)?;
let mut from = Some(call);
'walk: for _ in 0..CHAIN {
let insts: Vec<Inst> = match from.take() {
Some(call) => self
.func
.insts_backwards(block)
.skip_while(|&inst| inst != call)
.skip(1)
.collect(),
None => self.func.insts_backwards(block).collect(),
};
for inst in insts {
if self.wrote(inst, base, size, &mut bytes).is_none() {
break 'walk;
}
}
let Some(pred) = self.only_way_in(block) else { break };
block = pred;
}
Some(bytes)
}
fn only_way_in(&self, block: Block) -> Option<Block> {
let mut live = self
.cfg
.predecessors(block)
.iter()
.copied()
.filter(|&pred| !self.func.insts(pred).any(|inst| self.never_back(inst)));
let first = live.next()?;
live.next().is_none().then_some(first)
}
fn never_back(&self, inst: Inst) -> bool {
if self.func[inst].opcode != Opcode::Call {
return false;
}
let Extra::Call(at) = self.func[inst].extra else { return false };
let Some(callee) = self.func[at].callee else { return false };
let Some(SymbolRef::Func(id)) = self.module.lookup(callee) else { return false };
let target = &self.module[id];
target.attrs.set.contains(AttrSet::NORETURN)
|| (target.entry().is_none()
&& matches!(self.called(inst), Some("abort" | "exit" | "_Exit" | "quick_exit")))
}
fn held(&self, call: Inst, value: Value, count: usize) -> Option<Vec<u8>> {
if let Some(bytes) = self.raw(value) {
return Some(bytes.get(..count)?.to_vec());
}
let (base, offset) = self.address(value)?;
let bytes = self.before(call, base)?;
(offset..).take(count).map(|at| bytes.get(&at).copied()).collect()
}
fn wrote(
&self,
inst: Inst,
base: Value,
size: u64,
bytes: &mut HashMap<i128, u8>,
) -> Option<()> {
let data = &self.func[inst];
if !data.opcode.writes_memory() {
return Some(());
}
let args = &self.func[data.args];
let (to, written) = match data.opcode {
Opcode::Store => {
let &[byte, to] = args else { return None };
if self.func[byte].ty != Type::int(8) {
return None;
}
(to, vec![u8::try_from(self.number(byte)?).ok()?])
}
Opcode::Call => {
let &to = args.first()?;
let count =
|| self.number(*args.get(2)?).filter(|&count| count <= u128::from(size));
let written = match self.called(inst)? {
"memset" => {
vec![self.character(*args.get(1)?)?; usize::try_from(count()?).ok()?]
}
"memcpy" => {
let count = usize::try_from(count()?).ok()?;
self.raw(*args.get(1)?)?.get(..count)?.to_vec()
}
"strcpy" => {
let mut text = self.one(*args.get(1)?)?;
text.push(0);
text
}
"memcmp" | "memchr" | "strcmp" | "strncmp" | "strlen" | "strchr" => {
return Some(());
}
_ => return None,
};
(to, written)
}
_ => return None,
};
let (root, at) = self.address(to)?;
if root != base {
return self.local(root).then_some(());
}
let end = at.checked_add(i128::try_from(written.len()).ok()?)?;
if at < 0 || end > i128::from(size) {
return None;
}
for (place, byte) in (at..).zip(written) {
bytes.entry(place).or_insert(byte);
}
Some(())
}
fn called(&self, inst: Inst) -> Option<&str> {
let Extra::Call(at) = self.func[inst].extra else { return None };
let callee = self.func[at].callee?;
let name = self.names.resolve(self.standard.get(&callee).copied().unwrap_or(callee));
(!self.no_builtin.iter().any(|it| it == name)).then_some(name)
}
fn extent(&self, value: Value) -> Option<u64> {
let Def::Result { inst, .. } = self.func[value].def else { return None };
let data = &self.func[inst];
if data.opcode != Opcode::Alloca || !self.func[data.args].is_empty() {
return None;
}
let Extra::Mem(mem) = data.extra else { return None };
Some(self.func[mem].size)
}
fn strnlen(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
if args.len() != 2 || self.func[args[0]].ty != Type::PTR {
return None;
}
let ty = self.answers(data)?;
if let Some(text) = self.literal(args[0]) {
let len = u64::try_from(text.len()).ok()?;
if let Some((imm, _)) = crate::fold::evaluated(self.func, args[1], DEPTH) {
let least = imm.unsigned().min(u128::from(len));
return Some(Plan::Answer(Answer::Number(i128::try_from(least).ok()?)));
}
if len == 0 {
return Some(Plan::Answer(Answer::Number(0)));
}
(self.func[args[1]].ty == ty).then_some(())?;
return Some(Plan::Answer(Answer::Least { count: args[1], len }));
}
let count = self.count(args[1])?;
let bytes = self.raw(args[0])?;
let window = bytes.get(..count.min(bytes.len()))?;
let len = match window.iter().position(|&byte| byte == 0) {
Some(at) => at,
None if window.len() == count => count,
None => return None,
};
Some(Plan::Answer(Answer::Number(i128::try_from(len).ok()?)))
}
fn memcmp(&self, call: Inst, data: &InstData, args: &[Value]) -> Option<Plan> {
(args.len() == 3).then_some(())?; if self.func[args[0]].ty != Type::PTR || self.func[args[1]].ty != Type::PTR {
return None;
}
let ty = self.answers(data)?;
let count = self.count(args[2])?;
if count == 0 {
return Some(Plan::Answer(Answer::Number(0)));
}
match (self.held(call, args[0], count), self.held(call, args[1], count)) {
(Some(left), Some(right)) => {
let differs = left.iter().zip(&right).find(|(this, that)| this != that);
let sign = differs.map_or(0, |(this, that)| if this < that { -1 } else { 1 });
Some(Plan::Answer(Answer::Number(sign)))
}
(Some(known), None) => self.byte(ty, &known, args[1], true, count),
(None, Some(known)) => self.byte(ty, &known, args[0], false, count),
(None, None) => None,
}
}
fn strcmp(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
(args.len() == 2).then_some(())?;
self.compared(data, args, usize::MAX)
}
fn strncmp(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
(args.len() == 3).then_some(())?; let count = self.count(args[2])?;
self.compared(data, args, count)
}
fn compared(&self, data: &InstData, args: &[Value], bound: usize) -> Option<Plan> {
if self.func[args[0]].ty != Type::PTR || self.func[args[1]].ty != Type::PTR {
return None;
}
let ty = self.answers(data)?;
if bound == 0 {
return Some(Plan::Answer(Answer::Number(0)));
}
match (self.one(args[0]), self.one(args[1])) {
(Some(left), Some(right)) => {
Some(Plan::Answer(Answer::Number(walk(&left, &right, bound))))
}
(Some(known), None) => self.byte(ty, &known, args[1], true, bound),
(None, Some(known)) => self.byte(ty, &known, args[0], false, bound),
(None, None) => None,
}
}
fn byte(
&self,
ty: Type,
known: &[u8],
other: Value,
leading: bool,
bound: usize,
) -> Option<Plan> {
(bound == 1 || known.is_empty()).then_some(())?;
(ty.bits() > 8).then_some(())?;
let against = known.first().copied().unwrap_or(0);
Some(Plan::Answer(Answer::Byte { of: other, against, leading }))
}
fn span(&self, data: &InstData, args: &[Value], set: Set) -> Option<Plan> {
if args.len() != 2
|| self.func[args[0]].ty != Type::PTR
|| self.func[args[1]].ty != Type::PTR
{
return None;
}
let ty = self.answers(data)?;
if self.one(args[0]).is_some_and(|text| text.is_empty()) {
return Some(Plan::Answer(Answer::Number(0)));
}
let accept = self.one(args[1])?;
if let Some(text) = self.one(args[0]) {
let len = text
.iter()
.position(|byte| accept.contains(byte) != matches!(set, Set::Inside))
.unwrap_or(text.len());
return Some(Plan::Answer(Answer::Number(i128::try_from(len).ok()?)));
}
accept.is_empty().then_some(())?;
match set {
Set::Inside => Some(Plan::Answer(Answer::Number(0))),
Set::Outside => {
let (_, signature) = self.shapes.get("strlen")?;
signature.return_types().eq([ty]).then_some(())?;
self.call("strlen", vec![Argument::Have(args[0])])
}
}
}
fn strpbrk(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
if args.len() != 2 || self.func[args[0]].ty != Type::PTR || !self.places(data) {
return None;
}
if self.func[args[1]].ty != Type::PTR {
return None;
}
let accept = self.one(args[1])?;
if accept.is_empty() {
return Some(Plan::Answer(Answer::Nowhere));
}
match self.one(args[0]) {
Some(text) => {
Some(Plan::Answer(match text.iter().position(|byte| accept.contains(byte)) {
Some(at) => Answer::Along(args[0], u64::try_from(at).ok()?),
None => Answer::Nowhere,
}))
}
None => match accept.as_slice() {
[one] => self.call("strchr", vec![Argument::Have(args[0]), Argument::Char(*one)]),
_ => None,
},
}
}
fn strstr(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
if args.len() != 2 {
return None;
}
let (haystack, needle) = (args[0], args[1]);
if self.func[haystack].ty != Type::PTR || self.func[needle].ty != Type::PTR {
return None;
}
let mut results = data.results();
if !results.next().is_some_and(|result| self.func[result].ty == Type::PTR)
|| results.next().is_some()
{
return None;
}
let needle = self.one(needle)?;
if needle.is_empty() {
return Some(Plan::Answer(Answer::Along(haystack, 0)));
}
match self.one(haystack) {
Some(hay) => Some(Plan::Answer(match at(&hay, &needle) {
Some(found) => Answer::Along(haystack, u64::try_from(found).ok()?),
None => Answer::Nowhere,
})),
None => match needle.as_slice() {
[one] => self.call("strchr", vec![Argument::Have(haystack), Argument::Char(*one)]),
_ => None,
},
}
}
fn printf(&self, args: &[Value], quiet: bool) -> Option<Plan> {
let format = self.one(*args.first()?)?;
match args.len() {
1 => self.plain(&format, None, quiet),
2 if format == b"%s\n" && !quiet && self.func[args[1]].ty == Type::PTR => {
self.call("puts", vec![Argument::Have(args[1])])
}
2 if format == b"%c" && !quiet && self.func[args[1]].ty == int() => {
self.call("putchar", vec![Argument::Have(args[1])])
}
2 if format == b"%s" => self.plain(&self.one(args[1])?, None, quiet),
_ => None,
}
}
fn fprintf(&self, args: &[Value], quiet: bool) -> Option<Plan> {
let stream = *args.first()?;
if self.func[stream].ty != Type::PTR {
return None;
}
let format = self.one(*args.get(1)?)?;
match args.len() {
2 => self.plain(&format, Some((args[1], stream)), quiet),
3 if format == b"%c" && !quiet && self.func[args[2]].ty == int() => {
self.call("fputc", vec![Argument::Have(args[2]), Argument::Have(stream)])
}
3 if format == b"%s" && self.func[args[2]].ty == Type::PTR => {
match self.strings(args[2]) {
Some(candidates) => self.string(&candidates, args[2], stream, quiet),
None if quiet => None,
None => {
self.call("fputs", vec![Argument::Have(args[2]), Argument::Have(stream)])
}
}
}
_ => None,
}
}
fn fputs(&self, args: &[Value], quiet: bool) -> Option<Plan> {
if args.len() != 2 {
return None;
}
let (text, stream) = (args[0], args[1]);
if self.func[text].ty != Type::PTR || self.func[stream].ty != Type::PTR {
return None;
}
self.string(&self.strings(text)?, text, stream, quiet)
}
fn plain(&self, format: &[u8], stream: Option<(Value, Value)>, quiet: bool) -> Option<Plan> {
if format.is_empty() {
return Some(Plan::Drop);
}
if quiet || format.contains(&b'%') {
return None;
}
match (format, stream) {
([one], Some((_, stream))) => {
self.call("fputc", vec![Argument::Char(*one), Argument::Have(stream)])
}
(_, Some((text, stream))) => self.fwrite(Argument::Have(text), format.len(), stream),
([one], None) => self.call("putchar", vec![Argument::Char(*one)]),
(_, None) => {
let (&last, rest) = format.split_last()?;
match last {
b'\n' => self.call("puts", vec![Argument::Text(rest.to_vec())]),
_ => None,
}
}
}
}
fn string(
&self,
candidates: &[Vec<u8>],
text: Value,
stream: Value,
quiet: bool,
) -> Option<Plan> {
let first = candidates.first()?;
if candidates.iter().any(|it| it.len() != first.len()) {
return None;
}
if first.is_empty() {
return Some(Plan::Drop);
}
if quiet {
return None;
}
match first.as_slice() {
[one] if candidates.iter().all(|it| it[0] == *one) => {
self.call("fputc", vec![Argument::Char(*one), Argument::Have(stream)])
}
_ => self.fwrite(Argument::Have(text), first.len(), stream),
}
}
fn fwrite(&self, text: Argument, bytes: usize, stream: Value) -> Option<Plan> {
let len = u64::try_from(bytes).ok()?;
self.call(
"fwrite",
vec![text, Argument::Count(1), Argument::Count(len), Argument::Have(stream)],
)
}
fn strcpy(&self, data: &InstData, name: &str, args: &[Value], ignored: bool) -> Option<Plan> {
let [dest, source] = *args else { return None };
if !self.places(data) {
return None;
}
let end = name == "stpcpy";
if end && ignored {
return self.unchecked(data, "strcpy", &[]);
}
let len = self.length(source)?;
let (callee, signature) = self.shapes.get("memcpy")?;
let args =
vec![Argument::Have(dest), Argument::Have(source), Argument::Count(len as u64 + 1)];
let answer = end.then_some(Answer::Along(dest, len as u64));
Some(Plan::Swap { callee, signature, args, answer })
}
fn nothing(&self, data: &InstData, args: &[Value], count: Option<Value>) -> Option<Plan> {
let [dest, source] = *args else { return None };
let nothing = self.one(source).is_some_and(|text| text.is_empty())
|| count.is_some_and(|count| self.number(count) == Some(0));
(nothing && self.places(data)).then_some(Plan::Answer(Answer::Along(dest, 0)))
}
fn strcat(&self, inst: Inst, data: &InstData, args: &[Value]) -> Option<Plan> {
if let Some(plan) = self.nothing(data, args, None) {
return Some(plan);
}
let [dest, source] = *args else { return None };
if !self.places(data) {
return None;
}
let len = u64::try_from(self.length(source)?).ok()?;
let before = u64::try_from(self.stored(inst, dest)?.len()).ok()?;
let (callee, signature) = self.shapes.get("memcpy")?;
let args =
vec![Argument::At(dest, before), Argument::Have(source), Argument::Count(len + 1)];
Some(Plan::Swap { callee, signature, args, answer: Some(Answer::Along(dest, 0)) })
}
fn strncat(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
let [dest, source, count] = *args else { return None };
if let Some(plan) = self.nothing(data, &[dest, source], Some(count)) {
return Some(plan);
}
let len = self.one(source)?.len() as u128;
(self.number(count)? >= len).then(|| self.unchecked(data, "strcat", &[2]))?
}
fn memmove(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
let [dest, source, count] = *args else { return None };
if !self.places(data) {
return None;
}
self.moved(dest, source, count)
}
fn moved(&self, dest: Value, source: Value, count: Value) -> Option<Plan> {
if self.number(count) == Some(0) {
return Some(Plan::Answer(Answer::Along(dest, 0)));
}
let (to, _) = self.address(dest)?;
let (from, _) = self.address(source)?;
let apart = self.largest(count).is_some_and(|count| count <= 1)
|| self.fixed(from)
|| (to != from && self.object(to) && self.object(from))
&& (self.local(to) || self.local(from));
apart.then(|| self.copy(dest, source, count))?
}
fn strncpy(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
let [dest, source, count] = *args else { return None };
if !self.places(data) {
return None;
}
let number = self.number(count)?;
if number == 0 {
return Some(Plan::Answer(Answer::Along(dest, 0)));
}
let len = u128::try_from(self.length(source)?).ok()?;
(number <= len + 1).then(|| self.copy(dest, source, count))?
}
fn copy(&self, dest: Value, source: Value, count: Value) -> Option<Plan> {
let (callee, signature) = self.shapes.get("memcpy")?;
let args = vec![Argument::Have(dest), Argument::Have(source), Argument::Have(count)];
Some(Plan::Swap { callee, signature, args, answer: None })
}
fn object(&self, value: Value) -> bool {
self.local(value)
|| matches!(self.func[value].def, Def::Result { inst, .. } if self.func[inst].opcode == Opcode::GlobalAddr)
}
fn local(&self, value: Value) -> bool {
matches!(self.func[value].def, Def::Result { inst, .. } if self.func[inst].opcode == Opcode::Alloca)
}
fn fixed(&self, value: Value) -> bool {
let Def::Result { inst, .. } = self.func[value].def else { return false };
if self.func[inst].opcode != Opcode::GlobalAddr {
return false;
}
let Extra::Symbol(name) = self.func[inst].extra else { return false };
let Some(SymbolRef::Global(id)) = self.module.lookup(name) else { return false };
let global = &self.module[id];
global.constant && vouched(global, self.pic)
}
fn mempcpy(&self, data: &InstData, args: &[Value], ignored: bool) -> Option<Plan> {
let [dest, source, count] = *args else { return None };
if ignored {
return self.unchecked(data, "memcpy", &[]);
}
let along = u64::try_from(self.number(count)?).ok()?;
if !self.places(data) {
return None;
}
let (callee, signature) = self.shapes.get("memcpy")?;
let args = vec![Argument::Have(dest), Argument::Have(source), Argument::Have(count)];
Some(Plan::Swap { callee, signature, args, answer: Some(Answer::Along(dest, along)) })
}
fn sprintf(&self, data: &InstData, args: &[Value], ignored: bool) -> Option<Plan> {
let (&dest, &format) = (args.first()?, args.get(1)?);
let text = self.one(format)?;
let source = match *args {
[_, _] if !text.contains(&b'%') => format,
[_, _, arg] if text == b"%s" && self.func[arg].ty == Type::PTR => arg,
_ => return None,
};
let answer = if ignored {
None
} else {
self.answers(data)?;
Some(Answer::Number(i128::try_from(self.one(source)?.len()).ok()?))
};
let (callee, signature) = self.shapes.get("strcpy")?;
Some(Plan::Swap {
callee,
signature,
args: vec![Argument::Have(dest), Argument::Have(source)],
answer,
})
}
fn memory_chk(
&self,
data: &InstData,
name: &str,
args: &[Value],
ignored: bool,
) -> Option<Plan> {
let [_, _, count, size] = *args else { return None };
if self.fits(count, size) {
return self.unchecked(data, plain(name), &[3]);
}
(name == "__mempcpy_chk" && ignored).then(|| self.unchecked(data, "__memcpy_chk", &[]))?
}
fn strcpy_chk(
&self,
data: &InstData,
name: &str,
args: &[Value],
ignored: bool,
) -> Option<Plan> {
let [dest, source, size] = *args else { return None };
let end = name == "__stpcpy_chk";
let fits = self.unknown(size)
|| self.longest(source).zip(self.number(size)).is_some_and(|(len, size)| len < size);
if fits {
return self.unchecked(data, if end && !ignored { "stpcpy" } else { "strcpy" }, &[2]);
}
if end {
return ignored.then(|| self.unchecked(data, "__strcpy_chk", &[]))?;
}
let len = self.one(source)?.len() as u64;
let args = vec![
Argument::Have(dest),
Argument::Have(source),
Argument::Count(len + 1),
Argument::Have(size),
];
self.call("__memcpy_chk", args)
}
fn strncpy_chk(
&self,
data: &InstData,
name: &str,
args: &[Value],
ignored: bool,
) -> Option<Plan> {
let [_, _, count, size] = *args else { return None };
let end = name == "__stpncpy_chk";
if self.fits(count, size) {
return self.unchecked(data, if end && !ignored { "stpncpy" } else { "strncpy" }, &[3]);
}
(end && ignored).then(|| self.unchecked(data, "__strncpy_chk", &[]))?
}
fn strcat_chk(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
let [dest, source, size] = *args else { return None };
if let Some(plan) = self.nothing(data, &[dest, source], None) {
return Some(plan);
}
self.unknown(size).then(|| self.unchecked(data, "strcat", &[2]))?
}
fn strncat_chk(&self, data: &InstData, args: &[Value]) -> Option<Plan> {
let [dest, source, count, size] = *args else { return None };
if let Some(plan) = self.nothing(data, &[dest, source], Some(count)) {
return Some(plan);
}
if self.unknown(size) {
return self.unchecked(data, "strncat", &[3]);
}
let len = self.one(source)?.len() as u128;
(self.number(count)? >= len).then(|| self.unchecked(data, "__strcat_chk", &[2]))?
}
fn sprintf_chk(&self, data: &InstData, name: &str, args: &[Value]) -> Option<Plan> {
let (&flag, &size, &format) = (args.get(1)?, args.get(2)?, args.get(3)?);
let text = self.one(format);
let len = match (text.as_deref(), args.get(4..)?) {
(Some(text), rest)
if !text.contains(&b'%') && (name == "__vsprintf_chk" || rest.is_empty()) =>
{
Some(text.len() as u128)
}
(Some(b"%s"), &[arg]) if name == "__sprintf_chk" => {
self.one(arg).map(|arg| arg.len() as u128)
}
_ => None,
};
let fits =
self.unknown(size) || len.zip(self.number(size)).is_some_and(|(len, size)| len < size);
(fits && self.flagless(flag, text.as_deref()))
.then(|| self.unchecked(data, plain(name), &[1, 2]))?
}
fn snprintf_chk(&self, data: &InstData, name: &str, args: &[Value]) -> Option<Plan> {
let (&count, &flag, &size, &format) =
(args.get(1)?, args.get(2)?, args.get(3)?, args.get(4)?);
let text = self.one(format);
(self.fits(count, size) && self.flagless(flag, text.as_deref()))
.then(|| self.unchecked(data, plain(name), &[2, 3]))?
}
fn flagless(&self, flag: Value, text: Option<&[u8]>) -> bool {
self.number(flag) == Some(0)
|| text.is_some_and(|text| !text.contains(&b'%') || text == b"%s")
}
fn unchecked(&self, data: &InstData, name: &str, drop: &'static [usize]) -> Option<Plan> {
let Extra::Call(at) = data.extra else { return None };
let want = without(&self.func[self.func[at].signature], drop)?;
let callee = self.shapes.unchecked(name, &want)?;
Some(Plan::Unchecked { callee, drop })
}
fn unknown(&self, size: Value) -> bool {
crate::fold::evaluated(self.func, size, DEPTH).is_some_and(|(imm, ty)| imm.signed(ty) == -1)
}
fn fits(&self, count: Value, size: Value) -> bool {
self.unknown(size)
|| self.largest(count).zip(self.number(size)).is_some_and(|(count, size)| count <= size)
}
fn largest(&self, value: Value) -> Option<u128> {
self.largest_on(value, CHAIN, &mut Vec::new())
}
fn largest_on(&self, value: Value, depth: u32, on: &mut Vec<Value>) -> Option<u128> {
if depth == 0 {
return None;
}
match self.func[value].def {
Def::Param { block, index } => {
if on.contains(&value) {
return Some(0);
}
let preds = self.cfg.predecessors(block);
on.push(value);
let mut most = None;
for &pred in preds {
let term = self.func.terminator(pred)?;
for call in self.func.successors(term).collect::<Vec<_>>() {
if call.block != block {
continue;
}
let arg = *self.func[call.args].get(index as usize)?;
most = most.max(Some(self.largest_on(arg, depth - 1, on)?));
}
}
on.pop();
most
}
Def::Result { inst, .. } if self.func[inst].opcode == Opcode::Select => {
let args = &self.func[self.func[inst].args];
let (then, other) = (*args.get(1)?, *args.get(2)?);
let then = self.largest_on(then, depth - 1, on)?;
Some(then.max(self.largest_on(other, depth - 1, on)?))
}
_ => self.number(value).or_else(|| self.bounded(value, depth, on)),
}
}
fn bounded(&self, value: Value, depth: u32, on: &mut Vec<Value>) -> Option<u128> {
let Def::Result { inst, .. } = self.func[value].def else { return None };
let args = &self.func[self.func[inst].args];
let narrow = *args.first()?;
match self.func[inst].opcode {
Opcode::And => self.number(narrow).or_else(|| self.number(*args.get(1)?)),
Opcode::URem => self.number(*args.get(1)?)?.checked_sub(1),
Opcode::ZExt => self.largest_on(narrow, depth - 1, on),
Opcode::SExt => {
let most = self.largest_on(narrow, depth - 1, on)?;
let top = 1u128.checked_shl(self.func[narrow].ty.bits().checked_sub(1)?)?;
(most < top).then_some(most)
}
_ => None,
}
}
fn length(&self, value: Value) -> Option<usize> {
let texts = self.strings(value)?;
let len = texts.first()?.len();
texts.iter().all(|text| text.len() == len).then_some(len)
}
fn number(&self, value: Value) -> Option<u128> {
crate::fold::evaluated(self.func, value, DEPTH).map(|(imm, _)| imm.unsigned())
}
fn longest(&self, value: Value) -> Option<u128> {
self.strings(value)?.iter().map(|text| text.len() as u128).max()
}
fn narrow(&self, data: &InstData, args: &[Value], callee: &'static str) -> Option<Plan> {
let &[wide] = args else { return None };
let double = Type::float(Float::F64);
let float = Type::float(Float::F32);
if self.func[wide].ty != double || self.answers_float(data) != Some(double) {
return None;
}
let Def::Result { inst, .. } = self.func[wide].def else { return None };
let widened = &self.func[inst];
let &[arg] = &self.func[widened.args] else { return None };
if widened.opcode != Opcode::FPExt || self.func[arg].ty != float {
return None;
}
let (callee, signature) = self.shapes.get(callee)?;
Some(Plan::Narrow { callee, signature, arg })
}
fn answers_float(&self, data: &InstData) -> Option<Type> {
let mut results = data.results();
let ty = self.func[results.next()?].ty;
(results.next().is_none() && ty.is_float() && !ty.is_vector()).then_some(ty)
}
fn call(&self, callee: &'static str, args: Vec<Argument>) -> Option<Plan> {
let (callee, signature) = self.shapes.get(callee)?;
Some(Plan::Swap { callee, signature, args, answer: None })
}
fn one(&self, value: Value) -> Option<Vec<u8>> {
let mut candidates = self.strings(value)?;
(candidates.len() == 1).then(|| candidates.pop()).flatten()
}
fn strings(&self, value: Value) -> Option<Vec<Vec<u8>>> {
self.strings_on(value, CHAIN, &mut Vec::new())
}
fn strings_on(&self, value: Value, depth: u32, on: &mut Vec<Value>) -> Option<Vec<Vec<u8>>> {
if depth == 0 {
return None;
}
match self.func[value].def {
Def::Param { block, index } => {
if on.contains(&value) {
return Some(Vec::new());
}
let preds = self.cfg.predecessors(block);
if preds.is_empty() {
return None;
}
on.push(value);
let mut all = Vec::new();
for &pred in preds {
let term = self.func.terminator(pred)?;
for call in self.func.successors(term).collect::<Vec<_>>() {
if call.block != block {
continue;
}
let arg = *self.func[call.args].get(index as usize)?;
all.extend(self.strings_on(arg, depth - 1, on)?);
}
}
on.pop();
(!all.is_empty()).then_some(all)
}
Def::Result { inst, .. } if self.func[inst].opcode == Opcode::Select => {
let args = &self.func[self.func[inst].args];
let (then, other) = (*args.get(1)?, *args.get(2)?);
let mut all = self.strings_on(then, depth - 1, on)?;
all.extend(self.strings_on(other, depth - 1, on)?);
Some(all)
}
_ => Some(vec![self.literal(value)?]),
}
}
fn literal(&self, value: Value) -> Option<Vec<u8>> {
let bytes = self.raw(value)?;
let end = bytes.iter().position(|&byte| byte == 0)?;
Some(bytes[..end].to_vec())
}
fn raw(&self, value: Value) -> Option<Vec<u8>> {
let (base, offset) = self.address(value)?;
let Def::Result { inst, .. } = self.func[base].def else { return None };
if self.func[inst].opcode != Opcode::GlobalAddr {
return None;
}
let Extra::Symbol(name) = self.func[inst].extra else { return None };
let Some(SymbolRef::Global(id)) = self.module.lookup(name) else { return None };
let global = &self.module[id];
if !global.constant || !vouched(global, self.pic) {
return None;
}
let mut bytes = Vec::new();
for &datum in &self.module[global.init?] {
match datum {
Datum::Bytes(range) => bytes.extend_from_slice(&self.module[range]),
Datum::Zero(count) => {
bytes.resize(bytes.len().checked_add(usize::try_from(count).ok()?)?, 0);
}
Datum::Scalar { .. } | Datum::Addr(_) | Datum::Away(_) | Datum::Apart { .. } => {
return None;
}
}
}
let size = usize::try_from(global.size).ok()?;
if bytes.len() < size {
bytes.resize(size, 0);
}
Some(bytes.get(usize::try_from(offset).ok()?..)?.to_vec())
}
fn address(&self, mut value: Value) -> Option<(Value, i128)> {
let mut offset: i128 = 0;
for _ in 0..DEPTH {
let Def::Result { inst, .. } = self.func[value].def else {
return Some((value, offset));
};
if self.func[inst].opcode != Opcode::PtrAdd {
return Some((value, offset));
}
let args = &self.func[self.func[inst].args];
offset = offset.checked_add(self.step(*args.get(1)?)?)?;
value = *args.first()?;
}
None
}
fn step(&self, value: Value) -> Option<i128> {
let (imm, ty) = crate::fold::evaluated(self.func, value, DEPTH)?;
Some(imm.signed(ty))
}
}
fn apply(
module: &mut Module,
id: FuncId,
names: &mut Interner,
texts: &mut HashMap<Vec<u8>, Symbol>,
inst: Inst,
plan: Plan,
) -> HashMap<Value, Value> {
let (callee, signature, args, answer) = match plan {
Plan::Drop => {
module[id].remove_inst(inst);
return HashMap::new();
}
Plan::Answer(answer) => {
let width = size(module);
return answered(&mut module[id], inst, answer, width);
}
Plan::Swap { callee, signature, args, answer } => (callee, signature, args, answer),
Plan::Narrow { callee, signature, arg } => {
let func = &mut module[id];
let Some(old) = func[inst].results().next() else { return HashMap::new() };
let ty = func[old].ty;
let span = func.span(inst);
let varargs = func.push_abis(&[]);
let made = call(func, inst, callee, signature, varargs, &[arg]);
let narrow = func[made].results().next().expect("a rounding is one value");
let args = func.push_values(&[narrow]);
let data = InstData { args, ..InstData::new(Opcode::FPExt) };
let wide = func.create_inst(data, &[ty], span);
func.insert_before(wide, inst);
let value = func[wide].results().next().expect("a conversion is one value");
let forward = HashMap::from([(old, value)]);
uses::substitute(func, &forward);
func.remove_inst(inst);
return forward;
}
Plan::Unchecked { callee, drop } => {
let func = &mut module[id];
let Extra::Call(at) = func[inst].extra else { return HashMap::new() };
let info = func[at];
let Some(signature) = without(&func[info.signature], drop) else {
return HashMap::new();
};
let values: Vec<Value> = func[func[inst].args]
.iter()
.enumerate()
.filter(|(index, _)| !drop.contains(index))
.map(|(_, &value)| value)
.collect();
let made = call(func, inst, callee, signature, info.varargs, &values);
return forward(func, inst, made);
}
};
let symbols: Vec<Option<Symbol>> = args
.iter()
.map(|arg| match arg {
Argument::Text(bytes) => Some(object(module, names, texts, bytes)),
_ => None,
})
.collect();
let width = size(module);
let func = &mut module[id];
let span = func.span(inst);
let mut values = Vec::with_capacity(args.len());
for (arg, symbol) in args.iter().zip(symbols) {
values.push(match arg {
Argument::Have(value) => *value,
Argument::Char(byte) => constant(func, inst, int(), i128::from(*byte)),
Argument::Count(count) => constant(func, inst, width, i128::from(*count)),
Argument::At(value, by) => {
let step = constant(func, inst, width, i128::from(*by));
let args = func.push_values(&[*value, step]);
let data = InstData { args, ..InstData::new(Opcode::PtrAdd) };
let made = func.create_inst(data, &[Type::PTR], span);
func.insert_before(made, inst);
func[made].results().next().expect("an address is one value")
}
Argument::Text(_) => {
let extra = Extra::Symbol(symbol.expect("a text argument has an object"));
let data = InstData { extra, ..InstData::new(Opcode::GlobalAddr) };
let made = func.create_inst(data, &[Type::PTR], span);
func.insert_before(made, inst);
func[made].results().next().expect("an address is one value")
}
});
}
let varargs = func.push_abis(&[]);
let made = call(func, inst, callee, signature, varargs, &values);
match answer {
Some(answer) => answered(func, inst, answer, width),
None => forward(func, inst, made),
}
}
fn plain(name: &str) -> &str {
name.strip_prefix("__").and_then(|rest| rest.strip_suffix("_chk")).unwrap_or(name)
}
fn call(
func: &mut Func,
before: Inst,
callee: Symbol,
signature: Signature,
varargs: AbiList,
values: &[Value],
) -> Inst {
let span = func.span(before);
let results: Vec<Type> = signature.return_types().collect();
let sig = func.add_signature(signature);
let info = func.add_call(CallInfo { callee: Some(callee), signature: sig, varargs });
let args = func.push_values(values);
let data = InstData { args, extra: Extra::Call(info), ..InstData::new(Opcode::Call) };
let made = func.create_inst(data, &results, span);
func.insert_before(made, before);
made
}
fn forward(func: &mut Func, old: Inst, new: Inst) -> HashMap<Value, Value> {
let forward: HashMap<Value, Value> = func[old]
.results()
.zip(func[new].results().collect::<Vec<Value>>())
.filter(|&(from, to)| func[from].ty == func[to].ty)
.collect();
if !forward.is_empty() {
uses::substitute(func, &forward);
}
func.remove_inst(old);
forward
}
fn without(signature: &Signature, drop: &[usize]) -> Option<Signature> {
drop.iter().all(|&index| index < signature.params.len()).then_some(())?;
let params = signature
.params
.iter()
.enumerate()
.filter(|(index, _)| !drop.contains(index))
.map(|(_, param)| *param)
.collect();
Some(Signature { params, ..signature.clone() })
}
fn answered(func: &mut Func, inst: Inst, answer: Answer, width: Type) -> HashMap<Value, Value> {
let span = func.span(inst);
let value = match answer {
Answer::Along(haystack, 0) => haystack,
Answer::Along(haystack, by) => {
let step = constant(func, inst, width, i128::from(by));
let args = func.push_values(&[haystack, step]);
let data = InstData { args, ..InstData::new(Opcode::PtrAdd) };
let made = func.create_inst(data, &[Type::PTR], span);
func.insert_before(made, inst);
func[made].results().next().expect("an address is one value")
}
Answer::Nowhere => {
let zero = constant(func, inst, width, 0);
let args = func.push_values(&[zero]);
let data = InstData { args, ..InstData::new(Opcode::IntToPtr) };
let made = func.create_inst(data, &[Type::PTR], span);
func.insert_before(made, inst);
func[made].results().next().expect("a null pointer is one value")
}
Answer::Number(number) => {
let ty = func[inst]
.results()
.next()
.map(|result| func[result].ty)
.expect("a call whose answer is a number has one");
constant(func, inst, ty, number)
}
Answer::Least { count, len } => {
let ty = func[count].ty;
let len = constant(func, inst, ty, i128::from(len));
let args = func.push_values(&[count, len]);
let data = InstData {
args,
extra: Extra::IntPred(IntPred::Ult),
..InstData::new(Opcode::ICmp)
};
let made = func.create_inst(data, &[Type::I1], span);
func.insert_before(made, inst);
let shorter = func[made].results().next().expect("a comparison is one value");
let args = func.push_values(&[shorter, count, len]);
let data = InstData { args, ..InstData::new(Opcode::Select) };
let made = func.create_inst(data, &[ty], span);
func.insert_before(made, inst);
func[made].results().next().expect("a choice is one value")
}
Answer::Less { len, step } => {
let ty = func[inst]
.results()
.next()
.map(|result| func[result].ty)
.expect("a call whose answer is a length has one");
let step = resize(func, inst, step, ty);
let len = constant(func, inst, ty, i128::from(len));
let args = func.push_values(&[len, step]);
let data = InstData { args, ..InstData::new(Opcode::Sub) };
let made = func.create_inst(data, &[ty], span);
func.insert_before(made, inst);
func[made].results().next().expect("a difference is one value")
}
Answer::Byte { of, against, leading } => {
let ty = func[inst]
.results()
.next()
.map(|result| func[result].ty)
.expect("a call whose answer is a byte has one");
let read = read(func, inst, of);
let args = func.push_values(&[read]);
let data = InstData { args, ..InstData::new(Opcode::ZExt) };
let made = func.create_inst(data, &[ty], span);
func.insert_before(made, inst);
let wide = func[made].results().next().expect("a conversion is one value");
let other = constant(func, inst, ty, i128::from(against));
let pair = if leading { [other, wide] } else { [wide, other] };
let args = func.push_values(&pair);
let data = InstData { args, ..InstData::new(Opcode::Sub) };
let made = func.create_inst(data, &[ty], span);
func.insert_before(made, inst);
func[made].results().next().expect("a difference is one value")
}
};
let forward: HashMap<Value, Value> =
func[inst].results().map(|result| (result, value)).collect();
uses::substitute(func, &forward);
func.remove_inst(inst);
forward
}
fn walk(left: &[u8], right: &[u8], bound: usize) -> i128 {
for at in 0..bound.min(left.len() + 1).min(right.len() + 1) {
let (this, that) =
(left.get(at).copied().unwrap_or(0), right.get(at).copied().unwrap_or(0));
if this != that {
return if this < that { -1 } else { 1 };
}
if this == 0 {
break;
}
}
0
}
fn at(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|window| window == needle)
}
fn read(func: &mut Func, before: Inst, from: Value) -> Value {
let span = func.span(before);
let mem = func.add_mem(MemInfo {
size: 1,
align: 1,
order: MemOrder::NotAtomic,
tbaa: None,
owns: 0,
restrict: Restrict::NONE,
});
let args = func.push_values(&[from]);
let data = InstData { args, extra: Extra::Mem(mem), ..InstData::new(Opcode::Load) };
let made = func.create_inst(data, &[Type::int(8)], span);
func.insert_before(made, before);
func[made].results().next().expect("a load is one value")
}
fn resize(func: &mut Func, before: Inst, value: Value, ty: Type) -> Value {
let opcode = match func[value].ty.bits().cmp(&ty.bits()) {
std::cmp::Ordering::Equal => return value,
std::cmp::Ordering::Less => Opcode::ZExt,
std::cmp::Ordering::Greater => Opcode::Trunc,
};
let span = func.span(before);
let args = func.push_values(&[value]);
let made = func.create_inst(InstData { args, ..InstData::new(opcode) }, &[ty], span);
func.insert_before(made, before);
func[made].results().next().expect("a conversion is one value")
}
fn constant(func: &mut Func, before: Inst, ty: Type, value: i128) -> Value {
let span = func.span(before);
let imm = func.add_imm(Imm::int(value, ty.lane()));
let data = InstData { extra: Extra::Imm(imm), ..InstData::new(Opcode::IConst) };
let made = func.create_inst(data, &[ty], span);
func.insert_before(made, before);
func[made].results().next().expect("a constant is one value")
}
fn object(
module: &mut Module,
names: &mut Interner,
texts: &mut HashMap<Vec<u8>, Symbol>,
bytes: &[u8],
) -> Symbol {
if let Some(&symbol) = texts.get(bytes) {
return symbol;
}
let mut image = bytes.to_vec();
image.push(0);
let mut symbol = names.intern(&format!(".Lfold.{}", texts.len()));
for next in texts.len().. {
if module.lookup(symbol).is_none() {
break;
}
symbol = names.intern(&format!(".Lfold.{}", next + 1));
}
let mut global = Global::new(symbol, image.len() as u64, 1);
global.linkage = Linkage::Internal;
global.constant = true;
let range = module.push_bytes(&image);
global.init = Some(module.push_data(&[Datum::Bytes(range)]));
module.add_global(global);
texts.insert(bytes.to_vec(), symbol);
symbol
}
#[cfg(test)]
mod tests {
use super::*;
const HEAD: &str = "\
; ModuleID = 't.c'
; format 0
target triple = \"x86_64-unknown-linux-gnu\"
target datalayout = \"e-p:64:64-i64:64-f80:128-S128\"
";
fn folded(body: &str) -> String {
run(body, &[], &mut Fuel::unlimited())
}
fn run(body: &str, no_builtin: &[String], fuel: &mut Fuel) -> String {
let mut names = Interner::new();
let text = format!("{HEAD}{body}");
let mut module = rucc_ir::parse(&text, &mut names).expect("the fixture parses");
fold(&mut module, &mut names, no_builtin, Pic::Executable, fuel);
if let Err(errors) = rucc_ir::verify(&module, &names) {
panic!("the fold left invalid IR, {errors:?}\n{}", rucc_ir::print(&module, &names));
}
rucc_ir::print(&module, &names)
}
#[test]
fn a_format_that_ends_in_a_newline_is_written_by_puts() {
let out = folded(
r#"
global @.Lstr.0 : bytes 13 = { bytes "hello world\0a\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf(%0) : (ptr, ...) -> i32
return
}
"#,
);
assert!(out.contains("call @puts("), "{out}");
assert!(!out.contains("call @printf("), "{out}");
assert!(out.contains(r#"@.Lfold.0 : bytes 12 = { bytes "hello world\00" }"#), "{out}");
}
#[test]
fn a_short_format_is_written_by_putchar_or_by_nothing() {
let out = folded(
r#"
global @.Lstr.0 : bytes 2 = { bytes "x\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf(%0) : (ptr, ...) -> i32
%2 = global_addr @.Lstr.1
%3 = call @printf(%2) : (ptr, ...) -> i32
return
}
"#,
);
assert!(out.contains("iconst.i32 120"), "the character is the argument, {out}");
assert!(out.contains("call @putchar("), "{out}");
assert_eq!(out.matches("call @").count(), 1, "the empty one is gone, {out}");
}
#[test]
fn the_two_formats_that_are_a_call_on_their_own_are_folded_for_any_argument() {
let out = folded(
r#"
global @.Lstr.0 : bytes 4 = { bytes "%s\0a\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 3 = { bytes "%c\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @g(ptr, i32), linkage(external) {
block0(%0: ptr, %1: i32):
%2 = global_addr @.Lstr.0
%3 = call @printf(%2, %0) : (ptr, ...) -> i32
%4 = global_addr @.Lstr.1
%5 = call @printf(%4, %1) : (ptr, ...) -> i32
return
}
"#,
);
assert!(out.contains("call @puts(%0)"), "{out}");
assert!(out.contains("call @putchar(%1)"), "{out}");
}
#[test]
fn a_string_argument_nothing_is_known_about_is_left_to_printf() {
let out = folded(
r#"
global @.Lstr.0 : bytes 3 = { bytes "%s\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @printf(%1, %0) : (ptr, ...) -> i32
return
}
"#,
);
assert!(out.contains("call @printf("), "{out}");
}
#[test]
fn a_stream_takes_the_whole_format_through_fwrite() {
let out = folded(
r#"
global @.Lstr.0 : bytes 13 = { bytes "hello world\0a\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 2 = { bytes "q\00" }, align 1, linkage(internal), constant
func @fprintf(ptr, ptr, ...) -> i32, linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @fprintf(%0, %1) : (ptr, ptr, ...) -> i32
%3 = global_addr @.Lstr.1
%4 = call @fprintf(%0, %3) : (ptr, ptr, ...) -> i32
return
}
"#,
);
assert!(out.contains("call @fwrite("), "{out}");
assert!(out.contains("iconst.i64 12"), "the whole format, newline and all, {out}");
assert!(out.contains("call @fputc("), "{out}");
assert!(!out.contains("call @fprintf("), "{out}");
}
#[test]
fn a_string_argument_with_a_stream_beside_it_becomes_fputs() {
let out = folded(
r#"
global @.Lstr.0 : bytes 3 = { bytes "%s\00" }, align 1, linkage(internal), constant
func @fprintf(ptr, ptr, ...) -> i32, linkage(external);
func @g(ptr, ptr), linkage(external) {
block0(%0: ptr, %1: ptr):
%2 = global_addr @.Lstr.0
%3 = call @fprintf(%0, %2, %1) : (ptr, ptr, ...) -> i32
return
}
"#,
);
assert!(out.contains("call @fputs(%1, %0)"), "{out}");
}
#[test]
fn fputs_of_a_string_this_module_holds_is_folded_by_its_length() {
let out = folded(
r#"
global @.Lstr.0 : bytes 7 = { bytes "abcdef\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 2 = { bytes "z\00" }, align 1, linkage(internal), constant
global @.Lstr.2 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @fputs(ptr, ptr) -> i32, linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @fputs(%1, %0) : (ptr, ptr) -> i32
%3 = global_addr @.Lstr.1
%4 = call @fputs(%3, %0) : (ptr, ptr) -> i32
%5 = global_addr @.Lstr.2
%6 = call @fputs(%5, %0) : (ptr, ptr) -> i32
return
}
"#,
);
assert!(out.contains("call @fwrite("), "{out}");
assert!(out.contains("iconst.i64 6"), "{out}");
assert!(out.contains("iconst.i32 122"), "{out}");
assert!(out.contains("call @fputc("), "{out}");
assert!(!out.contains("call @fputs("), "the empty one is gone too, {out}");
}
#[test]
fn an_index_into_a_literal_is_a_string_of_its_own() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @fputs(ptr, ptr) -> i32, linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = iconst.i32 6
%3 = sext.i64 %2
%4 = ptr_add %1, %3
%5 = call @fputs(%4, %0) : (ptr, ptr) -> i32
%6 = iconst.i32 11
%7 = sext.i64 %6
%8 = ptr_add %1, %7
%9 = call @fputs(%8, %0) : (ptr, ptr) -> i32
return
}
"#,
);
assert!(out.contains("iconst.i64 5"), "world without its terminator, {out}");
assert!(out.contains("call @fwrite("), "{out}");
assert!(!out.contains("call @fputs("), "and the terminator itself is nothing, {out}");
}
#[test]
fn a_choice_between_two_literals_is_folded_when_they_are_the_same_length() {
let text = r#"
global @.Lstr.0 : bytes 2 = { bytes "f\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 2 = { bytes "x\00" }, align 1, linkage(internal), constant
global @.Lstr.2 : bytes 4 = { bytes "abc\00" }, align 1, linkage(internal), constant
func @fputs(ptr, ptr) -> i32, linkage(external);
func @g(ptr, i1), linkage(external) {
block0(%0: ptr, %1: i1):
%2 = global_addr @.LEFT
%3 = global_addr @.Lstr.1
br_if %1, block1(%2), block1(%3)
block1(%4: ptr):
%5 = call @fputs(%4, %0) : (ptr, ptr) -> i32
return
}
"#;
let same = folded(&text.replace(".LEFT", ".Lstr.0"));
assert!(same.contains("call @fwrite("), "{same}");
assert!(same.contains("iconst.i64 1"), "{same}");
let differing = folded(&text.replace(".LEFT", ".Lstr.2"));
assert!(differing.contains("call @fputs("), "{differing}");
}
#[test]
fn a_call_whose_answer_is_read_is_not_folded() {
let out = folded(
r#"
global @n : bytes 4 = { zero 4 }, align 4, linkage(external)
global @.Lstr.0 : bytes 3 = { bytes "a\0a\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf(%0) : (ptr, ...) -> i32
%2 = global_addr @n
store %1 -> %2, align 4
return
}
"#,
);
assert!(out.contains("call @printf("), "{out}");
}
#[test]
fn a_body_for_a_standard_name_is_not_folded_inside_and_does_not_stop_the_fold() {
let out = folded(
r#"
global @.Lstr.0 : bytes 3 = { bytes "a\0a\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @puts(ptr) -> i32, linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @printf(%1) : (ptr, ...) -> i32
%3 = iconst.i32 0
return %3
}
func @__vprintf_chk(i32, ptr, ptr) -> i32, linkage(external) {
block0(%0: i32, %1: ptr, %2: ptr):
%3 = iconst.i32 0
return %3
}
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = iconst.i32 1
%2 = global_addr @.Lstr.0
%3 = call @__vprintf_chk(%1, %2, %0) : (i32, ptr, ptr) -> i32
return
}
"#,
);
assert!(!out.contains("call @__vprintf_chk("), "{out}");
assert_eq!(out.matches("call @puts(").count(), 1, "{out}");
assert!(out.contains("call @printf("), "the body of `puts` keeps its own call, {out}");
}
#[test]
fn a_declaration_of_another_shape_stops_the_fold() {
let out = folded(
r#"
global @.Lstr.0 : bytes 13 = { bytes "hello world\0a\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @puts(ptr, i32) -> i32, linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf(%0) : (ptr, ...) -> i32
return
}
"#,
);
assert!(out.contains("call @printf("), "{out}");
assert!(!out.contains("@.Lfold."), "and no object was left behind either, {out}");
}
#[test]
fn a_variable_by_the_name_of_a_replacement_stops_the_fold() {
let out = folded(
r#"
global @putchar : bytes 4 = { zero 4 }, align 4, linkage(external)
global @.Lstr.0 : bytes 2 = { bytes "x\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf(%0) : (ptr, ...) -> i32
return
}
"#,
);
assert!(out.contains("call @printf("), "{out}");
}
#[test]
fn the_unlocked_spellings_are_only_removed_when_they_write_nothing() {
let out = folded(
r#"
global @.Lstr.0 : bytes 13 = { bytes "hello world\0a\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @printf_unlocked(ptr, ...) -> i32, linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf_unlocked(%0) : (ptr, ...) -> i32
%2 = global_addr @.Lstr.1
%3 = call @printf_unlocked(%2) : (ptr, ...) -> i32
return
}
"#,
);
assert_eq!(out.matches("call @printf_unlocked(").count(), 1, "{out}");
assert!(!out.contains("call @puts("), "{out}");
}
#[test]
fn one_name_can_be_taken_away_without_taking_the_family_away() {
let body = r#"
global @.Lstr.0 : bytes 2 = { bytes "x\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @fputs(ptr, ptr) -> i32, linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @printf(%1) : (ptr, ...) -> i32
%3 = call @fputs(%1, %0) : (ptr, ptr) -> i32
return
}
"#;
let out = run(body, &["printf".to_owned()], &mut Fuel::unlimited());
assert!(out.contains("call @printf("), "{out}");
assert!(out.contains("call @fputc("), "and the other one still folded, {out}");
}
#[test]
fn a_run_out_of_fuel_transforms_nothing() {
let body = r#"
global @.Lstr.0 : bytes 2 = { bytes "x\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf(%0) : (ptr, ...) -> i32
return
}
"#;
let mut fuel = Fuel::of(0);
let out = run(body, &[], &mut fuel);
assert!(out.contains("call @printf("), "{out}");
assert_eq!(fuel.spent(), 0);
}
#[test]
fn one_object_serves_every_call_that_prints_the_same_thing() {
let out = folded(
r#"
global @.Lstr.0 : bytes 4 = { bytes "hi\0a\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf(%0) : (ptr, ...) -> i32
%2 = call @printf(%0) : (ptr, ...) -> i32
return
}
func @h(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @printf(%0) : (ptr, ...) -> i32
return
}
"#,
);
assert_eq!(out.matches("@.Lfold.0 : bytes").count(), 1, "{out}");
assert!(!out.contains("@.Lfold.1"), "{out}");
assert_eq!(out.matches("call @puts(").count(), 3, "{out}");
}
#[test]
fn a_search_for_nothing_answers_with_the_haystack_itself() {
let out = folded(
r#"
global @.Lstr.0 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @strstr(ptr, ptr) -> ptr, linkage(external);
func @g(ptr) -> ptr, linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strstr(%0, %1) : (ptr, ptr) -> ptr
return %2
}
"#,
);
assert!(!out.contains("call @strstr("), "{out}");
assert!(out.contains("return %0"), "{out}");
}
#[test]
fn two_strings_this_module_holds_answer_without_a_call() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 2 = { bytes "w\00" }, align 1, linkage(internal), constant
global @.Lstr.2 : bytes 3 = { bytes "zz\00" }, align 1, linkage(internal), constant
func @strstr(ptr, ptr) -> ptr, linkage(external);
func @use(ptr, ptr), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = global_addr @.Lstr.1
%2 = call @strstr(%0, %1) : (ptr, ptr) -> ptr
%3 = global_addr @.Lstr.2
%4 = call @strstr(%0, %3) : (ptr, ptr) -> ptr
call @use(%2, %4) : (ptr, ptr)
return
}
"#,
);
assert!(!out.contains("call @strstr("), "{out}");
assert!(out.contains("ptr_add %0, "), "the w is six bytes along, {out}");
assert!(out.contains("iconst.i64 6"), "{out}");
assert!(out.contains("inttoptr"), "and the zz is nowhere in it, {out}");
}
#[test]
fn a_one_character_needle_becomes_a_search_for_that_character() {
let out = folded(
r#"
global @.Lstr.0 : bytes 2 = { bytes "o\00" }, align 1, linkage(internal), constant
func @strstr(ptr, ptr) -> ptr, linkage(external);
func @g(ptr) -> ptr, linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strstr(%0, %1) : (ptr, ptr) -> ptr
return %2
}
"#,
);
assert!(!out.contains("call @strstr("), "{out}");
assert!(out.contains("call @strchr(%0, "), "{out}");
assert!(out.contains("iconst.i32 111"), "{out}");
}
#[test]
fn a_strchr_of_another_shape_is_not_the_one_to_call() {
let out = folded(
r#"
global @.Lstr.0 : bytes 2 = { bytes "o\00" }, align 1, linkage(internal), constant
func @strstr(ptr, ptr) -> ptr, linkage(external);
func @strchr(ptr, ptr) -> ptr, linkage(external);
func @g(ptr) -> ptr, linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strstr(%0, %1) : (ptr, ptr) -> ptr
return %2
}
"#,
);
assert!(out.contains("call @strstr("), "{out}");
}
#[test]
fn a_renamed_declaration_is_still_the_function_it_was_spelled() {
let out = folded(
r#"
global @.Lstr.0 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @my_strstr(ptr, ptr) -> ptr, linkage(external), spelled "strstr";
func @g(ptr) -> ptr, linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @my_strstr(%0, %1) : (ptr, ptr) -> ptr
return %2
}
"#,
);
assert!(!out.contains("call @my_strstr("), "{out}");
assert!(out.contains("return %0"), "{out}");
}
#[test]
fn a_renamed_replacement_is_called_by_the_symbol_the_rename_asked_for() {
let out = folded(
r#"
global @.Lstr.0 : bytes 2 = { bytes "o\00" }, align 1, linkage(internal), constant
func @strstr(ptr, ptr) -> ptr, linkage(external);
func @my_strchr(ptr, i32) -> ptr, linkage(external), spelled "strchr";
func @g(ptr) -> ptr, linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strstr(%0, %1) : (ptr, ptr) -> ptr
return %2
}
"#,
);
assert!(out.contains("call @my_strchr(%0, "), "{out}");
assert!(!out.contains("call @strchr("), "{out}");
}
#[test]
fn a_strstr_taken_away_is_a_call_like_any_other() {
let body = r#"
global @.Lstr.0 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @strstr(ptr, ptr) -> ptr, linkage(external);
func @g(ptr) -> ptr, linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strstr(%0, %1) : (ptr, ptr) -> ptr
return %2
}
"#;
let out = run(body, &["strstr".to_owned()], &mut Fuel::unlimited());
assert!(out.contains("call @strstr("), "{out}");
}
#[test]
fn strlen_of_a_string_this_module_holds_is_a_number() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @strlen(ptr) -> i64, linkage(external);
func @use(i64, i64), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = call @strlen(%0) : (ptr) -> i64
%2 = iconst.i64 6
%3 = ptr_add %0, %2
%4 = call @strlen(%3) : (ptr) -> i64
call @use(%1, %4) : (i64, i64)
return
}
"#,
);
assert!(!out.contains("call @strlen("), "{out}");
assert!(out.contains("iconst.i64 11"), "{out}");
assert!(out.contains("iconst.i64 5"), "the world on its own, {out}");
}
#[test]
fn a_move_that_cannot_overlap_is_a_copy() {
let out = folded(
r#"
global @.Lstr.0 : bytes 6 = { bytes "abcde\00" }, align 1, linkage(internal), constant
global @p : bytes 32 = { zero 32 }, align 16, linkage(external)
func @memmove(ptr, ptr, i64) -> ptr, linkage(external);
func @bcopy(ptr, ptr, i64), linkage(external);
func @use(ptr, ptr, ptr, ptr), linkage(external);
func @g(ptr, i64), linkage(external) {
block0(%0: ptr, %1: i64):
%2 = global_addr @p
%3 = global_addr @.Lstr.0
%4 = iconst.i64 6
%5 = call @memmove(%2, %3, %4) : (ptr, ptr, i64) -> ptr
%6 = iconst.i64 2
%7 = ptr_add %2, %6
%8 = iconst.i64 3
%9 = ptr_add %2, %8
%10 = iconst.i64 1
%11 = call @memmove(%7, %9, %10) : (ptr, ptr, i64) -> ptr
%12 = iconst.i64 0
%13 = call @memmove(%7, %0, %12) : (ptr, ptr, i64) -> ptr
call @bcopy(%9, %7, %10) : (ptr, ptr, i64)
%14 = alloca, size 8, align 8
%15 = call @memmove(%14, %0, %1) : (ptr, ptr, i64) -> ptr
%16 = call @memmove(%7, %9, %1) : (ptr, ptr, i64) -> ptr
call @use(%5, %11, %13, %16) : (ptr, ptr, ptr, ptr)
return
}
"#,
);
assert_eq!(out.matches("call @memcpy(").count(), 3, "{out}");
assert!(!out.contains("call @bcopy("), "{out}");
assert_eq!(
out.matches("call @memmove(").count(),
2,
"a local and a pointer from outside, and two places in one object, stay moves, {out}"
);
}
#[test]
fn strcat_onto_what_was_just_written_is_a_copy_to_its_end() {
let text = r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 6 = { bytes " 1111\00" }, align 1, linkage(internal), constant
global @.Lstr.2 : bytes 3 = { bytes "ab\00" }, align 1, linkage(internal), constant
func @strcat(ptr, ptr) -> ptr, linkage(external);
func @strcpy(ptr, ptr) -> ptr, linkage(external);
func @memset(ptr, i32, i64) -> ptr, linkage(external);
func @use(ptr), linkage(external);
func @touch(ptr), linkage(external);
func @g(), linkage(external) {
block0:
%0 = alloca, size 64, align 16
jump block1
block1:
%1 = iconst.i32 88
%2 = iconst.i64 64
%3 = call @memset(%0, %1, %2) : (ptr, i32, i64) -> ptr
%4 = global_addr @.Lstr.0
%5 = call @strcpy(%0, %4) : (ptr, ptr) -> ptr
TOUCH
jump block2
block2:
%6 = global_addr @.Lstr.1
%7 = call @strcat(%0, %6) : (ptr, ptr) -> ptr
%8 = global_addr @.Lstr.2
%9 = call @strcat(%7, %8) : (ptr, ptr) -> ptr
call @use(%9) : (ptr)
return
}
"#;
let out = folded(&text.replace("TOUCH", ""));
assert!(!out.contains("call @strcat("), "{out}");
assert!(out.contains("iconst.i64 11"), "the first goes where the terminator was, {out}");
assert!(out.contains("iconst.i64 16"), "the second after the first, {out}");
assert!(out.contains("call @use(%0)"), "{out}");
let out = folded(&text.replace("TOUCH", "call @touch(%0) : (ptr)"));
assert_eq!(out.matches("call @strcat(").count(), 2, "{out}");
}
#[test]
fn strncpy_that_pads_nothing_is_a_copy() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @strncpy(ptr, ptr, i64) -> ptr, linkage(external);
func @use(ptr, ptr, ptr, ptr), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = iconst.i64 4
%3 = call @strncpy(%0, %1, %2) : (ptr, ptr, i64) -> ptr
%4 = iconst.i64 12
%5 = call @strncpy(%0, %1, %4) : (ptr, ptr, i64) -> ptr
%6 = iconst.i64 0
%7 = call @strncpy(%0, %1, %6) : (ptr, ptr, i64) -> ptr
%8 = iconst.i64 13
%9 = call @strncpy(%0, %1, %8) : (ptr, ptr, i64) -> ptr
call @use(%3, %5, %7, %9) : (ptr, ptr, ptr, ptr)
return
}
"#,
);
assert_eq!(out.matches("call @memcpy(").count(), 2, "{out}");
assert_eq!(out.matches("call @strncpy(").count(), 1, "thirteen pads a byte, {out}");
}
#[test]
fn strcpy_of_a_choice_of_one_length_is_a_copy() {
let out = folded(
r#"
global @.Lstr.0 : bytes 4 = { bytes "abc\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 4 = { bytes "xyz\00" }, align 1, linkage(internal), constant
func @strcpy(ptr, ptr) -> ptr, linkage(external);
func @use(ptr), linkage(external);
func @g(ptr, i1), linkage(external) {
block0(%0: ptr, %1: i1):
%2 = global_addr @.Lstr.0
%3 = global_addr @.Lstr.1
br_if %1, block1(%2), block1(%3)
block1(%4: ptr):
%5 = call @strcpy(%0, %4) : (ptr, ptr) -> ptr
call @use(%5) : (ptr)
return
}
"#,
);
assert!(out.contains("call @memcpy("), "{out}");
assert!(out.contains("iconst.i64 4"), "{out}");
}
#[test]
fn strlen_of_what_stores_just_wrote_is_its_length() {
let text = r#"
func @strlen(ptr) -> i64, linkage(external);
func @use(i64, i64), linkage(external);
func @touch(), linkage(external);
func @g(), linkage(external) {
block0:
%0 = alloca, size 8, align 1
%1 = alloca, size 8, align 1
%2 = iconst.i8 110
store %2 -> %0, align 1
%3 = iconst.i64 1
%4 = ptr_add %0, %3
%5 = iconst.i8 116
store %5 -> %4, align 1
%6 = iconst.i64 2
%7 = ptr_add %0, %6
%8 = iconst.i8 0
store %8 -> %7, align 1
store %8 -> %1, align 1
CALL
%9 = call @strlen(%0) : (ptr) -> i64
%10 = call @strlen(%4) : (ptr) -> i64
call @use(%9, %10) : (i64, i64)
return
}
"#;
let out = folded(&text.replace("CALL", ""));
assert!(!out.contains("call @strlen("), "{out}");
assert!(out.contains("iconst.i64 2"), "{out}");
let out = folded(&text.replace("CALL", "call @touch() : ()"));
assert_eq!(out.matches("call @strlen(").count(), 2, "{out}");
}
#[test]
fn memcmp_of_bytes_known_in_front_of_it_is_their_order() {
let text = r#"
global @.Lstr.0 : bytes 5 = { bytes "abcd\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 5 = { bytes "efgh\00" }, align 1, linkage(internal), constant
global @.Lstr.2 : bytes 5 = { bytes "3141\00" }, align 1, linkage(internal), constant
func @memcmp(ptr, ptr, i64) -> i32, linkage(external);
func @strcpy(ptr, ptr) -> ptr, linkage(external);
func @use(i32, i32, i32, i32, i32), linkage(external);
func @touch(ptr), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = global_addr @.Lstr.1
%3 = iconst.i64 4
%4 = call @memcmp(%1, %2, %3) : (ptr, ptr, i64) -> i32
%5 = iconst.i64 0
%6 = call @memcmp(%0, %2, %5) : (ptr, ptr, i64) -> i32
%7 = alloca, size 8, align 1
%8 = global_addr @.Lstr.2
%9 = call @strcpy(%7, %8) : (ptr, ptr) -> ptr
%10 = iconst.i64 2
%11 = ptr_add %7, %10
%12 = iconst.i64 1
%13 = call @memcmp(%7, %11, %12) : (ptr, ptr, i64) -> i32
TOUCH
%14 = call @memcmp(%11, %7, %12) : (ptr, ptr, i64) -> i32
%15 = call @memcmp(%0, %1, %3) : (ptr, ptr, i64) -> i32
call @use(%4, %6, %13, %14, %15) : (i32, i32, i32, i32, i32)
return
}
"#;
let out = folded(&text.replace("TOUCH", ""));
assert_eq!(out.matches("call @memcmp(").count(), 1, "the unknown one stays, {out}");
assert!(out.contains("iconst.i32 -1"), "{out}");
assert!(out.contains("iconst.i32 1"), "{out}");
let out = folded(&text.replace("TOUCH", "call @touch(%7) : (ptr)"));
assert_eq!(out.matches("call @memcmp(").count(), 2, "{out}");
}
#[test]
fn an_arm_that_calls_abort_is_not_a_way_in() {
let text = r#"
global @.Lstr.0 : bytes 5 = { bytes "3141\00" }, align 1, linkage(internal), constant
func @memcmp(ptr, ptr, i64) -> i32, linkage(external);
func @strcpy(ptr, ptr) -> ptr, linkage(external);
func @abort(), linkage(external);
func @other(), linkage(external);
func @use(i32), linkage(external);
func @g(i1), linkage(external) {
block0(%0: i1):
%1 = alloca, size 8, align 1
%2 = global_addr @.Lstr.0
%3 = call @strcpy(%1, %2) : (ptr, ptr) -> ptr
br_if %0, block1, block2
block1:
call @STOP() : ()
jump block2
block2:
%4 = iconst.i64 2
%5 = ptr_add %1, %4
%6 = iconst.i64 1
%7 = call @memcmp(%1, %5, %6) : (ptr, ptr, i64) -> i32
call @use(%7) : (i32)
return
}
"#;
let out = folded(&text.replace("STOP", "abort"));
assert!(!out.contains("call @memcmp("), "{out}");
let out = folded(&text.replace("STOP", "other"));
assert!(out.contains("call @memcmp("), "{out}");
}
#[test]
fn strlen_of_a_choice_between_literals_of_one_length_is_that_length() {
let text = r#"
global @.Lstr.0 : bytes 4 = { bytes "abc\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 4 = { bytes "xyz\00" }, align 1, linkage(internal), constant
global @.Lstr.2 : bytes 3 = { bytes "ab\00" }, align 1, linkage(internal), constant
func @strlen(ptr) -> i64, linkage(external);
func @use(i64), linkage(external);
func @g(i1), linkage(external) {
block0(%0: i1):
%1 = global_addr @.LEFT
%2 = global_addr @.Lstr.1
br_if %0, block1(%1), block1(%2)
block1(%3: ptr):
%4 = call @strlen(%3) : (ptr) -> i64
call @use(%4) : (i64)
return
}
"#;
let same = folded(&text.replace(".LEFT", ".Lstr.0"));
assert!(!same.contains("call @strlen("), "{same}");
assert!(same.contains("iconst.i64 3"), "{same}");
let differing = folded(&text.replace(".LEFT", ".Lstr.2"));
assert!(differing.contains("call @strlen("), "{differing}");
}
#[test]
fn strlen_at_a_bounded_step_into_a_held_string_is_the_rest() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @strlen(ptr) -> i64, linkage(external);
func @use(i64, i64), linkage(external);
func @g(i32), linkage(external) {
block0(%0: i32):
%1 = global_addr @.Lstr.0
%2 = iconst.i32 7
%3 = and %0, %2
%4 = sext.i64 %3
%5 = ptr_add %1, %4
%6 = call @strlen(%5) : (ptr) -> i64
%7 = iconst.i32 15
%8 = and %0, %7
%9 = sext.i64 %8
%10 = ptr_add %1, %9
%11 = call @strlen(%10) : (ptr) -> i64
call @use(%6, %11) : (i64, i64)
return
}
"#,
);
assert_eq!(out.matches("call @strlen(").count(), 1, "{out}");
assert!(out.contains("sub %6, %4"), "eleven less the step, {out}");
assert!(out.contains("iconst.i64 11"), "{out}");
}
#[test]
fn strnlen_answers_the_count_where_the_string_runs_past_it() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @strnlen(ptr, i64) -> i64, linkage(external);
func @use(i64, i64), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = iconst.i64 3
%2 = call @strnlen(%0, %1) : (ptr, i64) -> i64
%3 = iconst.i64 40
%4 = call @strnlen(%0, %3) : (ptr, i64) -> i64
call @use(%2, %4) : (i64, i64)
return
}
"#,
);
assert!(!out.contains("call @strnlen("), "{out}");
assert!(out.contains("iconst.i64 3"), "the count came first, {out}");
assert!(out.contains("iconst.i64 11"), "the terminator came first, {out}");
}
#[test]
fn strnlen_of_a_string_this_module_holds_takes_any_count() {
let out = folded(
r#"
global @.Lstr.0 : bytes 4 = { bytes "123\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @strnlen(ptr, i64) -> i64, linkage(external);
func @use(i64, i64, i64), linkage(external);
func @g(i64), linkage(external) {
block0(%0: i64):
%1 = global_addr @.Lstr.0
%2 = call @strnlen(%1, %0) : (ptr, i64) -> i64
%3 = iconst.i32 -2
%4 = sext.i64 %3
%5 = call @strnlen(%1, %4) : (ptr, i64) -> i64
%6 = global_addr @.Lstr.1
%7 = call @strnlen(%6, %0) : (ptr, i64) -> i64
call @use(%2, %5, %7) : (i64, i64, i64)
return
}
"#,
);
assert!(!out.contains("call @strnlen("), "{out}");
assert!(out.contains("icmp ult %0"), "the count against the length, {out}");
assert!(out.contains("select"), "and the smaller of the two, {out}");
assert!(out.contains("iconst.i64 3"), "a negative count is past the terminator, {out}");
assert!(out.contains("iconst.i64 0"), "an empty string is nothing to count, {out}");
}
#[test]
fn a_count_that_was_widened_on_the_way_in_is_still_a_count() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @strnlen(ptr, i64) -> i64, linkage(external);
func @g() -> i64, linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = iconst.i32 4
%2 = sext.i64 %1
%3 = call @strnlen(%0, %2) : (ptr, i64) -> i64
return %3
}
"#,
);
assert!(!out.contains("call @strnlen("), "{out}");
assert!(out.contains("iconst.i64 4"), "{out}");
}
#[test]
fn memchr_searches_the_object_rather_than_the_string_in_it() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @memchr(ptr, i32, i64) -> ptr, linkage(external);
func @use(ptr, ptr), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = iconst.i32 0
%2 = iconst.i64 12
%3 = call @memchr(%0, %1, %2) : (ptr, i32, i64) -> ptr
%4 = iconst.i32 100
%5 = iconst.i64 10
%6 = call @memchr(%0, %4, %5) : (ptr, i32, i64) -> ptr
call @use(%3, %6) : (ptr, ptr)
return
}
"#,
);
assert!(!out.contains("call @memchr("), "{out}");
assert!(out.contains("iconst.i64 11"), "the terminator is inside the count, {out}");
assert!(out.contains("inttoptr.ptr "), "the d is one byte past the count, {out}");
}
#[test]
fn a_memchr_that_runs_off_the_object_is_left_alone() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @memchr(ptr, i32, i64) -> ptr, linkage(external);
func @g() -> ptr, linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = iconst.i32 122
%2 = iconst.i64 13
%3 = call @memchr(%0, %1, %2) : (ptr, i32, i64) -> ptr
return %3
}
"#,
);
assert!(out.contains("call @memchr("), "{out}");
}
#[test]
fn the_two_character_searches_answer_from_either_end() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @strchr(ptr, i32) -> ptr, linkage(external);
func @strrchr(ptr, i32) -> ptr, linkage(external);
func @use(ptr, ptr, ptr, ptr), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = iconst.i32 111
%2 = call @strchr(%0, %1) : (ptr, i32) -> ptr
%3 = call @strrchr(%0, %1) : (ptr, i32) -> ptr
%4 = iconst.i32 0
%5 = call @strchr(%0, %4) : (ptr, i32) -> ptr
%6 = iconst.i32 122
%7 = call @strchr(%0, %6) : (ptr, i32) -> ptr
call @use(%2, %3, %5, %7) : (ptr, ptr, ptr, ptr)
return
}
"#,
);
assert!(!out.contains("call @strchr("), "{out}");
assert!(!out.contains("call @strrchr("), "{out}");
assert!(out.contains("iconst.i64 4"), "the first o, {out}");
assert!(out.contains("iconst.i64 7"), "the last o, {out}");
assert!(out.contains("iconst.i64 11"), "the terminator, {out}");
assert!(out.contains("inttoptr.ptr "), "there is no z in it, {out}");
}
#[test]
fn the_two_comparisons_answer_a_sign() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 6 = { bytes "hello\00" }, align 1, linkage(internal), constant
func @strcmp(ptr, ptr) -> i32, linkage(external);
func @strncmp(ptr, ptr, i64) -> i32, linkage(external);
func @use(i32, i32, i32), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = global_addr @.Lstr.1
%2 = call @strcmp(%0, %1) : (ptr, ptr) -> i32
%3 = call @strcmp(%1, %0) : (ptr, ptr) -> i32
%4 = iconst.i64 5
%5 = call @strncmp(%0, %1, %4) : (ptr, ptr, i64) -> i32
call @use(%2, %3, %5) : (i32, i32, i32)
return
}
"#,
);
assert!(!out.contains("call @strcmp("), "{out}");
assert!(!out.contains("call @strncmp("), "{out}");
assert!(out.contains("iconst.i32 1"), "the longer one is the greater, {out}");
assert!(out.contains("iconst.i32 -1"), "and the other way round, {out}");
assert!(out.contains("iconst.i32 0"), "five bytes of each are the same, {out}");
}
#[test]
fn a_comparison_against_the_empty_string_is_a_read_of_one_byte() {
let out = folded(
r#"
global @.Lstr.0 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @strcmp(ptr, ptr) -> i32, linkage(external);
func @use(i32, i32), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strcmp(%0, %1) : (ptr, ptr) -> i32
%3 = call @strcmp(%1, %0) : (ptr, ptr) -> i32
call @use(%2, %3) : (i32, i32)
return
}
"#,
);
assert!(!out.contains("call @strcmp("), "{out}");
assert_eq!(out.matches("load.i8 %0").count(), 2, "one read for each call, {out}");
assert_eq!(out.matches("zext").count(), 2, "read as an unsigned char, {out}");
assert_eq!(out.matches("sub").count(), 2, "and the difference each way round, {out}");
}
#[test]
fn a_short_count_settles_a_comparison_without_the_other_string() {
let out = folded(
r#"
global @.Lstr.0 : bytes 4 = { bytes "ozz\00" }, align 1, linkage(internal), constant
func @strncmp(ptr, ptr, i64) -> i32, linkage(external);
func @use(i32, i32), linkage(external);
func @g(ptr, ptr), linkage(external) {
block0(%0: ptr, %1: ptr):
%2 = global_addr @.Lstr.0
%3 = iconst.i32 0
%4 = sext.i64 %3
%5 = call @strncmp(%0, %1, %4) : (ptr, ptr, i64) -> i32
%6 = iconst.i32 1
%7 = sext.i64 %6
%8 = call @strncmp(%2, %0, %7) : (ptr, ptr, i64) -> i32
call @use(%5, %8) : (i32, i32)
return
}
"#,
);
assert!(!out.contains("call @strncmp("), "{out}");
assert!(out.contains("iconst.i32 0"), "no bytes to read is no difference, {out}");
assert!(out.contains("load.i8 %0"), "one byte of the other string, {out}");
assert!(out.contains("iconst.i32 111"), "against the first byte of this one, {out}");
}
#[test]
fn an_index_and_a_count_worked_out_from_constants_are_constants() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @strncmp(ptr, ptr, i64) -> i32, linkage(external);
func @use(i32), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = iconst.i64 1
%2 = ptr_add %0, %1
%3 = iconst.i32 1
%4 = iconst.i32 3
%5 = and %3, %4
%6 = sext.i64 %5
%7 = ptr_add %0, %6
%8 = iconst.i32 2
%9 = add.nsw %8, %3
%10 = sext.i64 %9
%11 = call @strncmp(%2, %7, %10) : (ptr, ptr, i64) -> i32
call @use(%11) : (i32)
return
}
"#,
);
assert!(!out.contains("call @strncmp("), "{out}");
}
#[test]
fn a_comparison_with_no_room_for_a_byte_is_left_alone() {
let out = folded(
r#"
global @.Lstr.0 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @strcmp(ptr, ptr) -> i8, linkage(external);
func @use(i8), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strcmp(%0, %1) : (ptr, ptr) -> i8
call @use(%2) : (i8)
return
}
"#,
);
assert!(out.contains("call @strcmp("), "{out}");
}
#[test]
fn the_two_spans_are_one_walk_each_way() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 4 = { bytes "hel\00" }, align 1, linkage(internal), constant
global @.Lstr.2 : bytes 3 = { bytes "wz\00" }, align 1, linkage(internal), constant
func @strspn(ptr, ptr) -> i64, linkage(external);
func @strcspn(ptr, ptr) -> i64, linkage(external);
func @use(i64, i64), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = global_addr @.Lstr.1
%2 = call @strspn(%0, %1) : (ptr, ptr) -> i64
%3 = global_addr @.Lstr.2
%4 = call @strcspn(%0, %3) : (ptr, ptr) -> i64
call @use(%2, %4) : (i64, i64)
return
}
"#,
);
assert!(!out.contains("call @strspn("), "{out}");
assert!(!out.contains("call @strcspn("), "{out}");
assert!(out.contains("iconst.i64 4"), "hello stops at the o, {out}");
assert!(out.contains("iconst.i64 6"), "the w is six bytes along, {out}");
}
#[test]
fn an_empty_set_is_a_span_of_nothing_or_of_all_of_it() {
let out = folded(
r#"
global @.Lstr.0 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @strspn(ptr, ptr) -> i64, linkage(external);
func @strcspn(ptr, ptr) -> i64, linkage(external);
func @strlen(ptr) -> i64, linkage(external);
func @use(i64, i64), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strspn(%0, %1) : (ptr, ptr) -> i64
%3 = call @strcspn(%0, %1) : (ptr, ptr) -> i64
call @use(%2, %3) : (i64, i64)
return
}
"#,
);
assert!(!out.contains("call @strspn("), "{out}");
assert!(!out.contains("call @strcspn("), "{out}");
assert!(out.contains("call @strlen(%0)"), "{out}");
assert!(out.contains("iconst.i64 0"), "{out}");
}
#[test]
fn a_strcspn_of_another_width_than_strlen_is_left_alone() {
let out = folded(
r#"
global @.Lstr.0 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @strcspn(ptr, ptr) -> i32, linkage(external);
func @strlen(ptr) -> i64, linkage(external);
func @g(ptr) -> i32, linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strcspn(%0, %1) : (ptr, ptr) -> i32
return %2
}
"#,
);
assert!(out.contains("call @strcspn("), "{out}");
}
#[test]
fn strpbrk_of_a_short_set_is_a_search_or_an_answer() {
let out = folded(
r#"
global @.Lstr.0 : bytes 2 = { bytes "w\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
func @strpbrk(ptr, ptr) -> ptr, linkage(external);
func @strchr(ptr, i32) -> ptr, linkage(external);
func @use(ptr, ptr), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = call @strpbrk(%0, %1) : (ptr, ptr) -> ptr
%3 = global_addr @.Lstr.1
%4 = call @strpbrk(%0, %3) : (ptr, ptr) -> ptr
call @use(%2, %4) : (ptr, ptr)
return
}
"#,
);
assert!(!out.contains("call @strpbrk("), "{out}");
assert!(out.contains("call @strchr(%0, "), "{out}");
assert!(out.contains("iconst.i32 119"), "{out}");
assert!(out.contains("inttoptr.ptr "), "the empty set is nowhere, {out}");
}
#[test]
fn strpbrk_over_two_strings_this_module_holds_is_a_place() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 3 = { bytes "wz\00" }, align 1, linkage(internal), constant
global @.Lstr.2 : bytes 3 = { bytes "qz\00" }, align 1, linkage(internal), constant
func @strpbrk(ptr, ptr) -> ptr, linkage(external);
func @use(ptr, ptr), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = global_addr @.Lstr.1
%2 = call @strpbrk(%0, %1) : (ptr, ptr) -> ptr
%3 = global_addr @.Lstr.2
%4 = call @strpbrk(%0, %3) : (ptr, ptr) -> ptr
call @use(%2, %4) : (ptr, ptr)
return
}
"#,
);
assert!(!out.contains("call @strpbrk("), "{out}");
assert!(out.contains("iconst.i64 6"), "the w is six bytes along, {out}");
assert!(out.contains("inttoptr.ptr "), "there is neither a q nor a z in it, {out}");
}
#[test]
fn the_older_spellings_of_the_two_searches_are_folded_as_well() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @index(ptr, i32) -> ptr, linkage(external);
func @rindex(ptr, i32) -> ptr, linkage(external);
func @use(ptr, ptr), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
%1 = iconst.i32 111
%2 = call @index(%0, %1) : (ptr, i32) -> ptr
%3 = call @rindex(%0, %1) : (ptr, i32) -> ptr
call @use(%2, %3) : (ptr, ptr)
return
}
"#,
);
assert!(!out.contains("call @index("), "{out}");
assert!(!out.contains("call @rindex("), "{out}");
assert!(out.contains("iconst.i64 4"), "the first o, {out}");
assert!(out.contains("iconst.i64 7"), "the last o, {out}");
}
#[test]
fn a_strrchr_of_the_terminator_is_a_strchr_of_it() {
let out = folded(
r#"
func @strrchr(ptr, i32) -> ptr, linkage(external);
func @g(ptr) -> ptr, linkage(external) {
block0(%0: ptr):
%1 = iconst.i32 0
%2 = call @strrchr(%0, %1) : (ptr, i32) -> ptr
return %2
}
"#,
);
assert!(!out.contains("call @strrchr("), "{out}");
assert!(out.contains("call @strchr(%0, "), "{out}");
}
#[test]
fn a_strrchr_of_another_character_needs_the_string() {
let out = folded(
r#"
func @strrchr(ptr, i32) -> ptr, linkage(external);
func @g(ptr) -> ptr, linkage(external) {
block0(%0: ptr):
%1 = iconst.i32 111
%2 = call @strrchr(%0, %1) : (ptr, i32) -> ptr
return %2
}
"#,
);
assert!(out.contains("call @strrchr("), "{out}");
}
#[test]
fn a_strlen_that_answers_nothing_is_not_the_one_the_library_has() {
let out = folded(
r#"
global @.Lstr.0 : bytes 12 = { bytes "hello world\00" }, align 1, linkage(internal), constant
func @strlen(ptr), linkage(external);
func @g(), linkage(external) {
block0:
%0 = global_addr @.Lstr.0
call @strlen(%0) : (ptr)
return
}
"#,
);
assert!(out.contains("call @strlen("), "{out}");
}
#[test]
fn a_checking_copy_that_fits_is_the_plain_copy() {
let out = folded(
r#"
func @__memcpy_chk(ptr, ptr, i64, i64) -> ptr, linkage(external);
func @__mempcpy_chk(ptr, ptr, i64, i64) -> ptr, linkage(external);
func @use(ptr, ptr), linkage(external);
func @g(ptr, ptr, i64), linkage(external) {
block0(%0: ptr, %1: ptr, %2: i64):
%3 = iconst.i64 4
%4 = iconst.i64 32
%5 = call @__memcpy_chk(%0, %1, %3, %4) : (ptr, ptr, i64, i64) -> ptr
%6 = iconst.i64 40
%7 = call @__memcpy_chk(%0, %1, %6, %4) : (ptr, ptr, i64, i64) -> ptr
%8 = call @__mempcpy_chk(%0, %1, %2, %4) : (ptr, ptr, i64, i64) -> ptr
call @use(%5, %7) : (ptr, ptr)
return
}
"#,
);
assert!(out.contains("call @memcpy(%0, %1, %3)"), "four bytes fit in thirty two, {out}");
assert!(out.contains("call @__memcpy_chk(%0, %1, %6, %4)"), "forty do not, {out}");
assert!(out.contains("call @__memcpy_chk(%0, %1, %2, %4)"), "nothing read the end, {out}");
assert!(!out.contains("call @__mempcpy_chk("), "{out}");
}
#[test]
fn a_checking_string_copy_goes_as_far_as_the_string_is_known() {
let out = folded(
r#"
global @.Lstr.0 : bytes 6 = { bytes "abcde\00" }, align 1, linkage(internal), constant
func @__stpcpy_chk(ptr, ptr, i64) -> ptr, linkage(external);
func @use(ptr, ptr), linkage(external);
func @g(ptr, ptr), linkage(external) {
block0(%0: ptr, %1: ptr):
%2 = global_addr @.Lstr.0
%3 = iconst.i64 32
%4 = call @__stpcpy_chk(%0, %2, %3) : (ptr, ptr, i64) -> ptr
%5 = call @__stpcpy_chk(%0, %1, %3) : (ptr, ptr, i64) -> ptr
%6 = call @__stpcpy_chk(%0, %1, %3) : (ptr, ptr, i64) -> ptr
call @use(%4, %5) : (ptr, ptr)
return
}
"#,
);
assert!(out.contains("call @memcpy(%0, %2, "), "{out}");
assert!(out.contains("iconst.i64 6"), "five bytes and a terminator, {out}");
assert!(out.contains("ptr_add %0"), "the answer is the end of the copy, {out}");
assert!(out.contains("call @__stpcpy_chk(%0, %1, %3)"), "{out}");
assert!(out.contains("call @__strcpy_chk(%0, %1, %3)"), "{out}");
}
#[test]
fn a_checking_string_copy_that_does_not_fit_is_a_checking_copy_of_a_count() {
let out = folded(
r#"
global @.Lstr.0 : bytes 6 = { bytes "abcde\00" }, align 1, linkage(internal), constant
func @__strcpy_chk(ptr, ptr, i64) -> ptr, linkage(external);
func @use(ptr), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = iconst.i64 4
%3 = call @__strcpy_chk(%0, %1, %2) : (ptr, ptr, i64) -> ptr
call @use(%3) : (ptr)
return
}
"#,
);
assert!(out.contains("call @__memcpy_chk(%0, %1, "), "{out}");
assert!(out.contains("iconst.i64 6"), "{out}");
}
#[test]
fn a_checking_append_of_nothing_is_the_destination() {
let out = folded(
r#"
global @.Lstr.0 : bytes 1 = { bytes "\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 4 = { bytes "abc\00" }, align 1, linkage(internal), constant
func @__strcat_chk(ptr, ptr, i64) -> ptr, linkage(external);
func @__strncat_chk(ptr, ptr, i64, i64) -> ptr, linkage(external);
func @use(ptr, ptr, ptr, ptr), linkage(external);
func @g(ptr, ptr), linkage(external) {
block0(%0: ptr, %1: ptr):
%2 = global_addr @.Lstr.0
%3 = global_addr @.Lstr.1
%4 = iconst.i64 32
%5 = call @__strcat_chk(%0, %2, %4) : (ptr, ptr, i64) -> ptr
%6 = iconst.i64 0
%7 = call @__strncat_chk(%0, %1, %6, %4) : (ptr, ptr, i64, i64) -> ptr
%8 = iconst.i64 5
%9 = call @__strncat_chk(%0, %3, %8, %4) : (ptr, ptr, i64, i64) -> ptr
%10 = iconst.i64 2
%11 = call @__strncat_chk(%0, %3, %10, %4) : (ptr, ptr, i64, i64) -> ptr
call @use(%5, %7, %9, %11) : (ptr, ptr, ptr, ptr)
return
}
"#,
);
assert!(out.contains("call @use(%0, %0, "), "{out}");
assert_eq!(
out.matches("call @__strcat_chk(%0, ").count(),
1,
"five is no limit on three, {out}"
);
assert_eq!(out.matches("call @__strncat_chk(%0, ").count(), 1, "two is, {out}");
}
#[test]
fn a_checking_sprintf_of_a_known_string_is_a_copy() {
let out = folded(
r#"
global @.Lstr.0 : bytes 6 = { bytes "hello\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 3 = { bytes "%d\00" }, align 1, linkage(internal), constant
func @__sprintf_chk(ptr, i32, i64, ptr, ...) -> i32, linkage(external);
func @use(i32, i32), linkage(external);
func @g(ptr, i32), linkage(external) {
block0(%0: ptr, %1: i32):
%2 = global_addr @.Lstr.0
%3 = global_addr @.Lstr.1
%4 = iconst.i32 0
%5 = iconst.i64 32
%6 = call @__sprintf_chk(%0, %4, %5, %2) : (ptr, i32, i64, ptr, ...) -> i32
%7 = call @__sprintf_chk(%0, %4, %5, %3, %1) : (ptr, i32, i64, ptr, ...) -> i32
call @use(%6, %7) : (i32, i32)
return
}
"#,
);
assert!(out.contains("call @memcpy(%0, %2, "), "{out}");
assert!(out.contains("iconst.i32 5"), "the length is the answer, {out}");
assert!(out.contains("call @__sprintf_chk(%0, %4, %5, %3, %1)"), "{out}");
}
#[test]
fn a_checking_snprintf_keeps_its_arguments_and_loses_its_check() {
let out = folded(
r#"
global @.Lstr.0 : bytes 3 = { bytes "%d\00" }, align 1, linkage(internal), constant
func @__snprintf_chk(ptr, i64, i32, i64, ptr, ...) -> i32, linkage(external);
func @use(i32, i32), linkage(external);
func @g(ptr, i32), linkage(external) {
block0(%0: ptr, %1: i32):
%2 = global_addr @.Lstr.0
%3 = iconst.i64 8
%4 = iconst.i32 0
%5 = iconst.i64 32
%6 = call @__snprintf_chk(%0, %3, %4, %5, %2, %1) : (ptr, i64, i32, i64, ptr, ...) -> i32
%7 = iconst.i32 1
%8 = call @__snprintf_chk(%0, %3, %7, %5, %2, %1) : (ptr, i64, i32, i64, ptr, ...) -> i32
call @use(%6, %8) : (i32, i32)
return
}
"#,
);
assert!(
out.contains("call @snprintf(%0, %3, %2, %1) : (ptr, i64, ptr, ...) -> i32"),
"{out}"
);
assert!(out.contains("call @__snprintf_chk(%0, %3, %7, %5, %2, %1)"), "{out}");
}
#[test]
fn a_plain_function_of_another_shape_keeps_the_check() {
let out = folded(
r#"
func @__memcpy_chk(ptr, ptr, i64, i64) -> ptr, linkage(external);
func @memcpy(ptr, ptr, i32) -> ptr, linkage(external);
func @use(ptr), linkage(external);
func @g(ptr, ptr), linkage(external) {
block0(%0: ptr, %1: ptr):
%2 = iconst.i64 4
%3 = iconst.i64 32
%4 = call @__memcpy_chk(%0, %1, %2, %3) : (ptr, ptr, i64, i64) -> ptr
call @use(%4) : (ptr)
return
}
"#,
);
assert!(out.contains("call @__memcpy_chk("), "{out}");
}
#[test]
fn a_copy_into_the_end_of_a_copy_names_the_end_the_first_fold_wrote() {
let out = folded(
r#"
global @.Lstr.0 : bytes 8 = { bytes "abcdEFG\00" }, align 1, linkage(internal), constant
global @.Lstr.1 : bytes 4 = { bytes "efg\00" }, align 1, linkage(internal), constant
func @mempcpy(ptr, ptr, i64) -> ptr, linkage(external);
func @use(ptr), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = global_addr @.Lstr.1
%3 = iconst.i64 4
%4 = call @mempcpy(%0, %1, %3) : (ptr, ptr, i64) -> ptr
%5 = call @mempcpy(%4, %2, %3) : (ptr, ptr, i64) -> ptr
call @use(%5) : (ptr)
return
}
"#,
);
assert!(!out.contains("call @mempcpy("), "{out}");
assert_eq!(out.matches("call @memcpy(").count(), 2, "{out}");
assert_eq!(out.matches("ptr_add").count(), 2, "{out}");
}
#[test]
fn a_counted_append_of_all_of_a_known_string_is_the_uncounted_one() {
let out = folded(
r#"
global @.Lstr.0 : bytes 4 = { bytes "foo\00" }, align 1, linkage(internal), constant
func @strncat(ptr, ptr, i64) -> ptr, linkage(external);
func @use(ptr, ptr), linkage(external);
func @g(ptr), linkage(external) {
block0(%0: ptr):
%1 = global_addr @.Lstr.0
%2 = iconst.i64 3
%3 = call @strncat(%0, %1, %2) : (ptr, ptr, i64) -> ptr
%4 = iconst.i64 2
%5 = call @strncat(%0, %1, %4) : (ptr, ptr, i64) -> ptr
call @use(%3, %5) : (ptr, ptr)
return
}
"#,
);
assert_eq!(out.matches("call @strcat(%0, %1)").count(), 1, "{out}");
assert_eq!(out.matches("call @strncat(").count(), 1, "{out}");
}
#[test]
fn a_count_fits_where_the_largest_it_may_be_fits() {
let text = r#"
func @__memcpy_chk(ptr, ptr, i64, i64) -> ptr, linkage(external);
func @use(ptr), linkage(external);
func @g(ptr, ptr, i1), linkage(external) {
block0(%0: ptr, %1: ptr, %2: i1):
%3 = iconst.i64 8
%4 = iconst.i64 4
br_if %2, block1(%3), block1(%4)
block1(%5: i64):
%6 = iconst.i64 SIZE
%7 = call @__memcpy_chk(%0, %1, %5, %6) : (ptr, ptr, i64, i64) -> ptr
call @use(%7) : (ptr)
return
}
"#;
let fits = folded(&text.replace("SIZE", "8"));
assert!(fits.contains("call @memcpy("), "{fits}");
let short = folded(&text.replace("SIZE", "7"));
assert!(short.contains("call @__memcpy_chk("), "{short}");
}
#[test]
fn rounding_a_widened_float_is_done_in_float() {
let text = r#"
func @floor(f64) -> f64, linkage(external);
func @sin(f64) -> f64, linkage(external);
func @use(f64, f64, f64), linkage(external);
func @g(f32, f64), linkage(external) {
block0(%0: f32, %1: f64):
%2 = fpext.f64 %0
%3 = call @floor(%2) : (f64) -> f64
%4 = call @sin(%2) : (f64) -> f64
%5 = call @floor(%1) : (f64) -> f64
call @use(%3, %4, %5) : (f64, f64, f64)
return
}
"#;
let out = folded(text);
assert!(out.contains("call @floorf(%0) : (f32) -> f32"), "{out}");
assert_eq!(out.matches("call @floor(").count(), 1, "the double one stays, {out}");
assert_eq!(out.matches("call @sin(").count(), 1, "{out}");
}
}