tla-checker 0.3.9

A TLA+ model checker written in Rust
Documentation
use std::sync::Arc;

use indexmap::IndexSet;

use crate::ast::State;

#[derive(Debug, Clone)]
pub struct Edge {
    pub target: usize,
    pub action: Option<Arc<str>>,
}

pub struct StateGraph {
    pub states: IndexSet<State>,
    pub edges: Vec<Vec<Edge>>,
    pub parents: Vec<Option<usize>>,
}

impl StateGraph {
    pub fn new() -> Self {
        Self {
            states: IndexSet::new(),
            edges: Vec::new(),
            parents: Vec::new(),
        }
    }

    pub fn add_state(&mut self, state: State, parent: Option<usize>) -> (usize, bool) {
        let (idx, is_new) = self.states.insert_full(state);
        if is_new {
            self.parents.push(parent);
            self.edges.push(Vec::new());
        }
        (idx, is_new)
    }

    pub fn add_edge(&mut self, from: usize, to: usize, action: Option<Arc<str>>) {
        if from < self.edges.len() {
            self.edges[from].push(Edge { target: to, action });
        }
    }

    pub fn state_count(&self) -> usize {
        self.states.len()
    }

    pub fn edge_count(&self) -> usize {
        self.edges.iter().map(|e| e.len()).sum()
    }

    pub fn get_state(&self, idx: usize) -> Option<&State> {
        self.states.get_index(idx)
    }

    pub fn reconstruct_trace(&self, state_idx: usize) -> Vec<State> {
        let mut trace = Vec::new();
        let mut idx = Some(state_idx);
        while let Some(i) = idx {
            if let Some(state) = self.states.get_index(i) {
                trace.push(state.clone());
            }
            idx = self.parents.get(i).copied().flatten();
        }
        trace.reverse();
        trace
    }

    pub fn successors(&self, idx: usize) -> &[Edge] {
        self.edges.get(idx).map(|v| v.as_slice()).unwrap_or(&[])
    }

    pub fn outgoing_edges(&self, idx: usize) -> impl Iterator<Item = &Edge> {
        self.edges.get(idx).into_iter().flat_map(|v| v.iter())
    }

    pub fn has_edge_with_action(&self, idx: usize, action_name: &str) -> bool {
        self.outgoing_edges(idx)
            .any(|e| e.action.as_ref().is_some_and(|a| a.as_ref() == action_name))
    }
}

impl Default for StateGraph {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ast::{State, Value};

    fn state_with_x(n: i64) -> State {
        State {
            values: vec![Value::Int(n)],
        }
    }

    #[test]
    fn graph_basic_operations() {
        let mut graph = StateGraph::new();

        let s0 = state_with_x(0);
        let (idx0, is_new0) = graph.add_state(s0, None);
        assert_eq!(idx0, 0);
        assert!(is_new0);

        let s1 = state_with_x(1);
        let (idx1, is_new1) = graph.add_state(s1, Some(0));
        assert_eq!(idx1, 1);
        assert!(is_new1);

        graph.add_edge(0, 1, Some("Inc".into()));

        assert_eq!(graph.state_count(), 2);
        assert_eq!(graph.edge_count(), 1);
        assert!(graph.has_edge_with_action(0, "Inc"));
        assert!(!graph.has_edge_with_action(0, "Dec"));
    }

    #[test]
    fn trace_reconstruction() {
        let mut graph = StateGraph::new();

        graph.add_state(state_with_x(0), None);
        graph.add_state(state_with_x(1), Some(0));
        graph.add_state(state_with_x(2), Some(1));

        let trace = graph.reconstruct_trace(2);
        assert_eq!(trace.len(), 3);
    }
}