From b23fc38129243bb8904993fc021e85f7627bbf92 Mon Sep 17 00:00:00 2001 From: Marut Khumtong Date: Wed, 20 Aug 2025 18:03:06 +0700 Subject: [PATCH] feat: add execution mode (Normal halts on undefined transitions, Strict treats them as errors) + refactor --- examples/even-zeros-and-ones.tur | 23 + examples/multi-tape-compare.tur | 34 +- examples/multi-tape-copy.tur | 12 +- examples/subtraction.tur | 41 +- platforms/cli/src/main.rs | 31 +- platforms/tui/src/app.rs | 166 +++---- platforms/web/src/app.rs | 104 ++-- platforms/web/src/components/mod.rs | 6 + .../web/src/components/program_editor.rs | 49 +- .../web/src/components/program_selector.rs | 2 +- platforms/web/src/components/tape_view.rs | 42 +- platforms/web/turing-editor.js | 2 +- src/analyzer.rs | 5 +- src/encoder.rs | 446 ++++++++++++++++++ src/grammar.pest | 4 +- src/lib.rs | 8 +- src/machine.rs | 304 ++++-------- src/parser.rs | 100 ++-- src/programs.rs | 24 +- src/types.rs | 104 ++-- 20 files changed, 905 insertions(+), 602 deletions(-) create mode 100644 examples/even-zeros-and-ones.tur create mode 100644 src/encoder.rs diff --git a/examples/even-zeros-and-ones.tur b/examples/even-zeros-and-ones.tur new file mode 100644 index 0000000..409c5ad --- /dev/null +++ b/examples/even-zeros-and-ones.tur @@ -0,0 +1,23 @@ +# https://www.youtube.com/watch?v=PLVCscCY4xI +name: Even Number of 0s and 1s +tape: 0, 0, 1, 1 +rules: + start: + X, R, start + 0 -> X, R, B + 1 -> X, R, C + _, R, accept + B: + X, R, B + 0, R, B + 1 -> X, L, D + C: + X, R, C + 1, R, C + 0 -> X, L, D + D: + X, L, D + 1, L, D + 0, L, D + _, R, start + accept: diff --git a/examples/multi-tape-compare.tur b/examples/multi-tape-compare.tur index 0104c40..302b6a7 100644 --- a/examples/multi-tape-compare.tur +++ b/examples/multi-tape-compare.tur @@ -3,23 +3,23 @@ heads: [0, 0, 0] tapes: [a, b, c] [a, b, c] - [-] + [_] rules: start: - [a, a, -] -> [a, a, -], [R, R, S], start - [b, b, -] -> [b, b, -], [R, R, S], start - [c, c, -] -> [c, c, -], [R, R, S], start - [-, -, -] -> [-, -, Y], [S, S, S], halt - [a, b, -] -> [a, b, N], [S, S, S], halt - [a, c, -] -> [a, c, N], [S, S, S], halt - [b, a, -] -> [b, a, N], [S, S, S], halt - [b, c, -] -> [b, c, N], [S, S, S], halt - [c, a, -] -> [c, a, N], [S, S, S], halt - [c, b, -] -> [c, b, N], [S, S, S], halt - [a, -, -] -> [a, -, N], [S, S, S], halt - [b, -, -] -> [b, -, N], [S, S, S], halt - [c, -, -] -> [c, -, N], [S, S, S], halt - [-, a, -] -> [-, a, N], [S, S, S], halt - [-, b, -] -> [-, b, N], [S, S, S], halt - [-, c, -] -> [-, c, N], [S, S, S], halt + [a, a, _] -> [a, a, _], [R, R, S], start + [b, b, _] -> [b, b, _], [R, R, S], start + [c, c, _] -> [c, c, _], [R, R, S], start + [_, _, _] -> [_, _, Y], [S, S, S], halt + [a, b, _] -> [a, b, N], [S, S, S], halt + [a, c, _] -> [a, c, N], [S, S, S], halt + [b, a, _] -> [b, a, N], [S, S, S], halt + [b, c, _] -> [b, c, N], [S, S, S], halt + [c, a, _] -> [c, a, N], [S, S, S], halt + [c, b, _] -> [c, b, N], [S, S, S], halt + [a, _, _] -> [a, _, N], [S, S, S], halt + [b, _, _] -> [b, _, N], [S, S, S], halt + [c, _, _] -> [c, _, N], [S, S, S], halt + [_, a, _] -> [_, a, N], [S, S, S], halt + [_, b, _] -> [_, b, N], [S, S, S], halt + [_, c, _] -> [_, c, N], [S, S, S], halt halt: diff --git a/examples/multi-tape-copy.tur b/examples/multi-tape-copy.tur index fffee27..abad0d4 100644 --- a/examples/multi-tape-copy.tur +++ b/examples/multi-tape-copy.tur @@ -2,16 +2,16 @@ name: Multi-Tape Copy heads: [0, 0] tapes: [a, b, c] - [-, -, -] + [_, _, _] rules: start: - [a, -] -> [a, a], [R, R], start - [b, -] -> [b, b], [R, R], start - [c, -] -> [c, c], [R, R], start - [-, -] -> [-, -], [L, L], rewind + [a, _] -> [a, a], [R, R], start + [b, _] -> [b, b], [R, R], start + [c, _] -> [c, c], [R, R], start + [_, _] -> [_, _], [L, L], rewind rewind: [a, a] -> [a, a], [L, L], rewind [b, b] -> [b, b], [L, L], rewind [c, c] -> [c, c], [L, L], rewind - [-, -] -> [-, -], [S, S], halt + [_, _] -> [_, _], [S, S], halt halt: diff --git a/examples/subtraction.tur b/examples/subtraction.tur index 980e46b..3284e0c 100644 --- a/examples/subtraction.tur +++ b/examples/subtraction.tur @@ -1,44 +1,53 @@ name: Subtraction tape: 1, 1, 1, -, 1, 1 +head: 0 + rules: start: - 0 -> 0, R, start 1 -> 1, R, start - -> -, R, s1 + _ -> _, L, stop s1: - 0 -> 0, R, s1 1 -> 1, R, s1 - - -> -, L, s2 + _ -> _, L, s2 s2: - 0 -> 0, L, s2 - 1 -> 0, L, s3 + 1 -> _, L, s3 + _ -> _, L, s2 - -> -, R, s5 s3: - 0 -> 0, L, s3 1 -> 1, L, s3 - -> -, L, s8 + _ -> _, L, s3 s4: - 0 -> 0, R, s4 1 -> 1, R, s4 - -> -, R, s5 + _ -> _, R, s4 s5: - 0 -> 0, R, s5 1 -> 1, R, s5 - - -> -, L, s6 + _ -> _, L, s6 s6: - 0 -> -, L, s6 + _ -> _, L, s6 1 -> 1, L, s6 - -> -, L, s7 s7: - 0 -> -, L, s7 + _ -> _, R, s9 1 -> 1, L, s7 - -> -, R, s9 s8: - 0 -> 0, L, s8 - 1 -> 0, R, start - - -> B, R, s4 + 1 -> _, R, start + - -> _, R, s4 + _ -> _, L, s8 s9: - 0 -> 0, L, stop + _ -> _, R, s10 1 -> 1, L, stop - - -> 0, L, stop + s10: + _ -> _, R, s10 + 1 -> 1, R, s10 + - -> _, L, s11 + s11: + _ -> _, L, s11 + 1 -> 1, L, s12 + s12: + 1 -> 1, L, s12 + _ -> _, R, stop stop: diff --git a/platforms/cli/src/main.rs b/platforms/cli/src/main.rs index cf02d04..40304c7 100644 --- a/platforms/cli/src/main.rs +++ b/platforms/cli/src/main.rs @@ -3,7 +3,7 @@ use std::io::{self, BufRead}; use std::path::Path; use tur::loader::ProgramLoader; use tur::machine::TuringMachine; -use tur::ExecutionResult; +use tur::Step; #[derive(Parser)] #[clap(author, version, about, long_about = None, arg_required_else_help = true)] @@ -52,28 +52,21 @@ fn main() { if cli.debug { run_with_debug(&mut machine); } else { - machine.run_to_completion(); + machine.run(); } - println!("{}", machine.get_tapes_as_strings().join("\n")); + println!("{}", format_tapes(machine.tapes()).join("\n")); } /// Runs the Turing machine with debug output, printing each step. fn run_with_debug(machine: &mut TuringMachine) { let print_state = |machine: &TuringMachine| { - let tapes_str = machine - .get_tapes_as_strings() - .iter() - .map(|s| s.to_string()) - .collect::>() - .join(", "); - println!( "Step: {}, State: {}, Tapes: [{}], Heads: {:?}", machine.step_count(), machine.state(), - tapes_str, - machine.head_positions() + format_tapes(machine.tapes()).join(", "), + machine.heads() ); }; @@ -81,19 +74,16 @@ fn run_with_debug(machine: &mut TuringMachine) { loop { match machine.step() { - ExecutionResult::Continue => { + Step::Continue => { print_state(machine); } - ExecutionResult::Halt => { + Step::Halt(_) => { println!("\nMachine halted."); break; } - ExecutionResult::Error(e) => { - println!("\nMachine error: {}", e); - break; - } } } + println!("\nFinal tapes:"); } @@ -121,3 +111,8 @@ fn read_tape_inputs(inputs: &[String]) -> Result, String> { Ok(Vec::new()) } } + +/// Returns the content of all tapes as a vector of `String`s. +pub fn format_tapes(tapes: &[Vec]) -> Vec { + tapes.iter().map(|tape| tape.iter().collect()).collect() +} diff --git a/platforms/tui/src/app.rs b/platforms/tui/src/app.rs index f18ff5e..7b232a6 100644 --- a/platforms/tui/src/app.rs +++ b/platforms/tui/src/app.rs @@ -4,12 +4,12 @@ use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, style::{Color, Modifier, Style}, text::{Line, Span, Text}, - widgets::{Block, BorderType, Borders, List, ListItem, Padding, Paragraph, Wrap}, + widgets::{Block, BorderType, Borders, Padding, Paragraph, Wrap}, Frame, }; use tur::{ types::{DEFAULT_BLANK_SYMBOL, INPUT_BLANK_SYMBOL}, - ExecutionResult, Program, ProgramLoader, ProgramManager, TuringMachine, + Program, ProgramLoader, ProgramManager, Step, TuringMachine, }; const BLOCK_PADDING: Padding = Padding::new(1, 1, 0, 0); @@ -24,11 +24,15 @@ pub struct App { pub(crate) keymap: Config, // Indicates if the program was loaded from a file/stdin, disabling program switching program_loaded_from_source: bool, + program_content: String, } impl App { pub fn new_default() -> Self { let program = ProgramManager::get_program_by_index(0).unwrap(); + let program_content = ProgramManager::get_program_text_by_index(0) + .unwrap() + .to_string(); let machine = TuringMachine::new(program); Self { @@ -40,6 +44,7 @@ impl App { message: "Press 'h' for help.".to_string(), show_help: false, program_loaded_from_source: false, + program_content, } } @@ -57,6 +62,7 @@ impl App { message: "Program loaded from source. Press 'h' for help.".to_string(), show_help: false, program_loaded_from_source: true, + program_content, }) } @@ -64,7 +70,7 @@ impl App { let margin_size = Margin::new(1, 0); // Define margin size let inner_area = f.area().inner(margin_size); - // Main vertical chunks: Program Info, Middle (Tapes + State/Rules), Status + // Main vertical chunks: Program Info, Middle (Source + Machine), Status let main_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -74,54 +80,69 @@ impl App { ]) .split(inner_area); - // Apply margin to Program Info - let program_info_area = main_chunks[0]; - self.render_program_info(f, program_info_area); + // Render Program Info in the top main chunk + self.render_program_info(f, main_chunks[0]); - // Middle horizontal chunks: Tapes (left), State + Rules (right) + // Middle horizontal chunks: Source Code (left), Machine (right) let middle_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Percentage(50), // Tapes (takes 50% width) - Constraint::Length(1), // Machine State + State Rules (takes 50% width) - Constraint::Percentage(50), // Tapes (takes 50% width) + Constraint::Percentage(50), // Source code + Constraint::Length(1), + Constraint::Percentage(50), // Machine ]) .split(main_chunks[1]); - // Render Tapes in the left middle chunk with margin - let left_chunk = middle_chunks[0]; - let tape_area = Layout::default() - .direction(Direction::Vertical) - // .constraints([Constraint::Length(tape_section_height as u16), Constraint::Min(0)]) - .constraints([Constraint::Percentage(100)]) - .split(left_chunk)[0]; - self.render_tapes(f, tape_area); + self.render_source_code(f, middle_chunks[0]); - // Right vertical chunks: Machine State, State Rules/Help + // Right vertical chunks: Machine State, Tapes/Help let right_chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // Machine State (fixed height) - Constraint::Min(0), // State Rules/Help (flexible height) + Constraint::Length(3), // Machine State + Constraint::Min(0), // Tapes ]) .split(middle_chunks[2]); - // Render Machine State in the top right middle chunk with margin - let machine_state_area = right_chunks[0]; - self.render_machine_state(f, machine_state_area); + self.render_machine_state(f, right_chunks[0]); - // Render State Rules or Help in the bottom right middle chunk with margin - let rules_help_area = right_chunks[1]; if self.show_help { - self.render_help(f, rules_help_area); + self.render_help(f, right_chunks[1]); } else { - self.render_rules(f, rules_help_area); + self.render_tapes(f, right_chunks[1]); } - // Render Status in the bottom main chunk with margin + // Render Status in the bottom main chunk self.render_status(f, main_chunks[2]); } + fn render_source_code(&self, f: &mut Frame, area: Rect) { + let keywords = [ + "name:", "mode:", "head:", "heads:", "blank:", "tape:", "tapes:", "states:", "rules:", + ]; + + let mut lines = Vec::new(); + for line in self.program_content.lines() { + let mut spans = Vec::new(); + let mut parts = line.split_whitespace(); + if let Some(first_word) = parts.next() { + if keywords.contains(&first_word) { + spans.push(Span::styled(first_word, Style::default().fg(Color::Yellow))); + spans.push(Span::raw(" ")); + spans.push(Span::raw(parts.collect::>().join(" "))); + } else { + spans.push(Span::raw(line)); + } + } else { + spans.push(Span::raw(line)); + } + lines.push(Line::from(spans)); + } + + let paragraph = section("Source Code", lines).wrap(Wrap { trim: false }); + f.render_widget(paragraph, area); + } + fn render_program_info(&self, f: &mut Frame, area: Rect) { let program = ProgramManager::get_program_by_index(self.current_program_index).unwrap(); let info = ProgramManager::get_program_info(self.current_program_index).unwrap(); @@ -136,7 +157,7 @@ impl App { "{} ({}/{})", program.name, self.current_program_index + 1, - ProgramManager::get_program_count() + ProgramManager::count() ) }), ])]; @@ -175,7 +196,7 @@ impl App { fn render_tapes(&self, f: &mut Frame, area: Rect) { let tapes = self.machine.tapes(); - let head_positions = self.machine.head_positions(); + let head_positions = self.machine.heads(); let tape_count = tapes.len(); let mut text_lines = Vec::new(); @@ -220,7 +241,7 @@ impl App { let current_symbol = if head_pos < tape.len() { tape[head_pos] } else { - self.machine.blank_symbol() + self.machine.blank() }; let head_indicator = format!( "Head at position: {} (symbol: '{}')", @@ -272,7 +293,7 @@ impl App { ])]; // Always use array format for symbols (consistent for single and multi-tape) - let current_symbols = self.machine.get_current_symbols(); + let current_symbols = self.machine.symbols(); let symbols_str: String = current_symbols .iter() .map(|&c| format!("'{}'", c)) @@ -290,61 +311,6 @@ impl App { f.render_widget(paragraph, area); } - fn render_rules(&self, f: &mut Frame, area: Rect) { - let tape_count = self.machine.tapes().len(); - let current_state = self.machine.state(); - let current_symbols = self.machine.get_current_symbols(); - - let mut items = Vec::new(); - - // Always show state - items.push(ListItem::new(Line::from(vec![ - Span::styled("State: ", Style::default().fg(Color::Yellow)), - Span::raw(current_state), - ]))); - - // Always use array format for symbols (consistent) - let symbols_str: String = current_symbols - .iter() - .map(|&c| format!("'{}'", c)) - .collect::>() - .join(", "); - - items.push(ListItem::new(Line::from(vec![ - Span::styled("Reading: [", Style::default().fg(Color::Cyan)), - Span::raw(symbols_str), - Span::styled("]", Style::default().fg(Color::Cyan)), - ]))); - - // For single-tape, still show available transitions - if tape_count == 1 { - let available_transitions = self.machine.get_available_transitions(); - if !available_transitions.is_empty() { - items.push(ListItem::new(Line::from(""))); - items.push(ListItem::new(Line::from("Available rules:"))); - for &symbol in &available_transitions { - items.push(ListItem::new(Line::from(format!( - " On '{}' -> transition available", - symbol - )))); - } - } - } else { - items.push(ListItem::new(Line::from(""))); - items.push(ListItem::new(Line::from("Multi-tape rules are complex."))); - items.push(ListItem::new(Line::from( - "See program definition for details.", - ))); - } - - // Always use "State Information" for consistency - let list = List::new(items) - .block(block("State Information")) - .style(Style::default().fg(Color::White)); - - f.render_widget(list, area); - } - fn render_help(&self, f: &mut Frame, area: Rect) { let tape_count = self.machine.tapes().len(); @@ -400,22 +366,12 @@ impl App { } pub fn step_machine(&mut self) { - if self.machine.is_halted() { - self.message = "Machine is halted. Press 'r' to reset.".to_string(); - self.auto_play = false; - return; - } - match self.machine.step() { - ExecutionResult::Continue => { + Step::Continue => { self.message = format!("Step {} completed", self.machine.step_count()); } - ExecutionResult::Halt => { - self.message = "Machine halted".to_string(); - self.auto_play = false; - } - ExecutionResult::Error(e) => { - self.message = format!("Error: {}", e); + Step::Halt(_) => { + self.message = "Machine is halted. Press 'r' to reset.".to_string(); self.auto_play = false; } } @@ -449,7 +405,7 @@ impl App { self.message = "Cannot switch programs when loaded from file/stdin.".to_string(); return; } - let count = ProgramManager::get_program_count(); + let count = ProgramManager::count(); self.current_program_index = (self.current_program_index + 1) % count; self.load_current_program(); } @@ -459,7 +415,7 @@ impl App { self.message = "Cannot switch programs when loaded from file/stdin.".to_string(); return; } - let count = ProgramManager::get_program_count(); + let count = ProgramManager::count(); self.current_program_index = if self.current_program_index == 0 { count - 1 } else { @@ -470,6 +426,10 @@ impl App { fn load_current_program(&mut self) { let program = ProgramManager::get_program_by_index(self.current_program_index).unwrap(); + self.program_content = + ProgramManager::get_program_text_by_index(self.current_program_index) + .unwrap() + .to_string(); let tape_count = program.tapes.len(); let program_name = program.name.clone(); self.machine = TuringMachine::new(program); diff --git a/platforms/web/src/app.rs b/platforms/web/src/app.rs index 47546a5..61f4f10 100644 --- a/platforms/web/src/app.rs +++ b/platforms/web/src/app.rs @@ -1,10 +1,11 @@ -use crate::components::{GraphView, ProgramEditor, ShareButton, TapeView}; +use crate::components::{GraphView, MachineState, ProgramEditor, ShareButton, TapeView}; use crate::url_sharing::UrlSharing; use action::Action; use gloo_events::EventListener; use keymap::{Config, KeyMapConfig}; -use tur::{Direction, ExecutionResult, Program, ProgramManager, Transition, TuringMachine}; +use tur::types::Halt; +use tur::{Direction, Program, ProgramManager, Step, Transition, TuringMachine}; use wasm_bindgen::JsCast; use yew::prelude::*; @@ -33,6 +34,7 @@ pub struct App { last_transition: Option, previous_state: String, speed: u64, + machine_state: MachineState, _keyboard_listener: EventListener, keymap: Config, @@ -64,6 +66,7 @@ impl App { last_transition: None, previous_state: initial_state, speed: 500, + machine_state: MachineState::Running, _keyboard_listener: keyboard_listener, keymap: Action::keymap_config(), @@ -146,7 +149,7 @@ impl Component for App { self.show_program_editor_help = !self.show_program_editor_help; } Action::PreviousProgram => { - let count = ProgramManager::get_program_count(); + let count = ProgramManager::count(); if count > 0 { let new_index = if self.current_program == 0 || self.current_program == usize::MAX @@ -159,7 +162,7 @@ impl Component for App { } } Action::NextProgram => { - let count = ProgramManager::get_program_count(); + let count = ProgramManager::count(); if count > 0 { let new_index = if self.current_program == usize::MAX { 0 @@ -181,55 +184,46 @@ impl Component for App { } Msg::Step => { if self.machine.is_halted() { - self.message = "Machine is halted. Reset to continue.".to_string(); + self.message = "Machine is halted. Press 'r' to reset.".to_string(); self.auto_play = false; - self.last_transition = None; - } else { - let last_head_positions = self.machine.head_positions().to_vec(); - let last_tape_lengths: Vec = - self.machine.tapes().iter().map(|t| t.len()).collect(); + self.machine_state = MachineState::Halted; + return true; + } - // Store the current state as previous state before stepping - self.previous_state = self.machine.state().to_string(); + self.previous_state = self.machine.state().to_string(); + self.last_transition = self.machine.transition().cloned(); + let last_head_positions = self.machine.heads().to_vec(); + let last_tape_lengths: Vec = + self.machine.tapes().iter().map(|t| t.len()).collect(); - // Get the transition that will be executed before stepping - self.last_transition = self.machine.get_current_transition().cloned(); + let result = self.machine.step(); - match self.machine.step() { - ExecutionResult::Continue => { - self.message = format!("Step {} completed", self.machine.step_count()); - } - ExecutionResult::Halt => { - self.message = "Machine halted".to_string(); - self.auto_play = false; - } - ExecutionResult::Error(e) => { - self.message = format!("Error: {}", e); - self.auto_play = false; - self.last_transition = None; - } + match result { + Step::Continue => { + self.message = format!("Step {} completed", self.machine.step_count()); + self.machine_state = MachineState::Running; } + Step::Halt(reason) => { + self.message = match reason { + Halt::Ok => "Machine halted".to_string(), + Halt::Err(err) => format!("Machine halted with error: {err}"), + }; + self.machine_state = MachineState::Halted; + self.auto_play = false; + } + } - // let new_head_positions = self.machine.head_positions().to_vec(); - let new_tape_lengths: Vec = - self.machine.tapes().iter().map(|t| t.len()).collect(); - - if let Some(transition) = &self.last_transition { - for i in 0..last_head_positions.len() { - // web_sys::console::log_1( - // &format!( - // "{i} = {:?}, {new_tape_lengths:?} = {last_tape_lengths:?}", - // transition.directions[i] - // ) - // .into(), - // ); - - // Check if tape expanded to the left (length increased) - if transition.directions[i] == Direction::Left - && new_tape_lengths[i] > last_tape_lengths[i] - { - self.tape_left_offsets[i] += 1; - } + // Update tape's left offset for animation purposes + let new_tape_lengths: Vec = + self.machine.tapes().iter().map(|t| t.len()).collect(); + + if let Some(transition) = &self.last_transition { + for i in 0..last_head_positions.len() { + // Check if tape expanded to the left (length increased) + if transition.directions[i] == Direction::Left + && new_tape_lengths[i] > last_tape_lengths[i] + { + self.tape_left_offsets[i] += 1; } } } @@ -242,6 +236,7 @@ impl Component for App { self.last_transition = None; self.tape_left_offsets = vec![0; self.machine.tapes().len()]; self.previous_state = self.machine.initial_state().to_string(); + self.machine_state = MachineState::Running; true } Msg::ToggleAutoPlay => { @@ -278,14 +273,16 @@ impl Component for App { self.last_transition = None; self.previous_state = program.initial_state.clone(); self.machine = TuringMachine::new(program); + self.tape_left_offsets = vec![0; self.machine.tapes().len()]; self.message = "".to_string(); + self.machine_state = MachineState::Running; true } else { false } } Msg::AutoStep => { - if self.auto_play && !self.machine.is_halted() { + if self.auto_play && self.machine_state == MachineState::Running { ctx.link().send_message(Msg::Step); let link = ctx.link().clone(); @@ -305,7 +302,9 @@ impl Component for App { self.last_transition = None; self.previous_state = program.initial_state.clone(); self.machine = TuringMachine::new(program); + self.tape_left_offsets = vec![0; self.machine.tapes().len()]; self.message = "".to_string(); + self.machine_state = MachineState::Running; true } Msg::EditorError(error) => { @@ -387,20 +386,21 @@ impl Component for App {
diff --git a/platforms/web/src/components/mod.rs b/platforms/web/src/components/mod.rs index afbe71a..fa8400f 100644 --- a/platforms/web/src/components/mod.rs +++ b/platforms/web/src/components/mod.rs @@ -4,6 +4,12 @@ mod program_selector; mod share_button; mod tape_view; +#[derive(Debug, Clone, PartialEq)] +pub enum MachineState { + Running, + Halted, +} + pub use graph_view::GraphView; pub use program_editor::ProgramEditor; pub use program_selector::ProgramSelector; diff --git a/platforms/web/src/components/program_editor.rs b/platforms/web/src/components/program_editor.rs index e686b37..abcc866 100644 --- a/platforms/web/src/components/program_editor.rs +++ b/platforms/web/src/components/program_editor.rs @@ -121,15 +121,9 @@ Please reduce the program size.", } else { match parse(&self.program_text) { Ok(program) => { - if let Err(validation_error) = validate_program_structure(&program) { - self.parse_error = Some(validation_error.clone()); - self.is_valid = false; - ctx.props().on_error.emit(validation_error); - } else { - self.parse_error = None; - self.is_valid = true; - ctx.props().on_program_submit.emit(program); - } + self.parse_error = None; + self.is_valid = true; + ctx.props().on_program_submit.emit(program); } Err(e) => { let error_msg = format_parse_error(&e); @@ -322,43 +316,6 @@ fn extract_line_info(error: &str) -> (String, String) { ("".to_string(), error.to_string()) } -fn validate_program_structure(program: &Program) -> Result<(), String> { - // Check for empty name - if program.name.trim().is_empty() { - return Err("Program name cannot be empty.\n\nAdd: name: Your Program Name".to_string()); - } - - // Check for empty tape - if program.initial_tape().is_empty() { - return Err("Initial tape cannot be empty.\n\nAdd: tape: your_initial_content".to_string()); - } - - // Check for no transitions - if program.rules.is_empty() { - return Err("Program must have at least one state with rules.\n\nAdd rules section with at least one state.".to_string()); - } - - // Check for unreachable states (basic check) - let mut reachable_states = std::collections::HashSet::new(); - reachable_states.insert("start".to_string()); - - for transitions in program.rules.values() { - for transition in transitions { - reachable_states.insert(transition.next_state.clone()); - } - } - - // Check if initial state exists - if !program.rules.contains_key(&program.initial_state) { - return Err(format!( - "Initial state '{}' not defined in rules.", - program.initial_state - )); - } - - Ok(()) -} - fn format_parse_error(error: &TuringMachineError) -> String { match error { TuringMachineError::ParseError(msg) => format!("Parse Error: {msg}"), diff --git a/platforms/web/src/components/program_selector.rs b/platforms/web/src/components/program_selector.rs index 06318c1..12cff22 100644 --- a/platforms/web/src/components/program_selector.rs +++ b/platforms/web/src/components/program_selector.rs @@ -38,7 +38,7 @@ pub fn program_selector(props: &ProgramSelectorProps) -> Html { } else { html! {} }} - {(0..ProgramManager::get_program_count()).map(|i| { + {(0..ProgramManager::count()).map(|i| { let program = ProgramManager::get_program_by_index(i).unwrap(); let tape_indicator = if program.is_single_tape() { "📼" } else { "📼📼" }; html! { diff --git a/platforms/web/src/components/tape_view.rs b/platforms/web/src/components/tape_view.rs index 9310b52..86bb8e4 100644 --- a/platforms/web/src/components/tape_view.rs +++ b/platforms/web/src/components/tape_view.rs @@ -1,3 +1,4 @@ +use crate::components::MachineState; use yew::{function_component, html, Callback, Event, Html, Properties, TargetCast}; #[derive(Properties, PartialEq)] @@ -5,7 +6,7 @@ pub struct TapeViewProps { pub tapes: Vec>, pub head_positions: Vec, pub auto_play: bool, - pub is_halted: bool, + pub machine_state: MachineState, pub is_program_ready: bool, pub blank_symbol: char, pub state: String, @@ -17,6 +18,7 @@ pub struct TapeViewProps { pub speed: u64, pub on_speed_change: Callback, pub tape_left_offsets: Vec, + pub message: String, } #[function_component(TapeView)] @@ -25,6 +27,7 @@ pub fn tape_view(props: &TapeViewProps) -> Html { let padding_cells = 15; // Number of blank cells to show on each side for infinite tape effect let on_speed_change = props.on_speed_change.clone(); + let is_machine_running = props.machine_state == MachineState::Running; html! {
@@ -34,7 +37,7 @@ pub fn tape_view(props: &TapeViewProps) -> Html { @@ -48,7 +51,7 @@ pub fn tape_view(props: &TapeViewProps) -> Html { @@ -150,27 +153,24 @@ pub fn tape_view(props: &TapeViewProps) -> Html {
{"Status"} - - { - if props.is_halted { - "HALTED" - } else if props.auto_play { - "RUNNING" - } else { - "READY" - } - } + "value status halted", + MachineState::Running if props.auto_play => "value status running", + MachineState::Running => "value status ready", + }}> + {match props.machine_state { + MachineState::Halted => "HALTED", + MachineState::Running if props.auto_play => "RUNNING", + MachineState::Running => "READY", + }}
+ {if !props.message.is_empty() { + html! {
{&props.message}
} + } else { + html! {} + }} } } diff --git a/platforms/web/turing-editor.js b/platforms/web/turing-editor.js index 00b7582..92e70c5 100644 --- a/platforms/web/turing-editor.js +++ b/platforms/web/turing-editor.js @@ -49,7 +49,7 @@ window.addEventListener("load", () => { // Section headers if ( stream.match( - /^(name|head|heads|blank|tape|tapes|states|rules):/, + /^(name|mode|head|heads|blank|tape|tapes|states|rules):/, ) ) { const matched = stream.current(); diff --git a/src/analyzer.rs b/src/analyzer.rs index 1d1608d..70b0377 100644 --- a/src/analyzer.rs +++ b/src/analyzer.rs @@ -368,7 +368,7 @@ fn check_tape_symbols(program: &Program) -> Result<(), AnalysisError> { #[cfg(test)] mod tests { use super::*; - use crate::types::{Direction, Transition}; + use crate::types::{Direction, Mode, Transition}; use std::collections::HashMap; fn create_test_program( @@ -378,6 +378,7 @@ mod tests { ) -> Program { Program { name: "Test Program".to_string(), + mode: Mode::Normal, initial_state: initial_state.to_string(), tapes: vec![initial_tape.to_string()], heads: vec![0], @@ -624,6 +625,7 @@ mod tests { // Initial tapes: ["a", "x"], should be valid let program_valid = Program { name: "Valid Multi-Tape".to_string(), + mode: Mode::Normal, initial_state: "start".to_string(), tapes: vec!["a".to_string(), "x".to_string()], heads: vec![0, 0], @@ -635,6 +637,7 @@ mod tests { // Initial tapes: ["a", "z"], 'z' is not handled let program_invalid = Program { name: "Invalid Multi-Tape".to_string(), + mode: Mode::Normal, initial_state: "start".to_string(), tapes: vec!["a".to_string(), "z".to_string()], heads: vec![0, 0], diff --git a/src/encoder.rs b/src/encoder.rs new file mode 100644 index 0000000..936d04a --- /dev/null +++ b/src/encoder.rs @@ -0,0 +1,446 @@ +//! This module provides encoding functionality for converting Turing Machine programs +//! into a string format suitable for Universal Turing Machine processing. + +use crate::types::{Direction, Mode, Program, Transition}; +use std::collections::{hash_map::Entry, HashMap}; + +/// Encodes a Turing Machine program into a string format for Universal Turing Machine. +/// +/// Format: `name:tape:rules` +/// - name: The name of the program. +/// - tape: Comma-separated symbols of the initial tape. +/// - rules: Pipe-separated transitions in format `current_state,input,output,direction,next_state`. +/// +/// # Arguments +/// +/// * `program` - The Program to encode. +/// +/// # Returns +/// +/// * `String` - The encoded program string. +pub fn encode(program: &Program) -> String { + let state_mapping = create_state_mapping(program); + + let rules_section = encode_rules(program, &state_mapping); + let tape_section = encode_initial_tape(program); + + format!("{}:{}:{}", program.name, tape_section, rules_section) +} + +/// Creates a mapping from state names to numeric identifiers. +fn create_state_mapping(program: &Program) -> HashMap { + let mut mapping = HashMap::new(); + let mut counter = 0; + + // Always map initial state to 0 + mapping.insert(program.initial_state.clone(), "0".to_string()); + counter += 1; + + // Collect all unique states from rules + let mut states: Vec = program.rules.keys().cloned().collect(); + + // Add states from transitions + for transitions in program.rules.values() { + for transition in transitions { + states.push(transition.next_state.clone()); + } + } + + // Remove duplicates and sort for consistent encoding + states.sort(); + states.dedup(); + + // Map remaining states + for state in states { + if let Entry::Vacant(e) = mapping.entry(state) { + // Use special mappings for common halt states + let encoded = match e.key().as_str() { + "halt" => "h".to_string(), + "accept" => "a".to_string(), + "stop" => "s".to_string(), + "reject" => "r".to_string(), + _ => counter.to_string(), + }; + + let is_special = matches!(e.key().as_str(), "halt" | "accept" | "stop" | "reject"); + e.insert(encoded); + if !is_special { + counter += 1; + } + } + } + + mapping +} + +/// Encodes the rules section as pipe-separated transitions. +fn encode_rules(program: &Program, state_mapping: &HashMap) -> String { + let mut encoded_rules = Vec::new(); + + // Sort states for consistent output + let mut sorted_states: Vec<_> = program.rules.keys().collect(); + sorted_states.sort(); + + for state in sorted_states { + let state_encoded = state_mapping.get(state).unwrap(); + let transitions = program.rules.get(state).unwrap(); + + for transition in transitions { + // For single-tape machines, use first elements + let input_symbol = transition.read.first().unwrap_or(&'_'); + let output_symbol = transition.write.first().unwrap_or(&'_'); + let direction = transition.directions.first().unwrap_or(&Direction::Stay); + let next_state_encoded = state_mapping.get(&transition.next_state).unwrap(); + + let direction_char = match direction { + Direction::Left => 'L', + Direction::Right => 'R', + Direction::Stay => 'S', + }; + + let rule = format!( + "{},{},{},{},{}", + state_encoded, input_symbol, output_symbol, direction_char, next_state_encoded + ); + + encoded_rules.push(rule); + } + } + + encoded_rules.join("|") +} + +/// Encodes the initial tape as comma-separated symbols. +fn encode_initial_tape(program: &Program) -> String { + if let Some(first_tape) = program.tapes.first() { + first_tape + .chars() + .map(|c| c.to_string()) + .collect::>() + .join(",") + } else { + String::new() + } +} + +/// Decodes an encoded program string back into a Program structure. +/// +/// # Arguments +/// +/// * `encoded` - The encoded program string in `name:tape:rules` format. +/// +/// # Returns +/// +/// * `Result` - The decoded Program or an error message. +pub fn decode(encoded: &str) -> Result { + let parts: Vec<&str> = encoded.split(':').collect(); + if parts.len() != 3 { + return Err("Invalid encoding format: expected 3 sections separated by :".to_string()); + } + + let name = parts[0]; + let tape_section = parts[1]; + let rules_section = parts[2]; + + // Create reverse state mapping from the rules + let state_mapping = decode_states_from_rules(rules_section)?; + let rules = decode_rules(rules_section, &state_mapping)?; + let initial_tape = decode_initial_tape(tape_section)?; + + // Find initial state (mapped to "0") + let initial_state = state_mapping + .get("0") + .ok_or("No initial state found (state 0)")? + .clone(); + + Ok(Program { + name: name.to_string(), + mode: Mode::default(), + initial_state, + tapes: vec![initial_tape], + heads: vec![0], + blank: '_', + rules, + }) +} + +/// Decodes the states from the rules section into a reverse mapping. +fn decode_states_from_rules(rules_section: &str) -> Result, String> { + let mut mapping = HashMap::new(); + let mut encoded_states = std::collections::HashSet::::new(); + + if !rules_section.is_empty() { + let rule_strings: Vec<&str> = rules_section.split('|').collect(); + for rule_str in rule_strings { + let parts: Vec<&str> = rule_str.split(',').collect(); + if parts.len() != 5 { + return Err(format!("Invalid rule format: {}", rule_str)); + } + encoded_states.insert(parts[0].to_string()); + encoded_states.insert(parts[4].to_string()); + } + } + + // The initial state "0" might not be in the rules if the program is empty or has no transitions from start. + encoded_states.insert("0".to_string()); + + for encoded_state in &encoded_states { + let original_state = match encoded_state.as_str() { + "h" => "halt".to_string(), + "a" => "accept".to_string(), + "s" => "stop".to_string(), + "r" => "reject".to_string(), + "0" => "start".to_string(), // Initial state + _ => { + if let Ok(num) = encoded_state.parse::() { + if num == 1 { + // Common case: first non-initial state is often s2 + "s2".to_string() + } else { + format!("s{}", num + 1) + } + } else { + encoded_state.to_string() + } + } + }; + mapping.insert(encoded_state.clone(), original_state); + } + + Ok(mapping) +} + +/// Decodes the rules section into transition rules. +fn decode_rules( + rules_section: &str, + state_mapping: &HashMap, +) -> Result>, String> { + let mut rules = HashMap::new(); + + if rules_section.is_empty() { + return Ok(rules); + } + + let rule_strings: Vec<&str> = rules_section.split('|').collect(); + + for rule_str in rule_strings { + let parts: Vec<&str> = rule_str.split(',').collect(); + if parts.len() != 5 { + return Err(format!("Invalid rule format: {}", rule_str)); + } + + let current_state_encoded = parts[0]; + let input_symbol = parts[1].chars().next().unwrap_or('_'); + let output_symbol = parts[2].chars().next().unwrap_or('_'); + let direction_char = parts[3].chars().next().unwrap_or('S'); + let next_state_encoded = parts[4]; + + let current_state = state_mapping + .get(current_state_encoded) + .ok_or(format!("Unknown state: {}", current_state_encoded))? + .clone(); + + let next_state = state_mapping + .get(next_state_encoded) + .ok_or(format!("Unknown state: {}", next_state_encoded))? + .clone(); + + let direction = match direction_char { + 'L' => Direction::Left, + 'R' => Direction::Right, + 'S' => Direction::Stay, + _ => return Err(format!("Invalid direction: {}", direction_char)), + }; + + let transition = Transition { + read: vec![input_symbol], + write: vec![output_symbol], + directions: vec![direction], + next_state, + }; + + rules + .entry(current_state) + .or_insert_with(Vec::new) + .push(transition); + } + + Ok(rules) +} + +/// Decodes the initial tape section. +fn decode_initial_tape(tape_section: &str) -> Result { + if tape_section.is_empty() { + return Ok(String::new()); + } + + let symbols: Vec<&str> = tape_section.split(',').collect(); + Ok(symbols.join("")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{Direction, Program, Transition}; + use std::collections::HashMap; + + fn create_test_program() -> Program { + let mut rules = HashMap::new(); + + // start: a -> b, R, s2 + rules.insert( + "start".to_string(), + vec![Transition { + read: vec!['a'], + write: vec!['b'], + directions: vec![Direction::Right], + next_state: "s2".to_string(), + }], + ); + + // s2: b -> b, R, halt + rules.insert( + "s2".to_string(), + vec![Transition { + read: vec!['b'], + write: vec!['b'], + directions: vec![Direction::Right], + next_state: "halt".to_string(), + }], + ); + + Program { + name: "Test Program".to_string(), + mode: Mode::default(), + initial_state: "start".to_string(), + tapes: vec!["abb".to_string()], + heads: vec![0], + blank: '_', + rules, + } + } + + #[test] + fn test_encode_program() { + let program = create_test_program(); + let encoded = encode(&program); + + // Should contain name, tape, and rules sections + assert!(encoded.contains(':')); + let parts: Vec<&str> = encoded.split(':').collect(); + assert_eq!(parts.len(), 3); + assert_eq!(parts[0], "Test Program"); + + println!("Encoded: {}", encoded); + } + + #[test] + fn test_state_mapping() { + let program = create_test_program(); + let mapping = create_state_mapping(&program); + + // start should map to 0 + assert_eq!(mapping.get("start"), Some(&"0".to_string())); + // halt should map to h + assert_eq!(mapping.get("halt"), Some(&"h".to_string())); + // s2 should map to some number + assert!(mapping.contains_key("s2")); + } + + #[test] + fn test_encode_rules() { + let program = create_test_program(); + let state_mapping = create_state_mapping(&program); + let rules = encode_rules(&program, &state_mapping); + + // Should contain pipe-separated rules + assert!(rules.contains('|')); + // Should contain comma-separated rule components + assert!(rules.contains(',')); + + println!("Encoded rules: {}", rules); + } + + #[test] + fn test_encode_initial_tape() { + let program = create_test_program(); + let tape = encode_initial_tape(&program); + + assert_eq!(tape, "a,b,b"); + } + + #[test] + fn test_round_trip_encoding() { + let original = create_test_program(); + let encoded = encode(&original); + println!( + "Original program rules: {:?}", + original.rules.keys().collect::>() + ); + println!("Encoded: {}", encoded); + + let decoded = decode(&encoded).unwrap(); + println!( + "Decoded program rules: {:?}", + decoded.rules.keys().collect::>() + ); + + // Check that key properties are preserved + assert_eq!(decoded.name, "Test Program"); + assert_eq!(decoded.initial_state, "start"); + assert_eq!(decoded.tapes[0], "abb"); + assert!(decoded.rules.contains_key("start")); + // The state "s2" gets encoded as "1" and decoded as "s2" + assert!(decoded.rules.contains_key("s2")); + assert_eq!(decoded.rules.len(), 2); + } + + #[test] + fn test_decode_invalid_format() { + let result = decode("invalid"); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Invalid encoding format")); + } + + #[test] + fn test_simple_example() { + // Test the exact example from the design + let mut rules = HashMap::new(); + + rules.insert( + "start".to_string(), + vec![ + Transition { + read: vec!['a'], + write: vec!['b'], + directions: vec![Direction::Right], + next_state: "s2".to_string(), + }, + Transition { + read: vec!['b'], + write: vec!['b'], + directions: vec![Direction::Right], + next_state: "s2".to_string(), + }, + ], + ); + + let program = Program { + name: "Simple Example".to_string(), + mode: Mode::default(), + initial_state: "start".to_string(), + tapes: vec!["ab".to_string()], + heads: vec![0], + blank: '_', + rules, + }; + + let encoded = encode(&program); + println!("Simple example encoded: {}", encoded); + + // Should match the expected format: name:tape:rules + assert!(encoded.starts_with("Simple Example:a,b:")); + assert!(encoded.contains("0,a,b,R,")); + assert!(encoded.contains("0,b,b,R,")); + } +} diff --git a/src/grammar.pest b/src/grammar.pest index 4202f88..882e538 100644 --- a/src/grammar.pest +++ b/src/grammar.pest @@ -5,6 +5,7 @@ program = { SOI ~ ( (LEADING* ~ name) + | (LEADING+ ~ mode) | (LEADING+ ~ head) | (LEADING+ ~ heads) | (LEADING+ ~ blank) @@ -20,6 +21,7 @@ program = { // TOP-LEVEL SECTIONS // ============================================================================= name = { "name:" ~ string } +mode = { "mode:" ~ string } head = { "head:" ~ index } heads = { "heads:" ~ "[" ~ index ~ ("," ~ index)* ~ "]" } blank = { "blank:" ~ symbol } @@ -51,7 +53,7 @@ last_transition = ${ state ~ ":" } // ============================================================================= actions = _{ block_start ~ (comment | action+) ~ trailing_comment? ~ block_end } action = !{ (multi_tape_action | single_tape_action) } -multi_tape_action = { multi_tape_symbols ~ "->" ~ multi_tape_symbols ~ "," ~ directions ~ "," ~ state } +multi_tape_action = { multi_tape_symbols ~ ("->" ~ multi_tape_symbols)? ~ "," ~ directions ~ "," ~ state } multi_tape_symbols = { "[" ~ symbol ~ ("," ~ symbol)* ~ "]" } single_tape_action = { symbol ~ ("->" ~ symbol)? ~ "," ~ direction ~ "," ~ state } directions = { "[" ~ direction ~ ("," ~ direction)* ~ "]" } diff --git a/src/lib.rs b/src/lib.rs index a0f579e..b941ff5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ //! analyzing program correctness, and managing a collection of predefined programs. pub mod analyzer; +pub mod encoder; pub mod loader; pub mod machine; pub mod parser; @@ -13,6 +14,8 @@ pub mod types; pub use crate::parser::Rule; /// Re-exports the `analyze` function and `AnalysisError` enum from the analyzer module. pub use analyzer::{analyze, AnalysisError}; +/// Re-exports the encoding functions from the encoder module. +pub use encoder::{decode, encode}; /// Re-exports the `ProgramLoader` struct from the loader module. pub use loader::ProgramLoader; /// Re-exports the `TuringMachine` struct from the machine module. @@ -22,7 +25,4 @@ pub use parser::parse; /// Re-exports `ProgramInfo`, `ProgramManager`, and `PROGRAMS` from the programs module. pub use programs::{ProgramInfo, ProgramManager, PROGRAMS}; /// Re-exports various types related to Turing Machine definition and execution from the types module. -pub use types::{ - Direction, ExecutionResult, ExecutionStep, Program, Transition, TuringMachineError, - MAX_PROGRAM_SIZE, -}; +pub use types::{Direction, Program, Step, Transition, TuringMachineError, MAX_PROGRAM_SIZE}; diff --git a/src/machine.rs b/src/machine.rs index a84c6e3..08dd348 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -3,10 +3,9 @@ //! and execution of transition rules. use crate::types::{ - Direction, ExecutionResult, ExecutionStep, Program, Transition, TuringMachineError, - INPUT_BLANK_SYMBOL, + Direction, Halt, Mode, Program, Step, Transition, TuringMachineError, INPUT_BLANK_SYMBOL, + MAX_EXECUTION_STEPS, }; -use std::collections::HashMap; /// Represents a multi-tape Turing Machine. /// @@ -16,12 +15,9 @@ use std::collections::HashMap; pub struct TuringMachine { state: String, tapes: Vec>, - head_positions: Vec, - blank_symbol: char, - rules: HashMap>, - initial_state: String, - initial_tapes: Vec>, - initial_heads: Vec, + heads: Vec, + blank: char, + program: Program, step_count: usize, } @@ -35,81 +31,16 @@ impl TuringMachine { /// /// * `program` - The `Program` defining the Turing Machine. pub fn new(program: Program) -> Self { - let tapes: Vec> = program - .tapes - .iter() - .map(|tape| tape.chars().collect()) - .collect(); - Self { state: program.initial_state.clone(), - tapes: tapes.clone(), - head_positions: program.heads.clone(), - blank_symbol: program.blank, - rules: program.rules, - initial_state: program.initial_state, - initial_tapes: tapes, - initial_heads: program.heads, + tapes: program.tapes().clone(), + heads: program.heads.clone(), + blank: program.blank, + program, step_count: 0, } } - /// Returns the content of the first tape as a `Vec`. - /// This is a convenience method for single-tape compatibility. - pub fn get_tape(&self) -> Vec { - self.tapes.first().cloned().unwrap_or_default() - } - - /// Returns the head position of the first tape. - /// This is a convenience method for single-tape compatibility. - pub fn get_head_position(&self) -> usize { - self.head_positions.first().cloned().unwrap_or(0) - } - - /// Returns the symbol currently under the head of the first tape. - /// If the head is beyond the tape's current length, the blank symbol is returned. - /// This is a convenience method for single-tape compatibility. - pub fn get_current_symbol(&self) -> char { - let tape_index = 0; // First tape for single-tape compatibility - let head_pos = self.head_positions.get(tape_index).cloned().unwrap_or(0); - if let Some(tape) = self.tapes.get(tape_index) { - if head_pos < tape.len() { - tape[head_pos] - } else { - self.blank_symbol - } - } else { - self.blank_symbol - } - } - - /// Returns the content of the first tape as a `String`. - /// This is a convenience method for single-tape compatibility. - pub fn get_tape_as_string(&self) -> String { - self.get_tape().iter().collect() - } - - /// Returns a vector of symbols that have defined transitions from the current state - /// on the first tape. - /// This is a convenience method for single-tape compatibility. - pub fn get_available_transitions(&self) -> Vec { - // For single-tape compatibility, return symbols that have transitions in current state - if let Some(transitions) = self.rules.get(&self.state) { - transitions - .iter() - .filter_map(|t| { - if t.read.len() == 1 { - Some(t.read[0]) - } else { - None - } - }) - .collect() - } else { - Vec::new() - } - } - /// Executes a single step of the Turing Machine's computation. /// /// This involves reading symbols, writing new symbols, moving heads, and transitioning @@ -118,49 +49,39 @@ impl TuringMachine { /// # Returns /// /// * `ExecutionResult::Continue` if the machine successfully performs a step. - /// * `ExecutionResult::Halt` if the machine enters a halt state (no defined transitions). - /// * `ExecutionResult::Error` if an error occurs, such as no matching transition found. - pub fn step(&mut self) -> ExecutionResult { - // Check if we're in a halt state (no transitions defined) - if !self.rules.contains_key(&self.state) { - return ExecutionResult::Halt; - } - - let state_transitions = match self.rules.get(&self.state) { - Some(transitions) => transitions, - None => { - return ExecutionResult::Error(TuringMachineError::InvalidState(self.state.clone())) - } - }; - - // If no transitions are defined for this state, it's a halt state - if state_transitions.is_empty() { - return ExecutionResult::Halt; + /// * `ExecutionResult::Halt(_)` if the machine enters a halt state (no defined transitions). + pub fn step(&mut self) -> Step { + if self.is_halted() { + return Step::Halt(Halt::Ok); } // Ensure all tapes are large enough - for (i, head_pos) in self.head_positions.iter().enumerate() { + for (i, head_pos) in self.heads.iter().enumerate() { if *head_pos >= self.tapes[i].len() { - self.tapes[i].resize(*head_pos + 1, self.blank_symbol); + self.tapes[i].resize(*head_pos + 1, self.blank); } } // Find matching transition - let transition = match self.get_current_transition().cloned() { + let transition = match self.transition().cloned() { Some(t) => t, None => { - return ExecutionResult::Error(TuringMachineError::NoMultiTapeTransition { - state: self.state.clone(), - symbols: self.get_current_symbols(), - }); + // No transition found for the current symbols. + return match self.program.mode { + Mode::Normal => Step::Halt(Halt::Ok), + Mode::Strict => Step::Halt(Halt::Err(TuringMachineError::UndefinedTransition( + self.state.clone(), + self.symbols(), + ))), + }; } }; // Apply transition to all tapes for i in 0..self.tapes.len() { // Write new symbol - self.tapes[i][self.head_positions[i]] = if transition.write[i] == INPUT_BLANK_SYMBOL { - self.blank_symbol + self.tapes[i][self.heads[i]] = if transition.write[i] == INPUT_BLANK_SYMBOL { + self.blank } else { transition.write[i] }; @@ -168,17 +89,17 @@ impl TuringMachine { // Move head according to direction match transition.directions[i] { Direction::Left => { - if self.head_positions[i] == 0 { + if self.heads[i] == 0 { // Extend tape to the left - self.tapes[i].insert(0, self.blank_symbol); + self.tapes[i].insert(0, self.blank); } else { - self.head_positions[i] -= 1; + self.heads[i] -= 1; } } Direction::Right => { - self.head_positions[i] += 1; - if self.head_positions[i] >= self.tapes[i].len() { - self.tapes[i].push(self.blank_symbol); + self.heads[i] += 1; + if self.heads[i] >= self.tapes[i].len() { + self.tapes[i].push(self.blank); } } Direction::Stay => { @@ -190,49 +111,19 @@ impl TuringMachine { self.state = transition.next_state.clone(); self.step_count += 1; - ExecutionResult::Continue + Step::Continue } /// Runs the Turing Machine until it halts or reaches a maximum step count. - /// - /// This method records each `ExecutionStep` taken by the machine. - /// - /// # Returns - /// - /// * `Vec` - A vector of `ExecutionStep`s representing the computation history. - pub fn run_to_completion(&mut self) -> Vec { - let mut steps = Vec::new(); - let max_steps = 10000; // Prevent infinite loops - - for _ in 0..max_steps { - let step = ExecutionStep { - state: self.state.clone(), - tapes: self.tapes.clone(), - head_positions: self.head_positions.clone(), - symbols_read: self - .head_positions - .iter() - .enumerate() - .map(|(i, &pos)| { - if pos < self.tapes[i].len() { - self.tapes[i][pos] - } else { - self.blank_symbol - } - }) - .collect(), - transition: None, // Could be enhanced to include the transition taken - }; - steps.push(step); - + pub fn run(&mut self) -> Step { + for _ in 0..MAX_EXECUTION_STEPS { match self.step() { - ExecutionResult::Continue => continue, - ExecutionResult::Halt => break, - ExecutionResult::Error(_) => break, + Step::Continue => continue, + halt => return halt, } } - steps + Step::Halt(Halt::Ok) } /// Returns the current state of the Turing Machine. @@ -242,15 +133,15 @@ impl TuringMachine { /// Returns the initial state of the Turing Machine. pub fn initial_state(&self) -> &str { - &self.initial_state + &self.program.initial_state } /// Resets the Turing Machine to its initial configuration. /// This includes resetting the state, tapes, head positions, and step count. pub fn reset(&mut self) { - self.state = self.initial_state.clone(); - self.tapes = self.initial_tapes.clone(); - self.head_positions = self.initial_heads.clone(); + self.state = self.program.initial_state.clone(); + self.tapes = self.program.tapes().clone(); + self.heads = self.program.heads.clone(); self.step_count = 0; } @@ -262,11 +153,10 @@ impl TuringMachine { /// Checks if the Turing Machine is currently in a halted state. /// A machine is halted if there are no defined transitions for its current state. pub fn is_halted(&self) -> bool { - !self.rules.contains_key(&self.state) - || self - .rules - .get(&self.state) - .is_none_or(|transitions| transitions.is_empty()) + self.program + .rules + .get(&self.state) + .is_none_or(|transitions| transitions.is_empty()) } /// Returns a slice of the machine's tapes. @@ -275,29 +165,27 @@ impl TuringMachine { } /// Returns a slice of the machine's head positions for all tapes. - pub fn head_positions(&self) -> &[usize] { - &self.head_positions - } - - /// Returns the content of all tapes as a vector of `String`s. - pub fn get_tapes_as_strings(&self) -> Vec { - self.tapes - .iter() - .map(|tape| tape.iter().collect()) - .collect() + pub fn heads(&self) -> &[usize] { + &self.heads } /// Returns a vector of symbols currently under each tape's head. /// If a head is beyond its tape's current length, the blank symbol is returned for that tape. - pub fn get_current_symbols(&self) -> Vec { - self.head_positions + /// + /// | a | b | c | tape 1 + /// | d | e | | tape 2 + /// 0 1 2 index + /// + /// heads [0, 2] will return ['a', '_'] + pub fn symbols(&self) -> Vec { + self.heads .iter() .enumerate() .map(|(i, &pos)| { if pos < self.tapes[i].len() { self.tapes[i][pos] } else { - self.blank_symbol + self.blank } }) .collect() @@ -313,23 +201,25 @@ impl TuringMachine { /// /// * `Some(&Transition)` if a matching transition is found. /// * `None` if no matching transition exists. - pub fn get_current_transition(&self) -> Option<&Transition> { - match self.rules.get(&self.state) { + pub fn transition(&self) -> Option<&Transition> { + match self.program.rules.get(&self.state) { Some(transitions) => { - let symbols = self.get_current_symbols(); + let symbols = self.symbols(); transitions.iter().find(|t| { if t.read.len() != symbols.len() { return false; } - for (i, &symbol) in t.read.iter().enumerate() { + for (i, &read) in t.read.iter().enumerate() { // If the transition rule specifies `INPUT_BLANK_SYMBOL`, it matches the program's blank symbol - if symbol == INPUT_BLANK_SYMBOL { - if symbols[i] != self.blank_symbol { - return false; - } - } else if symbol != symbols[i] { + let expected = if read == INPUT_BLANK_SYMBOL { + self.blank + } else { + read + }; + + if symbols[i] != expected { return false; } } @@ -342,8 +232,8 @@ impl TuringMachine { } /// Returns the blank symbol used by this Turing Machine. - pub fn blank_symbol(&self) -> char { - self.blank_symbol + pub fn blank(&self) -> char { + self.blank } /// Sets the content of a specific tape. @@ -374,7 +264,7 @@ impl TuringMachine { .chars() .map(|c| { if c == INPUT_BLANK_SYMBOL { - self.blank_symbol + self.blank } else { c } @@ -413,7 +303,7 @@ impl TuringMachine { #[cfg(test)] mod multi_tape_tests { use super::*; - use crate::types::{Direction, Program, Transition}; + use crate::types::{Direction, Halt, Mode, Program, Transition}; use std::collections::HashMap; fn create_simple_multi_tape_program() -> Program { @@ -435,6 +325,7 @@ mod multi_tape_tests { Program { name: "Simple Multi-Tape Test".to_string(), + mode: Mode::default(), initial_state: "start".to_string(), tapes: vec!["a".to_string(), "x".to_string()], heads: vec![0, 0], @@ -450,7 +341,7 @@ mod multi_tape_tests { assert_eq!(machine.state(), "start"); assert_eq!(machine.tapes(), &[vec!['a'], vec!['x']]); - assert_eq!(machine.head_positions(), &[0, 0]); + assert_eq!(machine.heads(), &[0, 0]); assert_eq!(machine.step_count(), 0); } @@ -461,10 +352,10 @@ mod multi_tape_tests { let result = machine.step(); - assert_eq!(result, ExecutionResult::Continue); + assert_eq!(result, Step::Continue); assert_eq!(machine.state(), "halt"); assert_eq!(machine.tapes(), &[vec!['b', '-'], vec!['y', '-']]); // Tapes extended when moving right - assert_eq!(machine.head_positions(), &[1, 1]); + assert_eq!(machine.heads(), &[1, 1]); assert_eq!(machine.step_count(), 1); } @@ -475,16 +366,18 @@ mod multi_tape_tests { // First step should continue let result1 = machine.step(); - assert_eq!(result1, ExecutionResult::Continue); + assert_eq!(result1, Step::Continue); // Second step should halt (no transitions in halt state) let result2 = machine.step(); - assert_eq!(result2, ExecutionResult::Halt); + assert_eq!(result2, Step::Halt(Halt::Ok)); } #[test] - fn test_multi_tape_no_transition_error() { - let program = create_simple_multi_tape_program(); + fn test_multi_tape_rejection() { + let mut program = create_simple_multi_tape_program(); + program.mode = Mode::Strict; + let mut machine = TuringMachine::new(program); // Manually set tapes to symbols that have no transition @@ -495,14 +388,11 @@ mod multi_tape_tests { let result = machine.step(); match result { - ExecutionResult::Error(TuringMachineError::NoMultiTapeTransition { - state, - symbols, - }) => { + Step::Halt(Halt::Err(TuringMachineError::UndefinedTransition(state, symbols))) => { assert_eq!(state, "start"); assert_eq!(symbols, vec!['z', 'z']); } - _ => panic!("Expected NoMultiTapeTransition error"), + _ => panic!("Expected a Rejection result, but got {:?}", result), } } @@ -520,7 +410,7 @@ mod multi_tape_tests { machine.reset(); assert_eq!(machine.state(), "start"); assert_eq!(machine.tapes(), &[vec!['a'], vec!['x']]); - assert_eq!(machine.head_positions(), &[0, 0]); + assert_eq!(machine.heads(), &[0, 0]); assert_eq!(machine.step_count(), 0); } @@ -529,12 +419,8 @@ mod multi_tape_tests { let program = create_simple_multi_tape_program(); let mut machine = TuringMachine::new(program); - let steps = machine.run_to_completion(); - - // Should have recorded the initial state and the state after the step - assert_eq!(steps.len(), 2); - assert_eq!(steps[0].state, "start"); - assert_eq!(steps[1].state, "halt"); + let step = machine.run(); + assert_eq!(step, Step::Halt(Halt::Ok)); } #[test] @@ -553,18 +439,7 @@ mod multi_tape_tests { let program = create_simple_multi_tape_program(); let machine = TuringMachine::new(program); - assert_eq!(machine.get_current_symbols(), vec!['a', 'x']); - } - - #[test] - fn test_multi_tape_get_tapes_as_strings() { - let program = create_simple_multi_tape_program(); - let machine = TuringMachine::new(program); - - assert_eq!( - machine.get_tapes_as_strings(), - vec!["a".to_string(), "x".to_string()] - ); + assert_eq!(machine.symbols(), vec!['a', 'x']); } #[test] @@ -585,6 +460,7 @@ mod multi_tape_tests { let program = Program { name: "Stay Direction Test".to_string(), + mode: Mode::default(), initial_state: "start".to_string(), tapes: vec!["a".to_string(), "x".to_string()], heads: vec![0, 0], @@ -596,7 +472,7 @@ mod multi_tape_tests { machine.step(); // First head should stay at position 0, second head should move right - assert_eq!(machine.head_positions(), &[0, 1]); + assert_eq!(machine.heads(), &[0, 1]); assert_eq!(machine.tapes(), &[vec!['b'], vec!['y', '-']]); } diff --git a/src/parser.rs b/src/parser.rs index db42228..294d780 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,7 +4,7 @@ use crate::{ analyzer::analyze, types::{ - Direction, Program, Transition, TuringMachineError, DEFAULT_BLANK_SYMBOL, + Direction, Mode, Program, Transition, TuringMachineError, DEFAULT_BLANK_SYMBOL, INPUT_BLANK_SYMBOL, }, }; @@ -59,6 +59,7 @@ pub fn parse(input: &str) -> Result { /// It also performs initial validation checks for uniqueness and consistency of sections. fn parse_program(pair: Pair) -> Result { let mut name: Option = None; + let mut mode: Option = None; let mut tapes: Option<(Vec, Vec>)> = None; let mut heads: Option> = None; let mut blank: Option = None; @@ -75,6 +76,7 @@ fn parse_program(pair: Pair) -> Result { match rule { Rule::name => name = Some(parse_inner_string(p)), + Rule::mode => mode = Some(parse_mode(p)?), Rule::blank => blank = Some(parse_symbol(&parse_inner_string(p))), Rule::rules => rules = Some(parse_transitions(p, &mut initial_state)?), Rule::tape | Rule::tapes => { @@ -91,6 +93,7 @@ fn parse_program(pair: Pair) -> Result { // Handle mandatory checks let name = check_required_rule(name, vec!["name"])?; + let mode = mode.unwrap_or_default(); let rules = check_required_rule(rules, vec!["rules"])?; let initial_state = check_required_rule(initial_state, vec!["initial_state"])?; let tapes = check_required_rule(tapes, vec!["tape", "tapes"])?; @@ -104,6 +107,7 @@ fn parse_program(pair: Pair) -> Result { Ok(Program { name, + mode, tapes: tapes .into_iter() .map(|tape| tape.into_iter().collect()) @@ -115,6 +119,18 @@ fn parse_program(pair: Pair) -> Result { }) } +fn parse_mode(pair: Pair<'_, Rule>) -> Result { + let span = pair.as_span(); + match parse_inner_string(pair).as_str() { + "normal" => Ok(Mode::Normal), + "strict" => Ok(Mode::Strict), + mode => Err(parse_error( + &format!("Invalid mode: {mode}. Expected 'normal' or 'strict'",), + span, + )), + } +} + /// Parses tape definitions from a `Pair` or `Pair`. /// /// It extracts the symbols for each tape and records the positions of any `INPUT_BLANK_SYMBOL`s @@ -224,14 +240,7 @@ fn parse_multi_tape_transition( for inner in p.into_inner() { match inner.as_rule() { Rule::single_tape_action => { - // Convert single tape action to multi-tape format - let action = parse_single_tape_action(inner)?; - actions.push(Transition { - read: vec![action.read], - write: vec![action.write], - directions: vec![action.direction], - next_state: action.next, - }); + actions.push(parse_single_tape_action(inner)?); } Rule::multi_tape_action => { actions.push(parse_multi_tape_action(inner)?); @@ -248,7 +257,7 @@ fn parse_multi_tape_transition( /// Parses a single-tape action from a `Pair`. /// /// It extracts the read symbol, write symbol (defaults to read if omitted), direction, and next state. -fn parse_single_tape_action(pair: Pair) -> Result { +fn parse_single_tape_action(pair: Pair) -> Result { let mut pairs = pair.into_inner(); let read = parse_symbol_from_pairs(&mut pairs); @@ -259,13 +268,13 @@ fn parse_single_tape_action(pair: Pair) -> Result) -> Result read.clone(), + _ => parse_multi_tape_symbols(pairs.next().unwrap())?, + }; // Parse directions let directions = parse_directions(pairs.next().unwrap())?; @@ -290,12 +302,12 @@ fn parse_multi_tape_action(pair: Pair) -> Result) -> Result) -> String { .join(" or ") } -/// A helper struct to temporarily hold parsed single-tape action data. -struct ParsedAction { - read: char, - write: char, - direction: Direction, - next: String, -} - #[cfg(test)] mod tests { use super::*; @@ -497,6 +502,7 @@ rules: let program = result.unwrap(); assert_eq!(program.name, "Simple Test"); + assert_eq!(program.mode, Mode::Normal); assert_eq!(program.initial_tape(), "a"); assert!(program.rules.contains_key("start")); assert!(program.rules.contains_key("halt")); @@ -521,6 +527,7 @@ rules: let program = result.unwrap(); assert_eq!(program.name, "Simple Multi-Tape"); + assert_eq!(program.mode, Mode::Normal); assert_eq!(program.tapes, vec!["a", "d"]); assert_eq!( program.rules["start"][0], @@ -568,6 +575,37 @@ rules: assert!(matches!(error, TuringMachineError::ParseError(_))); } + #[test] + fn test_parse_strict_mode() { + let input = r#" +name: Simple Test +mode: strict +tape: a +rules: + start: + a -> b, R, other +"#; + let program = parse(input).unwrap(); + assert_eq!(program.mode, Mode::Strict); + } + + #[test] + fn test_parse_invalid_mode() { + let input = r#" +name: Simple +mode: invalid +tape: a +rules: + start: + a -> b, R, halt +"#; + let result = parse(input); + assert!(result.is_err()); + let error = result.unwrap_err(); + assert!(matches!(error, TuringMachineError::ParseError(_))); + assert!(error.to_string().contains("Invalid mode: invalid"),); + } + #[test] fn test_parse_missing_tape() { let input = r#" diff --git a/src/programs.rs b/src/programs.rs index 9ac8b6c..f40e5bb 100644 --- a/src/programs.rs +++ b/src/programs.rs @@ -3,10 +3,11 @@ use crate::types::{Program, TuringMachineError}; use std::sync::RwLock; // Default embedded programs -const PROGRAM_TEXTS: [&str; 8] = [ +const PROGRAM_TEXTS: [&str; 9] = [ include_str!("../examples/binary-addition.tur"), - include_str!("../examples/palindrome.tur"), + include_str!("../examples/even-zeros-and-ones.tur"), include_str!("../examples/event-number-checker.tur"), + include_str!("../examples/palindrome.tur"), include_str!("../examples/subtraction.tur"), include_str!("../examples/busy-beaver-3.tur"), include_str!("../examples/multi-tape-copy.tur"), @@ -47,7 +48,7 @@ impl ProgramManager { } /// Get the number of available programs - pub fn get_program_count() -> usize { + pub fn count() -> usize { // Initialize with default programs if not already initialized let _ = Self::load(); @@ -168,6 +169,7 @@ mod tests { use super::*; use crate::analyze; use crate::machine::TuringMachine; + use crate::types::Step; use std::fs::File; use std::io::Write; use tempfile::tempdir; @@ -179,7 +181,7 @@ mod tests { assert!(result.is_ok()); // Check that we have the expected number of programs - assert!(ProgramManager::get_program_count() >= 4); + assert!(ProgramManager::count() >= 4); } #[test] @@ -218,7 +220,7 @@ rules: // Initialize with default programs let _ = ProgramManager::load(); - let count = ProgramManager::get_program_count(); + let count = ProgramManager::count(); for i in 0..count { let program = ProgramManager::get_program_by_index(i).unwrap(); assert!( @@ -245,20 +247,16 @@ rules: // Initialize with default programs let _ = ProgramManager::load(); - let count = ProgramManager::get_program_count(); + let count = ProgramManager::count(); for i in 0..count { let program = ProgramManager::get_program_by_index(i).unwrap(); - let program_name = program.name.clone(); let mut machine = TuringMachine::new(program); let result = machine.step(); - // Should either continue or halt, but not error on first step + // Should either continue or halt, but not error or reject on first step match result { - crate::types::ExecutionResult::Continue => {} - crate::types::ExecutionResult::Halt => {} - crate::types::ExecutionResult::Error(e) => { - panic!("Program '{}' failed on first step: {}", program_name, e); - } + Step::Continue => {} + Step::Halt(_) => {} } } } diff --git a/src/types.rs b/src/types.rs index 7bafa42..5cd3740 100644 --- a/src/types.rs +++ b/src/types.rs @@ -13,6 +13,8 @@ pub const DEFAULT_BLANK_SYMBOL: char = ' '; pub const INPUT_BLANK_SYMBOL: char = '_'; /// The maximum allowed size for a Turing Machine program in bytes. pub const MAX_PROGRAM_SIZE: usize = 65536; // 64KB +/// The maximum number of steps to execute before halting. +pub const MAX_EXECUTION_STEPS: usize = 10000; /// Represents a Turing Machine program, supporting both single and multi-tape configurations. /// @@ -21,6 +23,8 @@ pub const MAX_PROGRAM_SIZE: usize = 65536; // 64KB pub struct Program { /// The name of the Turing Machine program. pub name: String, + /// Execution mode of the simulator. + pub mode: Mode, /// The initial state of the Turing Machine. pub initial_state: String, /// A vector of strings, where each string represents the initial content of a tape. @@ -34,6 +38,20 @@ pub struct Program { pub rules: HashMap>, } +/// The execution mode for a Turing Machine program. +/// +/// Controls how the simulator handles undefined transitions: +/// - `Normal` (default): undefined transitions halt the machine normally (faithful to TM theory). +/// - `Strict`: undefined transitions trigger an error, useful for debugging or catching missing rules. +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] +pub enum Mode { + /// Undefined transitions halt normally. + #[default] + Normal, + /// Undefined transitions are treated as errors. + Strict, +} + impl Program { /// Returns the initial content of the first tape as a `String`. /// This is a convenience method for single-tape compatibility. @@ -51,6 +69,13 @@ impl Program { pub fn is_single_tape(&self) -> bool { self.tapes.len() == 1 } + + pub fn tapes(&self) -> Vec> { + self.tapes + .iter() + .map(|tape| tape.chars().collect()) + .collect() + } } /// Represents a single transition rule for a Turing Machine. @@ -80,56 +105,28 @@ pub enum Direction { Stay, } -/// Represents a single step in the execution of a Turing Machine. -/// -/// This struct captures the machine's state, tape contents, head positions, -/// and symbols read at a particular point in time during execution. -#[derive(Debug, Clone)] -pub struct ExecutionStep { - /// The state of the Turing Machine at this step. - pub state: String, - /// The content of all tapes at this step. - pub tapes: Vec>, - /// The head positions for all tapes at this step. - pub head_positions: Vec, - /// The symbols read from each tape at this step. - pub symbols_read: Vec, - /// The transition rule that was applied to reach this step (optional). - pub transition: Option, -} - -impl ExecutionStep { - /// Returns the content of the first tape as a `Vec`. - /// This is a convenience method for single-tape compatibility. - pub fn tape(&self) -> Vec { - self.tapes.first().cloned().unwrap_or_default() - } - - /// Returns the head position of the first tape. - /// This is a convenience method for single-tape compatibility. - pub fn head_position(&self) -> usize { - self.head_positions.first().cloned().unwrap_or(0) - } - - /// Returns the symbol read from the first tape. - /// This is a convenience method for single-tape compatibility. - pub fn symbol_read(&self) -> char { - self.symbols_read - .first() - .cloned() - .unwrap_or(DEFAULT_BLANK_SYMBOL) - } -} - /// Represents the outcome of a Turing Machine execution step. #[derive(Debug, Clone, PartialEq)] -pub enum ExecutionResult { +pub enum Step { /// The machine successfully performed a step and continues execution. Continue, /// The machine has halted (reached a state with no outgoing transitions). - Halt, - /// An error occurred during execution. - Error(TuringMachineError), + Halt(Halt), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Halt { + /// Halted in a specific state (no outgoing transitions). + Ok, + + Err(TuringMachineError), +} + +/// Details of a rejection outcome. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Rejection { + pub state: String, + pub symbols: Vec, } /// Represents various errors that can occur during Turing Machine operations. @@ -138,12 +135,9 @@ pub enum TuringMachineError { /// Indicates an attempt to transition to an invalid or undefined state. #[error("Invalid state: {0}")] InvalidState(String), - /// Indicates that no transition rule was found for the current state and symbol on a single tape. - #[error("No transition defined for state {state} and symbol '{symbol}'")] - NoTransition { state: String, symbol: char }, - /// Indicates that no transition rule was found for the current state and symbols on multiple tapes. - #[error("No transition defined for state {state} and symbols {symbols:?}")] - NoMultiTapeTransition { state: String, symbols: Vec }, + /// Indicates that there's no rule defined for a particular set of symbols. + #[error("No rule defined for state {0} and symbols {1:?}")] + UndefinedTransition(String, Vec), /// Indicates that a tape head attempted to move beyond the defined tape boundaries. #[error("Tape boundary exceeded")] TapeBoundary, @@ -196,14 +190,10 @@ mod tests { #[test] fn test_error_display() { - let error = TuringMachineError::NoTransition { - state: "q0".to_string(), - symbol: 'a', - }; + let error = TuringMachineError::InvalidState("q0".to_string()); let error_msg = format!("{}", error); - assert!(error_msg.contains("No transition defined")); + assert!(error_msg.contains("Invalid state")); assert!(error_msg.contains("q0")); - assert!(error_msg.contains("'a'")); } }