Simple Model Checking for Finite State Machines
Model checking is a powerful technique for verifying that a system (often represented as a finite state machine) satisfies a given property. This challenge asks you to implement a basic model checker in Rust that determines if a given finite state machine (FSM) satisfies a simple LTL (Linear Temporal Logic) property: "Always (G) - Globally, a certain condition holds true in every state reachable from the initial state." This is a foundational concept in formal verification and is useful for ensuring the correctness of systems like protocols, hardware designs, and software.
Problem Description
You are tasked with creating a model checker that verifies whether a given FSM satisfies the "Always" (G) LTL property. The FSM will be represented as an adjacency list where each key is a state and the value is a vector of tuples. Each tuple represents a transition: (next_state, input_symbol). The property to be checked will be a function that takes a state as input and returns a boolean indicating whether the condition holds true in that state.
What needs to be achieved:
- FSM Representation: The FSM is provided as a
HashMap<State, Vec<(State, InputSymbol)>>.StateandInputSymbolare enums defined below. - Property Function: A function
fn property(state: State) -> booldefines the property to be checked. - Model Checking Algorithm: Implement a breadth-first search (BFS) algorithm to explore all reachable states from the initial state.
- Verification: For each reachable state, apply the
propertyfunction. If the property ever fails (returnsfalse), the model checker should immediately returnfalse. If the BFS completes without finding a state where the property fails, the model checker should returntrue.
Key Requirements:
- The model checker must handle cycles in the FSM. BFS is suitable for this.
- The model checker must correctly identify whether the property holds true for all reachable states.
- The model checker should be efficient enough to handle reasonably sized FSMs.
Expected Behavior:
The function check_fsm_property should take the FSM (as a HashMap), the initial state, and the property function as input. It should return true if the property holds for all reachable states, and false otherwise.
Edge Cases to Consider:
- Empty FSM: What should happen if the FSM is empty (no states)?
- Unreachable States: States that are not reachable from the initial state should not affect the result.
- Cycles: The BFS must terminate even if the FSM contains cycles.
- Initial State Not Satisfying Property: The property might not hold true in the initial state.
Examples
Example 1:
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum State {
S1,
S2,
S3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum InputSymbol {
A,
B,
}
fn check_fsm_property(
fsm: &HashMap<State, Vec<(State, InputSymbol)>>,
initial_state: State,
property: fn(State) -> bool,
) -> bool {
// Implementation goes here
}
fn main() {
let mut fsm: HashMap<State, Vec<(State, InputSymbol)>> = HashMap::new();
fsm.insert(State::S1, vec![(State::S2, InputSymbol::A), (State::S3, InputSymbol::B)]);
fsm.insert(State::S2, vec![(State::S1, InputSymbol::B)]);
fsm.insert(State::S3, vec![(State::S2, InputSymbol::A)]);
let property = |state: State| {
match state {
State::S1 => true,
State::S2 => false, // Property fails in S2
State::S3 => true,
}
};
let result = check_fsm_property(&fsm, State::S1, property);
println!("Result: {}", result); // Expected Output: Result: false
}
Explanation: The FSM has a cycle. The property fails in state S2, which is reachable from the initial state S1. Therefore, the model checker should return false.
Example 2:
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum State {
S1,
S2,
S3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum InputSymbol {
A,
B,
}
fn check_fsm_property(
fsm: &HashMap<State, Vec<(State, InputSymbol)>>,
initial_state: State,
property: fn(State) -> bool,
) -> bool {
// Implementation goes here
}
fn main() {
let mut fsm: HashMap<State, Vec<(State, InputSymbol)>> = HashMap::new();
fsm.insert(State::S1, vec![(State::S2, InputSymbol::A)]);
fsm.insert(State::S2, vec![(State::S3, InputSymbol::B)]);
fsm.insert(State::S3, vec![(State::S1, InputSymbol::A)]);
let property = |state: State| {
match state {
State::S1 => true,
State::S2 => true,
State::S3 => true,
}
};
let result = check_fsm_property(&fsm, State::S1, property);
println!("Result: {}", result); // Expected Output: Result: true
}
Explanation: The property holds true in all reachable states (S1, S2, and S3). Therefore, the model checker should return true.
Constraints
- FSM Size: The number of states in the FSM will be at most 100.
- Input Symbols: The number of distinct input symbols will be at most 5.
- Property Function: The property function must be computationally inexpensive (e.g., no complex calculations).
- Memory Usage: The memory usage should be reasonable for the given FSM size.
- Time Complexity: The algorithm should have a time complexity of O(V + E), where V is the number of states and E is the number of transitions.
Notes
- Use a
VecDequefor the BFS queue. - Use a
HashSetto keep track of visited states to prevent infinite loops in cyclic FSMs. - The
StateandInputSymbolenums are provided for clarity. You can modify them if needed, but ensure the function signatures remain consistent. - Focus on correctness and clarity of the code. While performance is a consideration, prioritize a working solution first.
- Consider how to handle the case where the initial state is not present in the FSM. Return
truein this case, as there are no reachable states to violate the property. - The provided examples are meant to guide you, but you should test your implementation with a variety of FSMs and properties.