//! Stateright model-checking of death notifications (G6) and orphan cleanup (G7). //! //! Two focused models keep the state space tractable: //! //! 1. **MonitorModel** (G6): 3 actors with monitor/demonitor/kill. Proves //! notification exactness, no-notification-for-alive, demonitor suppression. //! //! 2. **OrphanModel** (G7): 4 actors with parent-child/kill/orphan-cleanup. //! Proves unsupervised children stop, supervised survive, cascading cleanup. use super::model_checker::{Model, Property}; // ═══════════════════════════════════════════════════════════════════════════ // G6: Monitor Model // ═══════════════════════════════════════════════════════════════════════════ const MON_N: usize = 3; const MON_PAIRS: usize = MON_N * (MON_N - 1); // 6 fn mon_pair(i: usize, j: usize) -> usize { debug_assert!(i < MON_N && j < MON_N && i != j); if j < i { i * (MON_N - 1) + j } else { i * (MON_N - 1) + j - 1 } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] struct MonitorState { alive: [bool; MON_N], /// Active monitor from i to j. active: [bool; MON_PAIRS], /// Demonitored while target was still alive (the meaningful demonitor case). deactivated_while_alive: [bool; MON_PAIRS], /// Notification count (capped at 2 to detect duplicates). notif: [u8; MON_PAIRS], } impl MonitorState { fn init() -> Self { Self { alive: [true; MON_N], active: [false; MON_PAIRS], deactivated_while_alive: [false; MON_PAIRS], notif: [0; MON_PAIRS], } } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] enum MonAction { Monitor(usize, usize), Demonitor(usize, usize), Kill(usize), } #[derive(Clone)] struct MonitorModel; impl Model for MonitorModel { type State = MonitorState; type Action = MonAction; fn init_states(&self) -> Vec { vec![MonitorState::init()] } fn actions(&self, s: &Self::State, actions: &mut Vec) { for i in 0..MON_N { if !s.alive[i] { continue; } for j in 0..MON_N { if i == j { continue; } let idx = mon_pair(i, j); if s.alive[j] && !s.active[idx] { actions.push(MonAction::Monitor(i, j)); } if s.active[idx] { actions.push(MonAction::Demonitor(i, j)); } } actions.push(MonAction::Kill(i)); } } fn next_state(&self, s: &Self::State, action: Self::Action) -> Option { let mut n = s.clone(); match action { MonAction::Monitor(w, t) => { let idx = mon_pair(w, t); n.active[idx] = true; n.deactivated_while_alive[idx] = false; } MonAction::Demonitor(w, t) => { let idx = mon_pair(w, t); n.active[idx] = false; if n.alive[t] { n.deactivated_while_alive[idx] = true; } } MonAction::Kill(t) => { if !n.alive[t] { return None; } n.alive[t] = false; for w in 0..MON_N { if w == t { continue; } let idx = mon_pair(w, t); if n.alive[w] && n.active[idx] && n.notif[idx] < 2 { n.notif[idx] += 1; } } } } if n == *s { None } else { Some(n) } } fn properties(&self) -> Vec> { vec![ Property::::always("G6a: exactly one notification per active monitor on dead target", |_, s| { for w in 0..MON_N { if !s.alive[w] { continue; } for t in 0..MON_N { if w == t { continue; } let idx = mon_pair(w, t); if s.active[idx] && !s.alive[t] && s.notif[idx] != 1 { return false; } } } true }), Property::::always("G6b: no notification for alive targets", |_, s| { for w in 0..MON_N { for t in 0..MON_N { if w == t { continue; } if s.alive[t] && s.notif[mon_pair(w, t)] > 0 { return false; } } } true }), Property::::always("G6c: demonitor before death suppresses notification", |_, s| { for w in 0..MON_N { for t in 0..MON_N { if w == t { continue; } let idx = mon_pair(w, t); // If demonitored while target was alive, no notification should exist if s.deactivated_while_alive[idx] && s.notif[idx] > 0 { return false; } } } true }), // Liveness Property::::sometimes("L1: monitor fires", |_, s| { s.notif.iter().any(|&c| c > 0) }), Property::::sometimes("L2: demonitor suppression reachable", |_, s| { (0..MON_N).any(|w| (0..MON_N).any(|t| { w != t && { let idx = mon_pair(w, t); s.deactivated_while_alive[idx] && !s.alive[t] && s.notif[idx] == 0 } })) }), Property::::sometimes("L3: multiple monitors on same target", |_, s| { (0..MON_N).any(|t| { let watchers: usize = (0..MON_N) .filter(|&w| w != t && s.notif[mon_pair(w, t)] > 0) .count(); watchers >= 2 }) }), ] } } // ═══════════════════════════════════════════════════════════════════════════ // G7: Orphan Model // ═══════════════════════════════════════════════════════════════════════════ /// 4 actors: enough for parent → child → grandchild chains (3 deep) plus /// a sibling to test supervised vs unsupervised. const ORP_N: usize = 4; const ORP_PAIRS: usize = ORP_N * (ORP_N - 1); // 12 fn orp_pair(i: usize, j: usize) -> usize { debug_assert!(i < ORP_N && j < ORP_N && i != j); if j < i { i * (ORP_N - 1) + j } else { i * (ORP_N - 1) + j - 1 } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] struct OrphanState { alive: [bool; ORP_N], /// Unsupervised parent→child. parent_unsup: [bool; ORP_PAIRS], /// Supervised parent→child. parent_sup: [bool; ORP_PAIRS], /// Orphan cleanup propagated for actor i. orphan_cleaned: [bool; ORP_N], /// Whether actor was killed by OrphanCleanup (not by explicit Kill). orphan_killed: [bool; ORP_N], /// How many parent-child links have been set (cap to limit state space). link_count: u8, } /// Max parent-child links to prevent state explosion. const MAX_LINKS: u8 = 3; impl OrphanState { fn init() -> Self { Self { alive: [true; ORP_N], parent_unsup: [false; ORP_PAIRS], parent_sup: [false; ORP_PAIRS], orphan_cleaned: [false; ORP_N], orphan_killed: [false; ORP_N], link_count: 0, } } fn has_parent(&self, c: usize) -> bool { (0..ORP_N).any(|p| p != c && { let idx = orp_pair(p, c); self.parent_unsup[idx] || self.parent_sup[idx] }) } fn has_children(&self, p: usize) -> bool { (0..ORP_N).any(|c| c != p && { let idx = orp_pair(p, c); self.parent_unsup[idx] || self.parent_sup[idx] }) } } #[derive(Clone, Debug, Hash, PartialEq, Eq)] enum OrpAction { SetParentUnsup(usize, usize), SetParentSup(usize, usize), Kill(usize), OrphanCleanup(usize), } #[derive(Clone)] struct OrphanModel; impl Model for OrphanModel { type State = OrphanState; type Action = OrpAction; fn init_states(&self) -> Vec { vec![OrphanState::init()] } fn actions(&self, s: &Self::State, actions: &mut Vec) { // SetParent actions only if under link cap if s.link_count < MAX_LINKS { for p in 0..ORP_N { if !s.alive[p] { continue; } for c in 0..ORP_N { if p == c || !s.alive[c] { continue; } if s.has_parent(c) { continue; } // Prevent cycles: c must not be an ancestor of p if is_ancestor(s, c, p) { continue; } actions.push(OrpAction::SetParentUnsup(p, c)); actions.push(OrpAction::SetParentSup(p, c)); } } } for i in 0..ORP_N { if s.alive[i] { actions.push(OrpAction::Kill(i)); } if !s.alive[i] && !s.orphan_cleaned[i] && s.has_children(i) { actions.push(OrpAction::OrphanCleanup(i)); } } } fn next_state(&self, s: &Self::State, action: Self::Action) -> Option { let mut n = s.clone(); match action { OrpAction::SetParentUnsup(p, c) => { n.parent_unsup[orp_pair(p, c)] = true; n.link_count += 1; } OrpAction::SetParentSup(p, c) => { n.parent_sup[orp_pair(p, c)] = true; n.link_count += 1; } OrpAction::Kill(i) => { if !n.alive[i] { return None; } n.alive[i] = false; } OrpAction::OrphanCleanup(dead_parent) => { n.orphan_cleaned[dead_parent] = true; // Kill unsupervised children for c in 0..ORP_N { if c == dead_parent { continue; } if n.parent_unsup[orp_pair(dead_parent, c)] && n.alive[c] { n.alive[c] = false; n.orphan_killed[c] = true; } } } } if n == *s { None } else { Some(n) } } fn properties(&self) -> Vec> { vec![ // G7a: After orphan cleanup, all unsupervised children are dead Property::::always("G7a: orphan cleanup stops unsupervised children", |_, s| { for p in 0..ORP_N { if !s.alive[p] && s.orphan_cleaned[p] { for c in 0..ORP_N { if c == p { continue; } if s.parent_unsup[orp_pair(p, c)] && s.alive[c] { return false; } } } } true }), // G7b: OrphanCleanup never kills supervised-only children. // If orphan_killed[c] is true, there must be a parent p with an // unsupervised link (parent_unsup[p→c]) that was orphan-cleaned. // A supervised-only child has no parent_unsup link, so orphan_killed // being true for it would violate this property. Property::::always("G7b: supervised children survive orphan cleanup", |_, s| { for c in 0..ORP_N { if s.orphan_killed[c] { // There must exist a dead, cleaned parent with unsup link to c let has_unsup_cleaned_parent = (0..ORP_N).any(|p| { p != c && s.parent_unsup[orp_pair(p, c)] && s.orphan_cleaned[p] }); if !has_unsup_cleaned_parent { return false; } } } true }), // G7c: Cascading — if orphan-cleaned parent's child also died and was // orphan-cleaned, its unsupervised children are dead too Property::::always("G7c: cascading orphan cleanup", |_, s| { for p in 0..ORP_N { if s.orphan_cleaned[p] { for c in 0..ORP_N { if c == p { continue; } if s.parent_unsup[orp_pair(p, c)] && !s.alive[c] && s.orphan_cleaned[c] { for gc in 0..ORP_N { if gc == c { continue; } if s.parent_unsup[orp_pair(c, gc)] && s.alive[gc] { return false; } } } } } } true }), // Liveness Property::::sometimes("L1: orphan cleanup triggers", |_, s| { s.orphan_cleaned.iter().any(|&c| c) }), Property::::sometimes("L2: cascading cleanup reachable", |_, s| { // Parent cleaned → child died → child cleaned (0..ORP_N).any(|p| s.orphan_cleaned[p] && (0..ORP_N).any(|c| { c != p && s.parent_unsup[orp_pair(p, c)] && !s.alive[c] && s.orphan_cleaned[c] })) }), Property::::sometimes("L3: supervised child survives cleanup", |_, s| { (0..ORP_N).any(|p| s.orphan_cleaned[p] && (0..ORP_N).any(|c| { c != p && s.parent_sup[orp_pair(p, c)] && s.alive[c] })) }), ] } } /// Check if `ancestor` is an ancestor of `descendant` via parent links. fn is_ancestor(s: &OrphanState, ancestor: usize, descendant: usize) -> bool { // Walk up from descendant let mut current = descendant; for _ in 0..ORP_N { let parent = (0..ORP_N).find(|&p| { p != current && { let idx = orp_pair(p, current); s.parent_unsup[idx] || s.parent_sup[idx] } }); match parent { Some(p) if p == ancestor => return true, Some(p) => current = p, None => return false, } } false } // ── Tests ──────────────────────────────────────────────────────────────────── #[test] #[ignore] // Exhaustive proof — run deliberately with `cargo test -- --ignored` fn g6_monitor_model_check() { let result = MonitorModel.checker().spawn_dfs().join(); let unique = result.unique_state_count(); let depth = result.max_depth(); println!("Stateright G6 (Monitor): {} unique states, max depth {}", unique, depth); result.assert_properties(); assert!(unique > 100, "Too few states ({unique})"); } #[test] #[ignore] // Exhaustive proof — run deliberately with `cargo test -- --ignored` fn g7_orphan_model_check() { let result = OrphanModel.checker().spawn_dfs().join(); let unique = result.unique_state_count(); let depth = result.max_depth(); println!("Stateright G7 (Orphan): {} unique states, max depth {}", unique, depth); result.assert_properties(); assert!(unique > 100, "Too few states ({unique})"); }