use std::collections::{HashMap, HashSet};
use rucc_base::{Interner, Symbol};
use rucc_ir::{
CallInfo, Datum, Def, Extra, 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 REPLACEMENTS: [&str; 7] = ["fputc", "fputs", "fwrite", "putchar", "puts", "strchr", "strlen"];
const SOURCES: [&str; 19] = [
"fprintf",
"fprintf_unlocked",
"fputs",
"fputs_unlocked",
"index",
"memchr",
"printf",
"printf_unlocked",
"rindex",
"strchr",
"strcmp",
"strcspn",
"strlen",
"strncmp",
"strnlen",
"strpbrk",
"strrchr",
"strspn",
"strstr",
];
#[derive(Debug, Clone, PartialEq, Eq)]
enum Plan {
Drop,
Answer(Answer),
Swap {
callee: Symbol,
signature: Signature,
args: Vec<Argument>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Answer {
Along(Value, u64),
Nowhere,
Number(i128),
Least {
count: Value,
len: u64,
},
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>),
}
struct Shapes {
held: HashMap<&'static str, Option<(Symbol, 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();
for id in module.funcs() {
let func = &module[id];
let spelled = func.spelled.unwrap_or(func.name);
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() {
if let Some(slot) = held.get_mut(names.resolve(module[id].name)) {
*slot = None;
}
}
for id in module.aliases() {
if let Some(slot) = held.get_mut(names.resolve(module[id].name)) {
*slot = None;
}
}
Self { held }
}
fn get(&self, name: &'static str) -> Option<(Symbol, Signature)> {
self.held.get(name)?.clone()
}
}
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]),
_ => {
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 defined: HashSet<Symbol> = module
.funcs()
.filter(|&id| !module[id].is_declaration())
.map(|id| module[id].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() || !mentions(&module[id], names, &standard) {
continue;
}
let mut stats = Stats::new();
let plans = {
let func = &module[id];
let site = Site {
module,
func,
cfg: &Cfg::new(func),
shapes: &shapes,
counts: &uses::count(func),
defined: &defined,
standard: &standard,
names,
no_builtin,
pic,
};
site.survey(fuel, &mut stats)
};
for (inst, plan) in plans {
apply(module, id, names, &mut texts, inst, plan);
}
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],
defined: &'a HashSet<Symbol>,
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",
});
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 Extra::Call(at) = data.extra else { return None };
let callee = self.func[at].callee?;
if self.defined.contains(&callee) {
return None;
}
let name = self.names.resolve(self.standard.get(&callee).copied().unwrap_or(callee));
if self.no_builtin.iter().any(|it| it == name) {
return None;
}
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),
"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),
"strlen" => self.strlen(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),
_ => 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, data: &InstData, args: &[Value]) -> Option<Plan> {
if args.len() != 1 || self.func[args[0]].ty != Type::PTR {
return None;
}
self.answers(data)?;
let text = self.one(args[0])?;
Some(Plan::Answer(Answer::Number(i128::try_from(text.len()).ok()?)))
}
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 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], DEPTH) {
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, DEPTH)?, 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 call(&self, callee: &'static str, args: Vec<Argument>) -> Option<Plan> {
let (callee, signature) = self.shapes.get(callee)?;
Some(Plan::Swap { callee, signature, args })
}
fn one(&self, value: Value) -> Option<Vec<u8>> {
let mut candidates = self.strings(value, DEPTH)?;
(candidates.len() == 1).then(|| candidates.pop()).flatten()
}
fn strings(&self, value: Value, depth: u32) -> Option<Vec<Vec<u8>>> {
if depth == 0 {
return None;
}
match self.func[value].def {
Def::Param { block, index } => {
let preds = self.cfg.predecessors(block);
if preds.is_empty() {
return None;
}
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(arg, depth - 1)?);
}
}
(!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(then, depth - 1)?;
all.extend(self.strings(other, depth - 1)?);
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(_) => 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,
) {
let (callee, signature, args) = match plan {
Plan::Drop => {
module[id].remove_inst(inst);
return;
}
Plan::Answer(answer) => {
let width = size(module);
answered(&mut module[id], inst, answer, width);
return;
}
Plan::Swap { callee, signature, args } => (callee, signature, args),
};
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::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 results: Vec<Type> = signature.return_types().collect();
let sig = func.add_signature(signature);
let varargs = func.push_abis(&[]);
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, inst);
let forward: HashMap<Value, Value> = func[inst]
.results()
.zip(func[made].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(inst);
}
fn answered(func: &mut Func, inst: Inst, answer: Answer, width: Type) {
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::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);
}
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 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_name_this_module_defines_is_that_definition() {
let out = folded(
r#"
global @.Lstr.0 : bytes 3 = { bytes "a\0a\00" }, align 1, linkage(internal), constant
func @printf(ptr, ...) -> i32, linkage(external) {
block0(%0: ptr):
%1 = iconst.i32 0
return %1
}
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 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 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}");
}
}