use std::panic::{AssertUnwindSafe, catch_unwind};
use crate::{
analysis::{SsaVerifier, VerifyLevel},
ir::function::SsaFunction,
target::Target,
};
const fn post_group_verify_level() -> VerifyLevel {
if cfg!(debug_assertions) {
VerifyLevel::Standard
} else {
VerifyLevel::Quick
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupOutcome {
Unchanged,
Applied,
RolledBack,
}
impl GroupOutcome {
#[must_use]
pub fn changed(self) -> bool {
matches!(self, Self::Applied)
}
}
#[derive(Debug)]
pub struct PassTransaction<T: Target> {
snapshot: SsaFunction<T>,
}
impl<T: Target> Default for PassTransaction<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Target> PassTransaction<T> {
#[must_use]
pub fn new() -> Self {
Self {
snapshot: SsaFunction::with_capacity(0, 0, 0, 0),
}
}
#[inline]
pub fn run_group<F>(&mut self, ssa: &mut SsaFunction<T>, group: F) -> GroupOutcome
where
F: FnOnce(&mut SsaFunction<T>) -> bool,
{
self.snapshot.clone_from(ssa);
let outcome = catch_unwind(AssertUnwindSafe(|| group(ssa)));
match outcome {
Ok(changed) => {
if !changed {
return GroupOutcome::Unchanged;
}
if SsaVerifier::new(ssa)
.verify(post_group_verify_level())
.is_empty()
{
return GroupOutcome::Applied;
}
log::warn!("pass group left the SSA verifier-invalid; rolling back");
}
Err(payload) => {
let detail = payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".to_string());
log::warn!("pass group panicked ({detail}); rolling back");
}
}
std::mem::swap(ssa, &mut self.snapshot);
GroupOutcome::RolledBack
}
#[inline]
pub fn run_one<F>(&mut self, ssa: &mut SsaFunction<T>, pass: F) -> GroupOutcome
where
F: FnOnce(&mut SsaFunction<T>) -> bool,
{
self.run_group(ssa, pass)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ir::{instruction::SsaInstruction, ops::SsaOp},
testing::{self, MockTarget},
};
fn corrupt(ssa: &mut SsaFunction<MockTarget>) -> bool {
if let Some(block) = ssa.block_mut(0) {
block.add_instruction(SsaInstruction::new((), SsaOp::Nop));
}
true
}
#[test]
fn an_unchanged_group_is_accepted_without_verifying() {
let mut ssa = testing::const_i32_return(7);
let before = ssa.block_count();
let mut transaction = PassTransaction::<MockTarget>::new();
let outcome = transaction.run_group(&mut ssa, |_| false);
assert_eq!(outcome, GroupOutcome::Unchanged);
assert!(!outcome.changed());
assert_eq!(ssa.block_count(), before);
assert_eq!(ssa.validate(), Ok(()));
}
#[test]
fn a_valid_group_is_applied() {
let mut ssa = testing::const_i32_return(7);
let mut transaction = PassTransaction::<MockTarget>::new();
let outcome = transaction.run_group(&mut ssa, |_| true);
assert_eq!(outcome, GroupOutcome::Applied);
assert!(outcome.changed());
assert_eq!(ssa.validate(), Ok(()));
}
#[test]
fn an_invalid_group_is_rolled_back() {
let mut ssa = testing::const_i32_return(7);
let original_blocks = ssa.block_count();
let mut transaction = PassTransaction::<MockTarget>::new();
let outcome = transaction.run_group(&mut ssa, corrupt);
assert_eq!(outcome, GroupOutcome::RolledBack);
assert!(!outcome.changed());
assert_eq!(
ssa.block_count(),
original_blocks,
"the pre-group IR was restored"
);
assert_eq!(ssa.validate(), Ok(()), "and it is still valid");
}
#[test]
fn a_panicking_group_is_rolled_back() {
let mut ssa = testing::const_i32_return(7);
let original_blocks = ssa.block_count();
let mut transaction = PassTransaction::<MockTarget>::new();
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let outcome = transaction.run_group(&mut ssa, |_| {
panic!("pass exploded");
});
std::panic::set_hook(hook);
assert_eq!(outcome, GroupOutcome::RolledBack);
assert_eq!(ssa.block_count(), original_blocks);
assert_eq!(ssa.validate(), Ok(()));
}
#[test]
fn the_buffer_is_reusable_across_functions() {
let mut transaction = PassTransaction::<MockTarget>::new();
let mut small = testing::const_i32_return(1);
assert_eq!(
transaction.run_group(&mut small, |_| false),
GroupOutcome::Unchanged
);
let mut larger = testing::diamond_phi_fixture();
let blocks = larger.block_count();
let outcome = transaction.run_group(&mut larger, corrupt);
assert_eq!(outcome, GroupOutcome::RolledBack);
assert_eq!(larger.block_count(), blocks, "restored after reuse");
assert_eq!(larger.validate(), Ok(()));
}
}