use std::collections::HashMap;
use std::collections::hash_map::Entry;
use rucc_base::Symbol;
use rucc_ir::{
Block, Datum, Def, Extra, Flags, Func, Imm, Inst, MemOrder, Module, Opcode, Pic, Type, Value,
};
use crate::extents::vouched;
use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
const FOLDED: &str = "load from a read only object folded to what it was initialized to";
const NO_FUEL: &str = "load from a read only object not folded, the pass ran out of fuel";
pub const NAME: &str = "image";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Image;
impl Pass for Image {
fn name(&self) -> &'static str {
NAME
}
fn describe(&self) -> &'static str {
"a load from an object nothing can write to becomes what the object was initialized to"
}
fn preserves(&self) -> Preserved {
Preserved::ALL.without(Analysis::Liveness)
}
fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
let mut stats = Stats::new();
let images = an.images();
if images.is_empty() {
return stats;
}
let blocks: Vec<Block> = func.blocks().collect();
for block in blocks {
let insts: Vec<Inst> = func.insts(block).collect();
for inst in insts {
let Some((opcode, imm)) = answer(func, inst, images) else { continue };
if !fuel.take() {
stats.missed(NO_FUEL);
continue;
}
let at = func.add_imm(imm);
let data = &mut func[inst];
data.opcode = opcode;
data.flags = Flags::NONE;
data.args = rucc_ir::ValueList::EMPTY;
data.extra = Extra::Imm(at);
stats.optimized(FOLDED);
}
}
stats
}
}
#[derive(Debug, Default, Clone)]
pub struct Images {
objects: HashMap<Symbol, Option<Object>>,
little_endian: bool,
}
impl Images {
#[must_use]
pub fn of(module: &Module, pic: Pic) -> Self {
let mut objects: HashMap<Symbol, Option<Object>> = HashMap::new();
for id in module.globals() {
let global = &module[id];
if !global.constant || !vouched(global, pic) {
continue;
}
let object = Object::of(module, global);
match objects.entry(global.name) {
Entry::Occupied(mut at) => *at.get_mut() = None,
Entry::Vacant(at) => {
at.insert(Some(object));
}
}
}
Self { objects, little_endian: module.datalayout.little_endian }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.objects.is_empty()
}
#[must_use]
pub fn read(&self, name: Symbol, ty: Type, offset: u64) -> Option<Imm> {
let object = self.objects.get(&name)?.as_ref()?;
let size = u64::from(ty.bits().div_ceil(8));
let end = offset.checked_add(size)?;
if size == 0 || end > object.size {
return None;
}
let mut at = 0u64;
for piece in &object.pieces {
let width = piece.size();
if at + width > offset {
return (at + width >= end)
.then(|| piece.read(ty, offset - at, size, self.little_endian))?;
}
at += width;
}
None
}
}
#[derive(Debug, Clone)]
struct Object {
size: u64,
pieces: Vec<Piece>,
}
impl Object {
fn of(module: &Module, global: &rucc_ir::Global) -> Self {
let data = global.init.map(|list| &module[list]).unwrap_or_default();
let pieces = data
.iter()
.map(|&datum| match datum {
Datum::Zero(bytes) => Piece::Zero(bytes),
Datum::Bytes(range) => Piece::Bytes(module[range].to_vec()),
Datum::Scalar { ty, value } => Piece::Scalar { ty, value: module[value] },
Datum::Addr(_) => Piece::Opaque(datum.size(module)),
})
.collect();
Self { size: global.size, pieces }
}
}
#[derive(Debug, Clone)]
enum Piece {
Zero(u64),
Bytes(Vec<u8>),
Scalar { ty: Type, value: Imm },
Opaque(u64),
}
impl Piece {
fn size(&self) -> u64 {
match self {
Self::Zero(bytes) | Self::Opaque(bytes) => *bytes,
Self::Bytes(bytes) => bytes.len() as u64,
Self::Scalar { ty, .. } => u64::from(ty.bits().div_ceil(8)) * u64::from(ty.lanes()),
}
}
fn read(&self, ty: Type, into: u64, size: u64, little_endian: bool) -> Option<Imm> {
match self {
Self::Zero(_) => Some(number(ty, 0)),
Self::Scalar { ty: held, value } => {
(into == 0 && held.is_scalar() && u64::from(held.bits().div_ceil(8)) == size)
.then_some(*value)
}
Self::Bytes(bytes) => {
let into = usize::try_from(into).ok()?;
let size = usize::try_from(size).ok()?;
let bytes = bytes.get(into..into.checked_add(size)?)?;
Some(number(ty, assemble(bytes, little_endian)))
}
Self::Opaque(_) => None,
}
}
}
fn answer(func: &Func, inst: Inst, images: &Images) -> Option<(Opcode, Imm)> {
let data = &func[inst];
if data.opcode != Opcode::Load || data.results != 1 || data.flags.contains(Flags::VOLATILE) {
return None;
}
let Extra::Mem(info) = data.extra else { return None };
if func[info].order != MemOrder::NotAtomic {
return None;
}
let ty = func[data.results().next()?].ty;
if !ty.is_scalar() || !(ty.is_int() || ty.is_float()) {
return None;
}
let (base, offset) = address(func, *func[data.args].first()?)?;
let Def::Result { inst: made, .. } = func[base].def else { return None };
if func[made].opcode != Opcode::GlobalAddr {
return None;
}
let Extra::Symbol(name) = func[made].extra else { return None };
let imm = images.read(name, ty, u64::try_from(offset).ok()?)?;
Some((if ty.is_int() { Opcode::IConst } else { Opcode::FConst }, imm))
}
fn address(func: &Func, mut value: Value) -> Option<(Value, i128)> {
let mut offset: i128 = 0;
loop {
let Def::Result { inst, .. } = func[value].def else { return Some((value, offset)) };
if func[inst].opcode != Opcode::PtrAdd {
return Some((value, offset));
}
let args = &func[func[inst].args];
let (step, step_ty) = crate::fold::constant(func, *args.get(1)?)?;
offset = offset.checked_add(step.signed(step_ty))?;
value = *args.first()?;
}
}
fn assemble(bytes: &[u8], little_endian: bool) -> u128 {
let mut value = 0u128;
if little_endian {
for &byte in bytes.iter().rev() {
value = value << 8 | u128::from(byte);
}
} else {
for &byte in bytes {
value = value << 8 | u128::from(byte);
}
}
value
}
fn number(ty: Type, bits: u128) -> Imm {
if ty.is_int() { Imm::int(bits as i128, ty) } else { Imm::from_bits(bits) }
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{Datum, Global, Imm, Linkage, Module, Pic, Reloc, Type};
use rucc_target::{TargetInfo, Triple};
use super::Images;
fn images(build: impl Fn(&mut Module) -> Vec<Datum>) -> (Interner, Images) {
let mut names = Interner::new();
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let mut module = Module::new(names.intern("t.c"), &target);
let data = build(&mut module);
let size = data.iter().map(|datum| datum.size(&module)).sum();
let mut global = Global::new(names.intern("g"), size, 8);
global.linkage = Linkage::Internal;
global.constant = true;
global.init = Some(module.push_data(&data));
module.add_global(global);
let images = Images::of(&module, Pic::Executable);
(names, images)
}
fn read(names: &mut Interner, images: &Images, ty: Type, offset: u64) -> Option<Imm> {
images.read(names.intern("g"), ty, offset)
}
#[test]
fn a_slot_of_a_table_reads_what_the_table_was_initialized_to() {
let (mut names, images) = images(|module| {
[10i128, 20, 30, 40]
.into_iter()
.map(|value| Datum::Scalar {
ty: Type::int(32),
value: module.add_imm(Imm::int(value, Type::int(32))),
})
.collect()
});
let mut at = |offset| read(&mut names, &images, Type::int(32), offset).map(Imm::unsigned);
assert_eq!(at(0), Some(10));
assert_eq!(at(8), Some(30));
assert_eq!(at(12), Some(40));
}
#[test]
fn a_byte_of_a_string_reads_the_byte_the_string_spells() {
let (mut names, images) = images(|module| vec![Datum::Bytes(module.push_bytes(b"abc\0"))]);
let mut at = |offset| read(&mut names, &images, Type::int(8), offset).map(Imm::unsigned);
assert_eq!(at(0), Some(u128::from(b'a')));
assert_eq!(at(1), Some(u128::from(b'b')));
assert_eq!(at(3), Some(0));
assert_eq!(at(4), None, "one past the end of the object");
}
#[test]
fn several_bytes_read_as_a_number_in_the_order_the_target_puts_them() {
let (mut names, images) =
images(|module| vec![Datum::Bytes(module.push_bytes(&[1, 2, 3, 4]))]);
assert_eq!(
read(&mut names, &images, Type::int(32), 0).map(Imm::unsigned),
Some(0x0403_0201),
"least significant byte first, which is what x86-64 is"
);
}
#[test]
fn a_run_of_zeroes_reads_zero() {
let (mut names, images) = images(|_| vec![Datum::Zero(16)]);
assert_eq!(read(&mut names, &images, Type::int(64), 8).map(Imm::unsigned), Some(0));
}
#[test]
fn a_part_of_a_scalar_is_not_read() {
let (mut names, images) = images(|module| {
vec![Datum::Scalar {
ty: Type::int(32),
value: module.add_imm(Imm::int(0x0403_0201, Type::int(32))),
}]
});
assert_eq!(read(&mut names, &images, Type::int(8), 0), None);
assert_eq!(read(&mut names, &images, Type::int(16), 2), None);
assert_eq!(
read(&mut names, &images, Type::int(32), 0).map(Imm::unsigned),
Some(0x0403_0201)
);
}
#[test]
fn an_access_that_crosses_from_one_piece_into_the_next_is_not_read() {
let (mut names, images) = images(|module| {
vec![Datum::Bytes(module.push_bytes(&[1, 2])), Datum::Bytes(module.push_bytes(&[3, 4]))]
});
assert_eq!(read(&mut names, &images, Type::int(32), 0), None);
assert_eq!(read(&mut names, &images, Type::int(16), 0).map(Imm::unsigned), Some(0x0201));
assert_eq!(read(&mut names, &images, Type::int(16), 2).map(Imm::unsigned), Some(0x0403));
}
#[test]
fn the_address_of_something_else_is_not_read() {
let (mut names, images) = images(|module| {
let symbol = module.name;
let to = module.add_reloc(Reloc { symbol, addend: 0, size: 8 });
vec![Datum::Addr(to), Datum::Bytes(module.push_bytes(&[7]))]
});
assert_eq!(read(&mut names, &images, Type::PTR, 0), None);
assert_eq!(
read(&mut names, &images, Type::int(8), 8).map(Imm::unsigned),
Some(7),
"what follows a relocation is still where it was"
);
}
#[test]
fn past_the_end_of_the_object_is_not_read() {
let (mut names, images) = images(|_| vec![Datum::Zero(4)]);
assert_eq!(read(&mut names, &images, Type::int(32), 4), None);
assert_eq!(read(&mut names, &images, Type::int(64), 0), None);
}
#[test]
fn a_global_that_is_not_read_only_has_no_image() {
let mut names = Interner::new();
let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
let mut module = Module::new(names.intern("t.c"), &target);
let mut global = Global::new(names.intern("g"), 4, 4);
global.linkage = Linkage::Internal;
global.init = Some(module.push_data(&[Datum::Zero(4)]));
module.add_global(global);
let images = Images::of(&module, Pic::Executable);
assert!(images.is_empty());
assert_eq!(images.read(names.intern("g"), Type::int(32), 0), None);
}
}