vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Adapter from [`Mutation`] (the catalog enum) to
//! [`AppliedMutation`] (the harness trait).
//!
//! The H6 mutation gate in [`crate::verify::harnesses::mutation`] operates on a
//! slice of `&dyn AppliedMutation`. The catalog in [`crate::adversarial::mutations`]
//! is an enum. The two were intentionally decoupled so the gate could
//! be developed independently of the catalog shape — this module is
//! the wire that joins them.
//!
//! Usage from the H6 caller:
//!
//! ```ignore
//! use vyre_conform::harnesses::mutation::mutation_probe;
//! use vyre_conform::mutations::applied::applied_for_classes;
//! use vyre_conform::spec::MutationClass;
//!
//! let applied = applied_for_classes(&[MutationClass::ArithmeticMutations]);
//! let refs: Vec<&dyn _> = applied.iter().map(|b| &**b).collect();
//! let report = mutation_probe(&path, "test_name", &refs, &[], &[]);
//! ```

use crate::adversarial::mutations::catalog::{
    apply, class_of, Mutation, MutationClass, MUTATION_CATALOG,
};
use crate::spec::types::MutationClass as SpecMutationClass;
use crate::verify::harnesses::mutation::{AppliedMutation, MutationApplyError};

/// Boxed [`AppliedMutation`] holding a single catalog [`Mutation`].
///
/// Stores the mutation by value plus a stable id/description string so
/// the trait methods can return `&str` borrows.
pub struct CatalogMutation {
    /// The catalog mutation this adapter wraps.
    mutation: Mutation,
    /// Pre-rendered short id used by reports.
    id: String,
    /// Pre-rendered human description used by failure feedback.
    description: String,
}

impl CatalogMutation {
    /// Wrap a catalog mutation. The id and description are computed
    /// once at construction so the trait methods are O(1) borrows.
    #[must_use]
    #[inline]
    pub fn new(mutation: Mutation) -> Self {
        let description = describe(&mutation);
        let id = identify(&mutation);
        Self {
            mutation,
            id,
            description,
        }
    }
}

impl AppliedMutation for CatalogMutation {
    fn id(&self) -> &str {
        &self.id
    }

    fn description(&self) -> &str {
        &self.description
    }

    fn class(&self) -> SpecMutationClass {
        // The catalog uses `crate::adversarial::mutations::catalog::MutationClass` which is a
        // re-export of the spec-level type, so this is a free conversion.
        let catalog_class: MutationClass = class_of(&self.mutation);
        catalog_class
    }

    fn apply(&self, source: &str) -> Result<String, MutationApplyError> {
        match apply(source, &self.mutation) {
            Ok(mutated) => Ok(mutated),
            Err(err) => Err(MutationApplyError::NotApplicable {
                reason: err.to_string(),
            }),
        }
    }

    fn hint(&self) -> String {
        format!(
            "test passed when I applied `{}`. Add an assertion that distinguishes \
             the original from the mutated behaviour. The catalog mutation id is `{}` \
             so you can reproduce locally with: \
             cargo run --bin mutation_probe -- --mutation {} --test <name>.",
            self.description, self.id, self.id,
        )
    }
}

/// Build a vector of boxed adapters covering every catalog mutation
/// whose class is in `classes`. An empty `classes` slice returns
/// adapters for the entire catalog.
///
/// The H6 caller borrows `&dyn AppliedMutation` from each `Box`:
///
/// ```ignore
/// let applied = applied_for_classes(&[]);
/// let refs: Vec<&dyn _> = applied.iter().map(|b| b.as_ref() as _).collect();
/// ```
#[must_use]
#[inline]
pub fn applied_for_classes(classes: &[SpecMutationClass]) -> Vec<Box<dyn AppliedMutation>> {
    let mut out: Vec<Box<dyn AppliedMutation>> = Vec::new();
    for mutation in MUTATION_CATALOG {
        if !classes.is_empty() && !classes.contains(&class_of(mutation)) {
            continue;
        }
        out.push(Box::new(CatalogMutation::new(mutation.clone())));
    }
    out
}

/// Convert a single catalog mutation into a boxed adapter for ad-hoc
/// callers (single-mutation probes, gauntlet tests, the canary path).
#[must_use]
#[inline]
pub fn applied_for(mutation: Mutation) -> Box<dyn AppliedMutation> {
    Box::new(CatalogMutation::new(mutation))
}

fn identify(mutation: &Mutation) -> String {
    // Stable, kebab-shaped identifiers. The Debug formatter is used as
    // a discriminator suffix so two `ArithOpSwap { Add → Sub }` and
    // `ArithOpSwap { Mul → Add }` get distinct ids.
    let raw = format!("{mutation:?}");
    raw.chars()
        .map(|c| match c {
            'A'..='Z' | 'a'..='z' | '0'..='9' => c.to_ascii_lowercase(),
            _ => '_',
        })
        .collect::<String>()
        .trim_matches('_')
        .to_string()
}

fn describe(mutation: &Mutation) -> String {
    format!("{mutation:?}")
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::{
        applied_for, applied_for_classes, CatalogMutation, Mutation, SpecMutationClass,
        MUTATION_CATALOG,
    };

    #[test]
    fn applied_for_classes_returns_full_catalog_for_empty_filter() {
        let applied = applied_for_classes(&[]);
        assert_eq!(applied.len(), MUTATION_CATALOG.len());
    }

    #[test]
    fn applied_for_classes_filters_to_one_class() {
        let applied = applied_for_classes(&[SpecMutationClass::ArithmeticMutations]);
        assert!(!applied.is_empty(), "arithmetic class should have entries");
        for adapter in &applied {
            assert_eq!(adapter.class(), SpecMutationClass::ArithmeticMutations);
        }
    }

    #[test]
    fn arith_op_swap_adapter_applies_to_a_known_source() {
        let applied = applied_for(Mutation::ArithOpSwap {
            from: super::super::BinOpKind::Add,
            to: super::super::BinOpKind::Sub,
        });
        let source = "fn add(a: u32, b: u32) -> u32 { a + b }";
        let mutated = applied.apply(source).expect("apply should succeed");
        assert!(
            mutated.contains("a - b"),
            "expected subtraction in mutated source"
        );
    }

    #[test]
    fn id_is_stable_across_calls() {
        let m = Mutation::ConstantZeroToOne;
        let a = CatalogMutation::new(m.clone());
        let b = CatalogMutation::new(m);
        assert_eq!(
            a.id(),
            b.id(),
            "id() must be deterministic for the same mutation"
        );
    }
}