use std::cmp::Ordering as CmpOrdering;
use std::hash::Hasher;
use std::sync::atomic::{AtomicU32, Ordering};
use rayon::prelude::*;
use super::csr::{Csr, InputError};
use super::hash::hasher;
use crate::ids::{SectionId, SymbolId};
const PARALLEL_THRESHOLD: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IcfMode {
All,
Safe,
}
#[derive(Clone, Copy, Debug)]
pub struct IcfSection<'a> {
pub contents: &'a [u8],
pub key: u64,
pub foldable: bool,
pub address_significant: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum IcfTarget {
Section {
section: SectionId,
offset: u64,
},
Symbol(SymbolId),
Value(u64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IcfReloc {
pub offset: u64,
pub kind: u32,
pub addend: i64,
pub target: IcfTarget,
}
#[derive(Clone, Debug)]
pub struct IcfInput<'a> {
sections: Vec<IcfSection<'a>>,
relocs: Csr<IcfReloc>,
}
impl<'a> IcfInput<'a> {
pub fn new(sections: Vec<IcfSection<'a>>, relocs: Csr<IcfReloc>) -> Result<Self, InputError> {
let len = sections.len();
if u32::try_from(len).is_err() {
return Err(InputError::TooLarge("section count"));
}
if relocs.rows() != len {
return Err(InputError::RowCount {
expected: len,
found: relocs.rows(),
});
}
let bad = relocs
.values()
.par_iter()
.find_map_first(|reloc| match reloc.target {
IcfTarget::Section { section, .. } if section.index() >= len => Some(section),
_ => None,
});
if let Some(section) = bad {
return Err(InputError::OutOfRange {
what: "relocation target section",
index: u64::from(section.as_u32()),
len,
});
}
Ok(Self { sections, relocs })
}
#[must_use]
pub fn len(&self) -> usize {
self.sections.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.sections.is_empty()
}
#[must_use]
pub fn sections(&self) -> &[IcfSection<'a>] {
&self.sections
}
#[must_use]
pub fn relocs(&self, section: SectionId) -> &[IcfReloc] {
self.relocs.row(section.index())
}
}
#[derive(Clone, Copy, Debug)]
struct Slot {
hash: u64,
id: u32,
start: bool,
done: bool,
}
struct Folder<'i, 'a> {
input: &'i IcfInput<'a>,
class: Vec<AtomicU32>,
}
impl Folder<'_, '_> {
fn section(&self, id: u32) -> &IcfSection<'_> {
&self.input.sections[id as usize]
}
fn relocs(&self, id: u32) -> &[IcfReloc] {
self.input.relocs.row(id as usize)
}
fn class_of(&self, section: SectionId) -> u32 {
self.class[section.index()].load(Ordering::Relaxed)
}
fn constant_hash(&self, id: u32) -> u64 {
let section = self.section(id);
let relocs = self.relocs(id);
let mut h = hasher();
h.write_u64(section.key);
h.write_usize(section.contents.len());
h.write(section.contents);
h.write_usize(relocs.len());
for reloc in relocs {
let (tag, value) = target_shape(reloc.target);
h.write_u64(reloc.offset);
h.write_u32(reloc.kind);
h.write_i64(reloc.addend);
h.write_u8(tag);
h.write_u64(value);
}
h.finish()
}
fn compare_constant(&self, a: u32, b: u32) -> CmpOrdering {
let (sa, sb) = (self.section(a), self.section(b));
let (ra, rb) = (self.relocs(a), self.relocs(b));
sa.key
.cmp(&sb.key)
.then_with(|| sa.contents.len().cmp(&sb.contents.len()))
.then_with(|| ra.len().cmp(&rb.len()))
.then_with(|| sa.contents.cmp(sb.contents))
.then_with(|| {
let key = |r: &IcfReloc| {
let (tag, value) = target_shape(r.target);
(r.offset, r.kind, r.addend, tag, value)
};
ra.iter().map(key).cmp(rb.iter().map(key))
})
}
fn variable_hash(&self, id: u32) -> u64 {
let mut h = hasher();
for reloc in self.relocs(id) {
if let IcfTarget::Section { section, .. } = reloc.target {
h.write_u32(self.class_of(section));
}
}
h.finish()
}
fn compare_variable(&self, a: u32, b: u32) -> CmpOrdering {
let key = |reloc: &IcfReloc| match reloc.target {
IcfTarget::Section { section, .. } => self.class_of(section),
_ => 0,
};
self.relocs(a)
.iter()
.map(key)
.cmp(self.relocs(b).iter().map(key))
}
fn has_section_targets(&self, id: u32) -> bool {
self.relocs(id)
.iter()
.any(|reloc| matches!(reloc.target, IcfTarget::Section { .. }))
}
fn split_run(&self, run: &mut [Slot], compare: &(impl Fn(u32, u32) -> CmpOrdering + Sync)) {
let Some(first) = run.first().map(|slot| slot.id) else {
return;
};
for slot in run.iter_mut() {
slot.start = false;
}
run[0].start = true;
let equal_to_first = |slot: &Slot| compare(first, slot.id) == CmpOrdering::Equal;
let all_equal = if run.len() > PARALLEL_THRESHOLD {
run[1..].par_iter().all(equal_to_first)
} else {
run[1..].iter().all(equal_to_first)
};
if all_equal {
return;
}
let order = |x: &Slot, y: &Slot| compare(x.id, y.id).then(x.id.cmp(&y.id));
if run.len() > PARALLEL_THRESHOLD {
run.par_sort_unstable_by(order);
} else {
run.sort_unstable_by(order);
}
for index in 1..run.len() {
run[index].start = compare(run[index - 1].id, run[index].id) != CmpOrdering::Equal;
}
run[0].start = true;
}
fn split_class(&self, class: &mut [Slot], compare: &(impl Fn(u32, u32) -> CmpOrdering + Sync)) {
if class.len() > PARALLEL_THRESHOLD {
class.par_sort_unstable_by_key(|slot| (slot.hash, slot.id));
} else {
class.sort_unstable_by_key(|slot| (slot.hash, slot.id));
}
let mut rest = class;
while let Some(hash) = rest.first().map(|slot| slot.hash) {
let len = rest.iter().take_while(|slot| slot.hash == hash).count();
let (run, tail) = rest.split_at_mut(len);
self.split_run(run, compare);
rest = tail;
}
}
fn commit(&self, work: &mut Vec<Slot>) {
work.par_chunk_by_mut(|_, next| !next.start)
.for_each(|class| {
let representative = class[0].id;
for slot in class.iter() {
self.class[slot.id as usize].store(representative, Ordering::Relaxed);
}
if class.len() == 1 || !self.has_section_targets(representative) {
for slot in class.iter_mut() {
slot.done = true;
}
}
});
work.retain(|slot| !slot.done);
}
}
fn target_shape(target: IcfTarget) -> (u8, u64) {
match target {
IcfTarget::Section { offset, .. } => (0, offset),
IcfTarget::Symbol(symbol) => (1, u64::from(symbol.as_u32())),
IcfTarget::Value(value) => (2, value),
}
}
fn count_classes(work: &[Slot]) -> usize {
work.par_iter().filter(|slot| slot.start).count()
}
#[must_use]
pub fn fold_identical(input: &IcfInput<'_>, mode: IcfMode) -> IcfResult {
let len = input.len();
let folder = Folder {
input,
class: (0..len)
.into_par_iter()
.map(|index| AtomicU32::new(index as u32))
.collect(),
};
let mut work: Vec<Slot> = input
.sections
.par_iter()
.enumerate()
.filter(|(_, section)| {
section.foldable && (mode == IcfMode::All || !section.address_significant)
})
.map(|(index, _)| {
let id = index as u32;
Slot {
hash: folder.constant_hash(id),
id,
start: false,
done: false,
}
})
.collect();
work.par_sort_unstable_by_key(|slot| (slot.hash, slot.id));
let mut previous = None;
for slot in &mut work {
slot.start = previous != Some(slot.hash);
previous = Some(slot.hash);
}
let constant = |a, b| folder.compare_constant(a, b);
work.par_chunk_by_mut(|_, next| !next.start)
.for_each(|run| folder.split_run(run, &constant));
folder.commit(&mut work);
let mut rounds = 1;
while !work.is_empty() {
rounds += 1;
let before = count_classes(&work);
work.par_iter_mut()
.for_each(|slot| slot.hash = folder.variable_hash(slot.id));
let variable = |a, b| folder.compare_variable(a, b);
work.par_chunk_by_mut(|_, next| !next.start)
.for_each(|class| folder.split_class(class, &variable));
let after = count_classes(&work);
folder.commit(&mut work);
if after == before {
break;
}
}
IcfResult {
fold_into: folder
.class
.into_iter()
.map(|class| SectionId::from_u32(class.into_inner()))
.collect(),
rounds,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IcfResult {
fold_into: Vec<SectionId>,
rounds: usize,
}
impl IcfResult {
#[must_use]
pub fn fold_into(&self) -> &[SectionId] {
&self.fold_into
}
#[must_use]
pub fn representative(&self, section: SectionId) -> SectionId {
self.fold_into
.get(section.index())
.copied()
.unwrap_or(section)
}
#[must_use]
pub fn is_folded(&self, section: SectionId) -> bool {
self.representative(section) != section
}
#[must_use]
pub fn num_folded(&self) -> usize {
self.fold_into
.par_iter()
.enumerate()
.filter(|&(index, rep)| rep.index() != index)
.count()
}
#[must_use]
pub fn rounds(&self) -> usize {
self.rounds
}
#[must_use]
pub fn report(&self) -> IcfReport {
let mut pairs: Vec<(SectionId, SectionId)> = self
.fold_into
.iter()
.enumerate()
.filter(|&(index, rep)| rep.index() != index)
.map(|(index, &rep)| (rep, SectionId::new(index)))
.collect();
pairs.sort_by_key(|&(rep, _)| rep);
let mut report = IcfReport {
kept: Vec::new(),
bounds: vec![0],
folded: Vec::with_capacity(pairs.len()),
};
for (rep, member) in pairs {
if report.kept.last() != Some(&rep) {
if !report.kept.is_empty() {
report.bounds.push(report.folded.len());
}
report.kept.push(rep);
}
report.folded.push(member);
}
if !report.kept.is_empty() {
report.bounds.push(report.folded.len());
}
report
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IcfReport {
kept: Vec<SectionId>,
bounds: Vec<usize>,
folded: Vec<SectionId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IcfGroup<'r> {
pub kept: SectionId,
pub folded: &'r [SectionId],
}
impl IcfReport {
#[must_use]
pub fn len(&self) -> usize {
self.kept.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.kept.is_empty()
}
pub fn groups(&self) -> impl Iterator<Item = IcfGroup<'_>> + '_ {
self.kept.iter().enumerate().map(|(index, &kept)| IcfGroup {
kept,
folded: self
.folded
.get(self.bounds[index]..self.bounds[index + 1])
.unwrap_or(&[]),
})
}
}
#[cfg(test)]
mod tests {
use super::super::csr::CsrBuilder;
use super::*;
fn id(index: usize) -> SectionId {
SectionId::new(index)
}
fn call(section: usize) -> IcfReloc {
IcfReloc {
offset: 1,
kind: 4,
addend: -4,
target: IcfTarget::Section {
section: id(section),
offset: 0,
},
}
}
fn section(contents: &[u8]) -> IcfSection<'_> {
IcfSection {
contents,
key: 1,
foldable: true,
address_significant: false,
}
}
fn run(
sections: Vec<IcfSection<'_>>,
relocs: &[(usize, IcfReloc)],
mode: IcfMode,
) -> IcfResult {
let mut builder = CsrBuilder::new(sections.len());
for &(row, reloc) in relocs {
builder.push(row, reloc);
}
let input = IcfInput::new(sections, builder.build().unwrap()).unwrap();
fold_identical(&input, mode)
}
#[test]
fn folds_identical_leaves() {
let code = b"\x55\xc3";
let result = run(
vec![
section(code),
section(b"\x90"),
section(code),
section(code),
],
&[],
IcfMode::All,
);
assert_eq!(result.fold_into(), &[id(0), id(1), id(0), id(0)]);
let report = result.report();
let groups: Vec<_> = report.groups().collect();
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].kept, id(0));
assert_eq!(groups[0].folded, &[id(2), id(3)]);
}
#[test]
fn folds_mutual_recursion() {
let code = b"\xe8\0\0\0\0\xc3";
let sections = vec![
section(code),
section(code),
section(code),
section(code),
section(code),
section(b"\xc3"),
];
let relocs = [
(0, call(1)),
(1, call(0)),
(2, call(3)),
(3, call(2)),
(4, call(5)),
];
let result = run(sections, &relocs, IcfMode::All);
assert_eq!(
result.fold_into(),
&[id(0), id(0), id(0), id(0), id(4), id(5)]
);
}
#[test]
fn distinguishes_by_target_class() {
let code = b"\xe8\0\0\0\0\xc3";
let sections = vec![
section(code),
section(code),
section(b"\x01"),
section(b"\x02"),
];
let relocs = [(0, call(2)), (1, call(3))];
let result = run(sections, &relocs, IcfMode::All);
assert!(!result.is_folded(id(1)));
assert_eq!(result.num_folded(), 0);
}
#[test]
fn safe_mode_respects_address_significance() {
let code = b"\xc3";
let mut significant = section(code);
significant.address_significant = true;
let sections = vec![section(code), significant, section(code)];
assert_eq!(
run(sections.clone(), &[], IcfMode::Safe).fold_into(),
&[id(0), id(1), id(0)]
);
assert_eq!(
run(sections, &[], IcfMode::All).fold_into(),
&[id(0), id(0), id(0)]
);
}
#[test]
fn keys_and_non_foldable_sections_block_folding() {
let code = b"\xc3";
let mut other_key = section(code);
other_key.key = 2;
let mut pinned = section(code);
pinned.foldable = false;
let result = run(vec![pinned, section(code), other_key], &[], IcfMode::All);
assert_eq!(result.num_folded(), 0);
}
#[test]
fn rejects_bad_input() {
let mut builder = CsrBuilder::new(1);
builder.push(0, call(3));
assert!(IcfInput::new(vec![section(b"")], builder.build().unwrap()).is_err());
assert!(IcfInput::new(vec![], Csr::empty(1)).is_err());
}
}