use crate::types::{Program, TuringMachineError};
use std::collections::HashSet;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum AnalysisError {
InvalidHead(usize),
InvalidStartState(String),
StopStatesNotFound(Vec<String>),
UndefinedNextStates(Vec<String>),
UnreachableStates(Vec<String>),
InvalidTapeSymbols(Vec<char>),
}
impl From<AnalysisError> for TuringMachineError {
fn from(error: AnalysisError) -> Self {
match error {
AnalysisError::InvalidHead(pos) => {
TuringMachineError::ValidationError(format!("Invalid head position: {}", pos))
}
AnalysisError::InvalidStartState(state) => {
TuringMachineError::ValidationError(format!("Invalid start state: {}", state))
}
AnalysisError::StopStatesNotFound(states) => TuringMachineError::ValidationError(
format!("Stop states not found in transitions: {:?}", states),
),
AnalysisError::UndefinedNextStates(transitions) => TuringMachineError::ValidationError(
format!("Transitions reference undefined states: {:?}", transitions),
),
AnalysisError::UnreachableStates(states) => TuringMachineError::ValidationError(
format!("Unreachable states detected: {:?}", states),
),
AnalysisError::InvalidTapeSymbols(symbols) => {
TuringMachineError::ValidationError(format!(
"Initial tape contains symbols not handled by any transition: {:?}",
symbols
))
}
}
}
}
pub fn analyze(program: &Program) -> Result<(), Vec<AnalysisError>> {
let errors = [
check_head,
check_valid_start_state,
check_valid_stop_states,
check_undefined_next_states,
check_unreachable_states,
check_tape_symbols,
]
.iter()
.filter_map(|f| f(program).err())
.collect::<Vec<_>>();
if !errors.is_empty() {
Err(errors)
} else {
Ok(())
}
}
fn check_head(program: &Program) -> Result<(), AnalysisError> {
if program.is_single_tape() {
let head_position = program.head_position();
let tape_length = program.initial_tape().len();
if head_position >= tape_length && tape_length > 0 {
Err(AnalysisError::InvalidHead(head_position))
} else {
Ok(())
}
} else {
for (&head_pos, tape) in program.heads.iter().zip(&program.tapes) {
if head_pos >= tape.len() && !tape.is_empty() {
return Err(AnalysisError::InvalidHead(head_pos));
}
}
Ok(())
}
}
fn check_valid_start_state(program: &Program) -> Result<(), AnalysisError> {
if !program.rules.contains_key(&program.initial_state) {
return Err(AnalysisError::InvalidStartState(
program.initial_state.clone(),
));
}
Ok(())
}
fn check_valid_stop_states(program: &Program) -> Result<(), AnalysisError> {
let stop_states: HashSet<String> = program
.rules
.iter()
.filter(|(_, transitions)| transitions.is_empty())
.map(|(state, _)| state.clone())
.collect();
let next_states: HashSet<String> = program
.rules
.values()
.flat_map(|transitions| transitions.iter())
.map(|transition| transition.next_state.clone())
.collect();
let mut invalid: Vec<String> = stop_states.difference(&next_states).cloned().collect();
if !invalid.is_empty() {
invalid.sort();
return Err(AnalysisError::StopStatesNotFound(invalid));
}
Ok(())
}
fn check_undefined_next_states(program: &Program) -> Result<(), AnalysisError> {
let defined_states: HashSet<String> = program.rules.keys().cloned().collect();
let mut undefined_transitions = Vec::new();
for (state, transitions) in &program.rules {
for (i, transition) in transitions.iter().enumerate() {
if !defined_states.contains(&transition.next_state) && transition.next_state != "halt" {
undefined_transitions
.push(format!("{}[{}] -> {}", state, i, transition.next_state));
}
}
}
if !undefined_transitions.is_empty() {
return Err(AnalysisError::UndefinedNextStates(undefined_transitions));
}
Ok(())
}
fn check_unreachable_states(program: &Program) -> Result<(), AnalysisError> {
let initial_state = program.initial_state.clone();
let mut visited = HashSet::new();
let mut queue = vec![initial_state];
while let Some(state) = queue.pop() {
if visited.contains(&state) {
continue;
}
visited.insert(state.clone());
if let Some(transitions) = program.rules.get(&state) {
for transition in transitions {
if !visited.contains(&transition.next_state) {
queue.push(transition.next_state.clone());
}
}
}
}
let all_states: HashSet<String> = program.rules.keys().cloned().collect();
let mut unreachable: Vec<String> = all_states.difference(&visited).cloned().collect();
if !unreachable.is_empty() {
unreachable.sort(); return Err(AnalysisError::UnreachableStates(unreachable));
}
Ok(())
}
fn check_tape_symbols(program: &Program) -> Result<(), AnalysisError> {
let mut initial_tape_symbols = HashSet::new();
for tape in &program.tapes {
for c in tape.chars() {
initial_tape_symbols.insert(c);
}
}
if initial_tape_symbols.is_empty() {
return Ok(());
}
let mut handled_symbols = HashSet::new();
handled_symbols.insert(program.blank);
for transitions in program.rules.values() {
for transition in transitions {
for &symbol_in_read_vec in &transition.read {
handled_symbols.insert(symbol_in_read_vec);
}
}
}
let mut unhandled_symbols: Vec<char> = initial_tape_symbols
.iter()
.filter(|c| !handled_symbols.contains(c))
.cloned()
.collect();
if !unhandled_symbols.is_empty() {
unhandled_symbols.sort();
unhandled_symbols.dedup();
return Err(AnalysisError::InvalidTapeSymbols(unhandled_symbols));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Direction, Transition};
use std::collections::HashMap;
fn create_test_program(
initial_state: &str,
initial_tape: &str,
rules: HashMap<String, Vec<Transition>>,
) -> Program {
Program {
name: "Test Program".to_string(),
initial_state: initial_state.to_string(),
tapes: vec![initial_tape.to_string()],
heads: vec![0],
blank: '-',
rules,
}
}
fn create_single_tape_transition(
read: char,
write: char,
direction: Direction,
next_state: &str,
) -> Transition {
Transition {
read: vec![read],
write: vec![write],
directions: vec![direction],
next_state: next_state.to_string(),
}
}
#[test]
fn test_valid_program() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![create_single_tape_transition(
'a',
'b',
Direction::Right,
"halt",
)],
);
rules.insert("halt".to_string(), Vec::new());
let program = create_test_program("start", "a", rules);
let result = analyze(&program);
assert!(result.is_ok());
}
#[test]
fn test_invalid_head_position() {
let mut rules = HashMap::new();
rules.insert("start".to_string(), Vec::new());
let program = create_test_program("start", "", rules);
let result = check_head(&program);
assert!(result.is_ok());
}
#[test]
fn test_stop_states_not_found() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![create_single_tape_transition(
'a',
'b',
Direction::Right,
"other",
)],
);
rules.insert("halt".to_string(), Vec::new());
rules.insert("other".to_string(), Vec::new());
let program = create_test_program("start", "a", rules);
let result = check_valid_stop_states(&program);
assert!(result.is_err());
let error = result.unwrap_err();
assert_eq!(
error,
AnalysisError::StopStatesNotFound(vec!["halt".to_string()])
);
}
#[test]
fn test_analysis_error_conversion() {
let error = AnalysisError::InvalidHead(5);
let tm_error: TuringMachineError = error.into();
match tm_error {
TuringMachineError::ValidationError(msg) => {
assert!(msg.contains("Invalid head position: 5"));
}
_ => panic!("Expected ParseError"),
}
}
#[test]
fn test_undefined_next_states() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![create_single_tape_transition(
'a',
'b',
Direction::Right,
"nonexistent",
)],
);
let program = create_test_program("start", "a", rules);
let result = check_undefined_next_states(&program);
assert!(result.is_err());
let error = result.unwrap_err();
match error {
AnalysisError::UndefinedNextStates(transitions) => {
assert_eq!(transitions.len(), 1);
assert!(transitions[0].contains("nonexistent"));
}
_ => panic!("Expected UndefinedNextStates error"),
}
}
#[test]
fn test_undefined_next_states_with_halt() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![create_single_tape_transition(
'a',
'b',
Direction::Right,
"halt",
)],
);
let program = create_test_program("start", "a", rules);
let result = check_undefined_next_states(&program);
assert!(result.is_ok());
}
#[test]
fn test_unreachable_states() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![create_single_tape_transition(
'a',
'b',
Direction::Right,
"middle",
)],
);
rules.insert(
"middle".to_string(),
vec![create_single_tape_transition(
'b',
'c',
Direction::Right,
"end",
)],
);
rules.insert("end".to_string(), Vec::new());
rules.insert("unreachable".to_string(), Vec::new());
let program = create_test_program("start", "a", rules);
let result = check_unreachable_states(&program);
assert!(result.is_err());
let error = result.unwrap_err();
match error {
AnalysisError::UnreachableStates(states) => {
assert_eq!(states.len(), 1);
assert_eq!(states[0], "unreachable");
}
_ => panic!("Expected UnreachableStates error"),
}
}
#[test]
fn test_tape_symbols() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![create_single_tape_transition(
'a',
'b',
Direction::Right,
"halt",
)],
);
let program = create_test_program("start", "ac", rules);
let result = check_tape_symbols(&program);
assert!(result.is_err());
let error = result.unwrap_err();
match error {
AnalysisError::InvalidTapeSymbols(symbols) => {
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0], 'c');
}
_ => panic!("Expected InvalidTapeSymbols error"),
}
}
#[test]
fn test_multi_tape_symbols() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![Transition {
read: vec!['a', 'x'],
write: vec!['b', 'y'],
directions: vec![Direction::Right, Direction::Right],
next_state: "halt".to_string(),
}],
);
rules.insert("halt".to_string(), Vec::new());
let program_valid = Program {
name: "Valid Multi-Tape".to_string(),
initial_state: "start".to_string(),
tapes: vec!["a".to_string(), "x".to_string()],
heads: vec![0, 0],
blank: '-',
rules: rules.clone(),
};
assert!(check_tape_symbols(&program_valid).is_ok());
let program_invalid = Program {
name: "Invalid Multi-Tape".to_string(),
initial_state: "start".to_string(),
tapes: vec!["a".to_string(), "z".to_string()],
heads: vec![0, 0],
blank: '-',
rules,
};
let result = check_tape_symbols(&program_invalid);
assert!(result.is_err());
let error = result.unwrap_err();
match error {
AnalysisError::InvalidTapeSymbols(symbols) => {
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0], 'z');
}
_ => panic!("Expected InvalidTapeSymbols error"),
}
}
#[test]
fn test_all_checks_together() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![create_single_tape_transition(
'a',
'b',
Direction::Right,
"nonexistent",
)],
);
rules.insert("unreachable".to_string(), Vec::new());
let program = create_test_program("start", "ax", rules);
let result = analyze(&program);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.len() >= 3);
let has_undefined = errors
.iter()
.any(|e| matches!(e, AnalysisError::UndefinedNextStates(_)));
let has_unreachable = errors
.iter()
.any(|e| matches!(e, AnalysisError::UnreachableStates(_)));
let has_invalid_tape = errors
.iter()
.any(|e| matches!(e, AnalysisError::InvalidTapeSymbols(_)));
assert!(has_undefined, "Missing UndefinedNextStates error");
assert!(has_unreachable, "Missing UnreachableStates error");
assert!(has_invalid_tape, "Missing InvalidTapeSymbols error");
}
#[test]
fn test_valid_program_with_all_checks() {
let mut rules = HashMap::new();
rules.insert(
"start".to_string(),
vec![create_single_tape_transition(
'a',
'b',
Direction::Right,
"middle",
)],
);
rules.insert(
"middle".to_string(),
vec![create_single_tape_transition(
'b',
'c',
Direction::Right,
"halt",
)],
);
let program = create_test_program("start", "a", rules);
let result = analyze(&program);
assert!(result.is_ok());
}
}