use rucc_ir::{Block, Extra, Flags, Func, Inst, MemOrder, Opcode};
use crate::alias::{Origin, origin};
use crate::range::query::Ranges;
pub const VOLATILE: &str = "it is volatile and every access to it is observable";
pub const ATOMIC: &str = "it is atomic and its place in the order is part of the program";
pub const EFFECTS: &str = "it does something rather than working out a value";
pub const CALL: &str = "nothing here knows what the call does";
pub const BY_ZERO: &str = "the divisor is not known to be other than zero";
pub const OVERFLOW: &str = "the division could be the most negative value over minus one";
pub const ADDRESS: &str = "the address is not known to be one it may read";
#[must_use]
pub fn is_safe(func: &Func, inst: Inst, ranges: &mut Ranges<'_>, at: Block) -> bool {
why_not(func, inst, ranges, at).is_none()
}
#[must_use]
pub fn why_not(
func: &Func,
inst: Inst,
ranges: &mut Ranges<'_>,
at: Block,
) -> Option<&'static str> {
let data = func[inst];
if data.flags.contains(Flags::VOLATILE) {
return Some(VOLATILE);
}
let order = match data.extra {
Extra::Mem(at) | Extra::Rmw(_, at) => func[at].order,
_ => MemOrder::NotAtomic,
};
if order != MemOrder::NotAtomic {
return Some(ATOMIC);
}
match data.opcode {
Opcode::SDiv | Opcode::SRem => division(func, inst, ranges, at, true),
Opcode::UDiv | Opcode::URem => division(func, inst, ranges, at, false),
Opcode::Load => load(func, inst),
Opcode::Call | Opcode::CallIndirect | Opcode::TailCall => Some(CALL),
other if other.has_effects() => Some(EFFECTS),
_ => None,
}
}
fn division(
func: &Func,
inst: Inst,
ranges: &mut Ranges<'_>,
at: Block,
signed: bool,
) -> Option<&'static str> {
let args = &func[func[inst].args];
let (top, bottom) = (*args.first()?, *args.get(1)?);
let ty = func[bottom].ty;
if !ty.is_int() || !ty.is_scalar() {
return Some(BY_ZERO);
}
if !ranges.at(bottom, at).nonzero() {
return Some(BY_ZERO);
}
if !signed {
return None;
}
let bits = ty.bits();
let all_ones = u128::MAX >> (u128::BITS - bits);
let most_negative = all_ones ^ (all_ones >> 1);
if ranges.at(bottom, at).contains(all_ones) && ranges.at(top, at).contains(most_negative) {
return Some(OVERFLOW);
}
None
}
fn load(func: &Func, inst: Inst) -> Option<&'static str> {
let pointer = *func[func[inst].args].first()?;
let (Origin::Local(local), Some(offset)) = origin(func, pointer) else {
return Some(ADDRESS);
};
let data = func[local];
if !func[data.args].is_empty() {
return Some(ADDRESS);
}
let Extra::Mem(at) = data.extra else {
return Some(ADDRESS);
};
let object = i128::from(func[at].size);
let ty = func[inst].results().next().map(|value| func[value].ty)?;
let bits = u64::from(ty.bits()) * u64::from(ty.lanes());
if bits == 0 {
return Some(ADDRESS);
}
let start = i128::from(offset);
let end = start + i128::from(bits.div_ceil(8));
if start >= 0 && end <= object { None } else { Some(ADDRESS) }
}
#[cfg(test)]
mod tests {
use rucc_base::{Interner, Symbol};
use rucc_ir::{
Builder, Extra, Flags, Func, Inst, InstData, MemInfo, MemOrder, Opcode, Restrict,
Signature, Type, Value,
};
use super::{ADDRESS, ATOMIC, BY_ZERO, CALL, EFFECTS, OVERFLOW, VOLATILE, is_safe, why_not};
use crate::cfg::Cfg;
use crate::dom::Dominators;
use crate::range::query::Ranges;
fn record(size: u64, order: MemOrder) -> MemInfo {
MemInfo { size, align: 8, order, tbaa: None, restrict: Restrict::NONE }
}
fn local(build: &mut Builder<'_>, size: u64) -> Value {
let mem = build.func().add_mem(record(size, MemOrder::NotAtomic));
build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
}
fn asked(write: impl FnOnce(&mut Builder<'_>, [Value; 3], Symbol)) -> Option<&'static str> {
let mut names = Interner::new();
let params = [Type::int(32), Type::int(32), Type::PTR];
let mut func = Func::new(names.intern("f"), Signature::new().with_params(¶ms));
let callee = names.intern("g");
let entry = func.create_block();
let handed = params.map(|ty| func.append_param(entry, ty));
let mut build = Builder::new(&mut func, entry);
write(&mut build, handed, callee);
build.ret(&[]);
let last: Inst = func
.insts(entry)
.filter(|inst| !func.is_terminator(*inst))
.last()
.expect("the caller wrote one");
let cfg = Cfg::new(&func);
let dom = Dominators::new(&cfg);
let mut ranges = Ranges::new(&func, &cfg, &dom);
let answer = why_not(&func, last, &mut ranges, entry);
assert_eq!(is_safe(&func, last, &mut ranges, entry), answer.is_none(), "the two agree");
answer
}
#[test]
fn adding_two_numbers_may_happen_early() {
let why = asked(|build, [a, b, _], _| {
build.binary(Opcode::Add, a, b, Flags::NONE);
});
assert_eq!(why, None);
}
#[test]
fn dividing_by_something_that_could_be_zero_may_not() {
let why = asked(|build, [a, b, _], _| {
build.binary(Opcode::UDiv, a, b, Flags::NONE);
});
assert_eq!(why, Some(BY_ZERO));
}
#[test]
fn dividing_by_a_number_the_ranges_have_settled_may() {
let why = asked(|build, [a, _, _], _| {
let three = build.iconst(Type::int(32), 3);
build.binary(Opcode::UDiv, a, three, Flags::NONE);
});
assert_eq!(why, None);
}
#[test]
fn a_remainder_is_the_same_question_as_a_division() {
let why = asked(|build, [a, b, _], _| {
build.binary(Opcode::SRem, a, b, Flags::NONE);
});
assert_eq!(why, Some(BY_ZERO));
}
#[test]
fn the_most_negative_value_over_minus_one_is_the_other_trap() {
let why = asked(|build, [a, _, _], _| {
let minus_one = build.iconst(Type::int(32), -1);
build.binary(Opcode::SDiv, a, minus_one, Flags::NONE);
});
assert_eq!(why, Some(OVERFLOW));
}
#[test]
fn either_operand_ruling_out_its_half_of_the_overflow_is_enough() {
let why = asked(|build, [_, _, _], _| {
let one = build.iconst(Type::int(32), 1);
let minus_one = build.iconst(Type::int(32), -1);
build.binary(Opcode::SDiv, one, minus_one, Flags::NONE);
});
assert_eq!(why, None);
}
#[test]
fn an_unsigned_division_has_no_overflow_to_ask_about() {
let why = asked(|build, [a, _, _], _| {
let minus_one = build.iconst(Type::int(32), -1);
build.binary(Opcode::UDiv, a, minus_one, Flags::NONE);
});
assert_eq!(why, None);
}
#[test]
fn a_volatile_load_may_not_happen_early() {
let why = asked(|build, [_, _, _], _| {
let slot = local(build, 4);
build.load(Type::int(32), slot, record(4, MemOrder::NotAtomic), Flags::VOLATILE);
});
assert_eq!(why, Some(VOLATILE), "the address is fine and the volatility is not");
}
#[test]
fn an_atomic_load_may_not_happen_early() {
let why = asked(|build, [_, _, _], _| {
let slot = local(build, 4);
build.atomic_load(Type::int(32), slot, record(4, MemOrder::Acquire), Flags::NONE);
});
assert_eq!(why, Some(ATOMIC));
}
#[test]
fn a_store_may_not_happen_early() {
let why = asked(|build, [a, _, _], _| {
let slot = local(build, 4);
build.store(a, slot, record(4, MemOrder::NotAtomic), Flags::NONE);
});
assert_eq!(why, Some(EFFECTS));
}
#[test]
fn a_call_may_not_happen_early() {
let why = asked(|build, [_, _, _], callee| {
let signature = build.func().add_signature(Signature::new());
build.call(callee, signature, &[]);
});
assert_eq!(why, Some(CALL));
}
#[test]
fn a_load_of_bytes_that_are_inside_a_local_may_happen_early() {
let why = asked(|build, [_, _, _], _| {
let slot = local(build, 4);
build.load(Type::int(32), slot, record(4, MemOrder::NotAtomic), Flags::NONE);
});
assert_eq!(why, None);
}
#[test]
fn a_load_that_runs_off_the_end_of_a_local_may_not() {
let why = asked(|build, [_, _, _], _| {
let slot = local(build, 2);
build.load(Type::int(32), slot, record(4, MemOrder::NotAtomic), Flags::NONE);
});
assert_eq!(why, Some(ADDRESS), "four bytes read out of two bytes of storage");
}
#[test]
fn a_load_at_an_offset_that_is_still_inside_may() {
let why = asked(|build, [_, _, _], _| {
let slot = local(build, 8);
let four = build.iconst(Type::int(64), 4);
let at = build.binary(Opcode::PtrAdd, slot, four, Flags::NONE);
build.load(Type::int(32), at, record(4, MemOrder::NotAtomic), Flags::NONE);
});
assert_eq!(why, None);
}
#[test]
fn a_load_through_a_pointer_the_function_was_handed_may_not() {
let why = asked(|build, [_, _, pointer], _| {
build.load(Type::int(32), pointer, record(4, MemOrder::NotAtomic), Flags::NONE);
});
assert_eq!(why, Some(ADDRESS));
}
}