use std::collections::{HashMap, HashSet, VecDeque};
use crate::error::{Result, ScxmlError};
use crate::model::{State, StateKind, Statechart};
pub fn validate_liveness(chart: &Statechart, state_count: usize) -> Result<()> {
let index = crate::index::StateIndex::new(chart);
validate_liveness_with_index(chart, state_count, &index)
}
pub fn validate_liveness_with_index(
chart: &Statechart,
state_count: usize,
index: &crate::index::StateIndex<'_>,
) -> Result<()> {
check_reachability(chart, state_count, index.state_map())?;
check_deadlocks(chart, state_count)?;
Ok(())
}
fn check_reachability(
chart: &Statechart,
state_count: usize,
state_index: &HashMap<&str, &State>,
) -> Result<()> {
let mut reachable: HashSet<&str> = HashSet::with_capacity(state_count);
let mut queue: VecDeque<&str> = VecDeque::with_capacity(state_count);
queue.push_back(chart.initial.as_str());
while let Some(id) = queue.pop_front() {
if !reachable.insert(id) {
continue;
}
if let Some(&state) = state_index.get(id) {
for t in &state.transitions {
for target in &t.targets {
queue.push_back(target.as_str());
}
}
match state.kind {
StateKind::Compound => {
if let Some(ref init) = state.initial {
queue.push_back(init.as_str());
} else if let Some(first) = state.children.first() {
queue.push_back(first.id.as_str());
}
}
StateKind::Parallel => {
for child in &state.children {
queue.push_back(child.id.as_str());
}
}
_ => {}
}
for child in &state.children {
if child.kind == StateKind::Compound || child.kind == StateKind::Parallel {
queue.push_back(child.id.as_str());
}
}
}
}
for state in chart.iter_all_states() {
if matches!(state.kind, StateKind::History(_)) {
continue;
}
if !reachable.contains(state.id.as_str()) {
return Err(ScxmlError::Unreachable(state.id.to_string()));
}
}
Ok(())
}
fn check_deadlocks(chart: &Statechart, state_count: usize) -> Result<()> {
let mut parent_transitions: HashMap<&str, bool> = HashMap::with_capacity(state_count);
let limit = crate::max_depth();
build_parent_has_transitions(&chart.states, &mut parent_transitions, false, 0, limit);
for state in chart.iter_all_states() {
match state.kind {
StateKind::Final
| StateKind::History(_)
| StateKind::Compound
| StateKind::Parallel => {}
StateKind::Atomic => {
if state.transitions.is_empty() {
let inherited = parent_transitions
.get(state.id.as_str())
.copied()
.unwrap_or(false);
if !inherited {
return Err(ScxmlError::Deadlock(state.id.to_string()));
}
}
}
}
}
Ok(())
}
fn build_parent_has_transitions<'a>(
states: &'a [crate::model::State],
map: &mut HashMap<&'a str, bool>,
ancestor_has_transitions: bool,
depth: usize,
limit: usize,
) {
if depth > limit {
return;
}
for state in states {
let this_has = ancestor_has_transitions || !state.transitions.is_empty();
for child in &state.children {
map.insert(child.id.as_str(), this_has);
}
if !state.children.is_empty() {
build_parent_has_transitions(&state.children, map, this_has, depth + 1, limit);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{State, Transition};
#[test]
fn all_reachable() {
let chart = Statechart::new(
"a",
vec![
{
let mut s = State::atomic("a");
s.transitions.push(Transition::new("go", "b"));
s
},
{
let mut s = State::atomic("b");
s.transitions.push(Transition::new("done", "end"));
s
},
State::final_state("end"),
],
);
assert!(validate_liveness(&chart, chart.iter_all_states().count()).is_ok());
}
#[test]
fn unreachable_state() {
let chart = Statechart::new(
"a",
vec![
{
let mut s = State::atomic("a");
s.transitions.push(Transition::new("done", "end"));
s
},
State::atomic("orphan"), State::final_state("end"),
],
);
let err = validate_liveness(&chart, chart.iter_all_states().count());
assert!(err.is_err());
}
#[test]
fn deadlock_detected() {
let chart = Statechart::new(
"a",
vec![
{
let mut s = State::atomic("a");
s.transitions.push(Transition::new("go", "b"));
s
},
State::atomic("b"), ],
);
let err = validate_liveness(&chart, chart.iter_all_states().count()).unwrap_err();
assert!(err.to_string().contains("deadlock") || err.to_string().contains("no outgoing"));
}
#[test]
fn child_with_parent_transition_not_deadlock() {
let chart = Statechart::new(
"wrapper",
vec![
{
let mut wrapper = State::compound(
"wrapper",
"child",
vec![
State::atomic("child"), ],
);
wrapper.transitions.push(Transition::new("escape", "done"));
wrapper
},
State::final_state("done"),
],
);
assert!(validate_liveness(&chart, chart.iter_all_states().count()).is_ok());
}
#[test]
fn compound_children_reachable() {
let chart = Statechart::new(
"main",
vec![State::compound(
"main",
"child_a",
vec![
{
let mut s = State::atomic("child_a");
s.transitions.push(Transition::new("next", "child_b"));
s
},
{
let mut s = State::atomic("child_b");
s.transitions.push(Transition::new("done", "end"));
s
},
State::final_state("end"),
],
)],
);
assert!(validate_liveness(&chart, chart.iter_all_states().count()).is_ok());
}
}