vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! OOM injection harness for conformance entry points.

/// Allocator wrapper used by the OOM injection harness.
#[cfg(feature = "oom-injection")]
pub mod alloc;

#[cfg(feature = "oom-injection")]
use std::sync::{Mutex, OnceLock};

/// Result of one allocation probe.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProbeReport {
    /// Allocation index configured to fail. Zero means count-only.
    pub fail_at: usize,
    /// Number of allocations reached before the entry point returned or failed.
    pub allocations: usize,
    /// Probe result.
    pub outcome: ProbeOutcome,
}

/// Outcome of one allocation probe.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProbeOutcome {
    /// Entry point returned normally.
    Returned,
    /// The configured allocation failed and was caught by the harness.
    OomInjected,
    /// Entry point panicked for a reason other than the allocator sentinel.
    Panicked(String),
}

/// Run `entry` with count-only allocation accounting.
#[cfg(feature = "oom-injection")]
#[inline]
pub fn count_allocations<R>(entry: impl FnOnce() -> R) -> ProbeReport {
    probe(0, entry)
}

/// Run `entry` while failing allocation `fail_at`.
#[cfg(feature = "oom-injection")]
#[inline]
pub fn probe<R>(fail_at: usize, entry: impl FnOnce() -> R) -> ProbeReport {
    use std::panic::{catch_unwind, take_hook, AssertUnwindSafe};

    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    let lock = LOCK.get_or_init(|| Mutex::new(()));
    let guard = match lock.lock() {
        Ok(guard) => guard,
        Err(poisoned) => poisoned.into_inner(),
    };

    let old_hook = take_hook();
    std::panic::set_hook(Box::new(|_| {}));

    alloc::arm_thread(fail_at);
    let result = catch_unwind(AssertUnwindSafe(entry));
    alloc::disarm_thread();
    let allocations = alloc::allocation_count();
    let triggered = alloc::triggered_at();
    alloc::clear_thread();

    std::panic::set_hook(old_hook);
    drop(guard);

    let outcome = match result {
        Ok(_) => ProbeOutcome::Returned,
        Err(payload) if alloc::is_oom_payload(payload.as_ref()) || triggered == fail_at => {
            ProbeOutcome::OomInjected
        }
        Err(payload) => ProbeOutcome::Panicked(panic_payload_message(payload.as_ref())),
    };

    ProbeReport {
        fail_at,
        allocations,
        outcome,
    }
}

/// Run `entry` with count-only allocation accounting.
#[cfg(not(feature = "oom-injection"))]
#[inline]
pub fn count_allocations<R>(_entry: impl FnOnce() -> R) -> ProbeReport {
    disabled_report(0)
}

/// Run `entry` while failing allocation `fail_at`.
#[cfg(not(feature = "oom-injection"))]
#[inline]
pub fn probe<R>(fail_at: usize, _entry: impl FnOnce() -> R) -> ProbeReport {
    disabled_report(fail_at)
}

#[cfg(not(feature = "oom-injection"))]
fn disabled_report(fail_at: usize) -> ProbeReport {
    ProbeReport {
        fail_at,
        allocations: 0,
        outcome: ProbeOutcome::Panicked(
            "oom-injection feature not enabled. Fix: enable the oom-injection feature.".to_string(),
        ),
    }
}

fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
    if let Some(message) = payload.downcast_ref::<&'static str>() {
        (*message).to_string()
    } else if let Some(message) = payload.downcast_ref::<String>() {
        message.clone()
    } else {
        "non-string panic payload".to_string()
    }
}