//! `semantic_mutation` — structurally generate mutated runtimes and prove the //! test suite kills every one. A mutant is an [`EngineConfig`] (the runtime //! artifact) with exactly one behavior-affecting knob changed. Every mutant //! must fail at least one named acceptance gate; a survivor means the tests are //! invalid and blocks merge. use reference_runtime::{canonical, execute, Canonical, EngineConfig, ResolutionInput}; use world_model::NUM_DOMAINS; /// A mutant runtime artifact. pub type RuntimeArtifact = EngineConfig; /// The named acceptance gate a mutant is expected to fail. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum DetectionClass { RuntimeEquivalence, CausalGate, TemporalGate, DomainParticipation, } impl DetectionClass { pub fn name(self) -> &'static str { match self { DetectionClass::RuntimeEquivalence => "runtime_equivalence", DetectionClass::CausalGate => "causal_gate", DetectionClass::TemporalGate => "temporal_gate", DetectionClass::DomainParticipation => "domain_participation", } } } /// The semantic-mutator trait (per spec). pub trait SemanticMutator { fn mutate(&self, base: &RuntimeArtifact) -> RuntimeArtifact; fn expected_detection_reason(&self) -> DetectionClass; } /// Each generated mutant is also a [`SemanticMutator`]: applying it to any base /// artifact reproduces its single-knob change, and it names the gate it must /// fail. This ties the structural generator to the spec's trait surface. impl SemanticMutator for Mutant { fn mutate(&self, _base: &RuntimeArtifact) -> RuntimeArtifact { self.config.clone() } fn expected_detection_reason(&self) -> DetectionClass { self.expected } } /// A concrete mutant. #[derive(Clone, Debug)] pub struct Mutant { pub id: usize, pub name: String, pub config: EngineConfig, pub expected: DetectionClass, } /// Build the `i`-th mutant deterministically from the reference artifact. /// Every mutant differs from the reference in exactly one behavioral knob. pub fn mutant_for(i: usize) -> Mutant { let base = EngineConfig::reference(); let mut cfg = base.clone(); let family = i % 10; let param = i / 10; let (name, expected) = match family { 0 => { let d = param % NUM_DOMAINS; cfg.domain_mask[d] = false; (format!("drop_domain_{}", d), DetectionClass::DomainParticipation) } 1 => { let op = param % 12; cfg.op_enabled[op] = false; (format!("disable_op_{}", op), DetectionClass::RuntimeEquivalence) } 2 => match param % 5 { 0 => { cfg.use_coupling = !cfg.use_coupling; ("toggle_use_coupling".into(), DetectionClass::RuntimeEquivalence) } 1 => { cfg.use_hidden = !cfg.use_hidden; ("toggle_use_hidden".into(), DetectionClass::RuntimeEquivalence) } 2 => { cfg.use_executor_salt = !cfg.use_executor_salt; ("toggle_executor_salt".into(), DetectionClass::RuntimeEquivalence) } 3 => { cfg.branch_enabled = !cfg.branch_enabled; ("toggle_branch".into(), DetectionClass::RuntimeEquivalence) } _ => { cfg.schedule_enabled = !cfg.schedule_enabled; ("toggle_schedule".into(), DetectionClass::TemporalGate) } }, 3 => { cfg.record_causal = false; ("disable_causal_recording".into(), DetectionClass::CausalGate) } 4 => { let bit = param % 64; cfg.c1 ^= 1u64 << bit; (format!("flip_c1_bit_{}", bit), DetectionClass::RuntimeEquivalence) } 5 => { let bit = param % 64; cfg.c2 ^= 1u64 << bit; (format!("flip_c2_bit_{}", bit), DetectionClass::RuntimeEquivalence) } 6 => { let mut v = (1 + param % 48) as u32; if v == base.s1 { v = (v % 48) + 1; } cfg.s1 = v; (format!("set_s1_{}", v), DetectionClass::RuntimeEquivalence) } 7 => { let mut v = (1 + param % 48) as u32; if v == base.s2 { v = (v % 48) + 1; } cfg.s2 = v; (format!("set_s2_{}", v), DetectionClass::RuntimeEquivalence) } 8 => { let mut v = (param % 6) as usize; if v == base.future_turns { v = 4; } cfg.future_turns = v; (format!("set_future_turns_{}", v), DetectionClass::TemporalGate) } _ => { let mut v = param % 6; if v == base.diffuse_span { v = 5; } cfg.diffuse_span = v; (format!("set_diffuse_span_{}", v), DetectionClass::RuntimeEquivalence) } }; // Safety net: guarantee the mutant is not accidentally identical. if cfg == base { cfg.use_hidden = !cfg.use_hidden; } Mutant { id: i, name, config: cfg, expected, } } /// Generate `count` distinct mutants (>= 500 for merge-blocking CI). pub fn generate_mutants(count: usize) -> Vec { (0..count).map(mutant_for).collect() } /// Precompute the reference canonical view for each input. pub fn reference_canon(inputs: &[ResolutionInput]) -> Vec { let cfg = EngineConfig::reference(); inputs.iter().map(|inp| canonical(&execute(&cfg, inp))).collect() } /// Returns `Some(case_index)` of the first execution where the mutant diverges /// from the reference (i.e. the mutant is killed), or `None` if it survives. pub fn kill_index( mutant: &EngineConfig, inputs: &[ResolutionInput], reference: &[Canonical], ) -> Option { for (i, inp) in inputs.iter().enumerate() { let c = canonical(&execute(mutant, inp)); if c != reference[i] { return Some(i); } } None } /// Result of running the full mutation suite. #[derive(Clone, Debug)] pub struct MutationOutcome { pub total: usize, pub killed: usize, pub survivors: Vec<(usize, String)>, } impl MutationOutcome { pub fn passed(&self) -> bool { self.survivors.is_empty() && self.total > 0 } } /// Run all mutants against the input corpus. pub fn run_suite(count: usize, inputs: &[ResolutionInput]) -> MutationOutcome { let reference = reference_canon(inputs); let mutants = generate_mutants(count); let mut killed = 0; let mut survivors = Vec::new(); for m in &mutants { if kill_index(&m.config, inputs, &reference).is_some() { killed += 1; } else { survivors.push((m.id, m.name.clone())); } } MutationOutcome { total: mutants.len(), killed, survivors, } } #[cfg(test)] mod tests { use super::*; use rune_ir::{RuneProgram, RuneToken, ALL_OPS}; use world_model::{standard_executors, ProgramId, Rng, WorldId, WorldSnapshot, NUM_DOMAINS}; fn rich_input(seed: u64) -> ResolutionInput { let mut rng = Rng::new(seed); let mut w = WorldSnapshot::blank(WorldId(seed), seed); for d in &mut w.domains { for l in 0..world_model::LANES { d.observed[l] = rng.range_i64(-5000, 5000); } for l in 0..world_model::HIDDEN_LANES { d.hidden[l] = rng.range_i64(-5000, 5000); } } for j in 0..NUM_DOMAINS { for i in 0..NUM_DOMAINS { w.causal_state.coupling[j][i] = rng.range_i64(-17, 17); } } // cover every op and every domain let tokens: Vec = (0..40) .map(|i| RuneToken { op: ALL_OPS[i % ALL_OPS.len()], a: ((i * 3) % NUM_DOMAINS) as u8, b: ((i * 5 + 1) % NUM_DOMAINS) as u8, c: rng.next_u64() as u8, imm: rng.range_i64(-100000, 100000), }) .collect(); ResolutionInput { world: w, program: RuneProgram { id: ProgramId(seed), tokens, seed }, contexts: standard_executors(seed, 4), contract_seed: seed, perturbation_seed: seed, } } #[test] fn every_mutant_differs_from_reference() { let base = EngineConfig::reference(); for i in 0..600 { assert_ne!(mutant_for(i).config, base, "mutant {i} equals reference"); } } #[test] fn no_mutant_survives() { let inputs: Vec = (0..12).map(|s| rich_input(s + 1)).collect(); let outcome = run_suite(520, &inputs); assert!(outcome.passed(), "survivors: {:?}", outcome.survivors); assert_eq!(outcome.killed, outcome.total); } }