Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ state:

```bash
# Run a program
cargo run --package tur-cli -- -p examples/binary-addition.tur
cargo run -p tur-cli -- examples/binary-addition.tur

# Pipe input to a program
echo '$011-' | cargo run -p tur-cli -- examples/binary-addition.tur

# Chaining programs with pipes
echo '$011' | cargo run -p tur-cli -- examples/binary-addition.tur | cargo run -p tur-cli -- examples/binary-addition.tur
```

### Terminal User Interface (TUI)
Expand Down
5 changes: 2 additions & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The `.tur` file format uses a structured syntax parsed by a Pest grammar with co
- **Name**: Specified with `name:` followed by the program name
- **Tape Configuration**:
- **Single-tape**: `tape: symbol1, symbol2, symbol3`
- **Multi-tape**:
- **Multi-tape**:
```tur
tapes:
[a, b, c]
Expand All @@ -47,7 +47,7 @@ The `.tur` file format uses a structured syntax parsed by a Pest grammar with co
- **Write-only transitions**: If `-> new_symbol` is omitted, the read symbol is preserved
- The first state defined is automatically the initial state
- **Comments**: Use `#` for line comments and inline comments
- **Special Symbols**:
- **Special Symbols**:
- `_` represents the blank symbol in program definitions
- Any Unicode character can be used as tape symbols
- The blank symbol can be customized with the `blank:` directive
Expand All @@ -56,7 +56,6 @@ The `.tur` file format uses a structured syntax parsed by a Pest grammar with co

### Single-Tape Programs
- **binary-addition.tur**: Adds two binary numbers
- **binary-counter.tur**: Increments a binary number
- **busy-beaver-3.tur**: Classic 3-state busy beaver
- **event-number-checker.tur**: Checks if a number is even
- **palindrome.tur**: Checks if input is a palindrome
Expand Down
7 changes: 5 additions & 2 deletions examples/binary-addition.tur
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
name: Binary addition
tape: $, 0, 0, 1, 1, 1, -
tape: $, 0, 0, 1, 1, 1
rules:
start:
$ -> $, R, s1
s1:
0 -> 0, R, s1
1 -> 1, R, s1
- -> -, L, s2
_ -> _, L, s2
s2:
1 -> 0, L, s2
0 -> 1, L, stop
$ -> 1, L, s3
s3:
_ -> $, R, stop
stop:
12 changes: 0 additions & 12 deletions examples/binary-counter.tur

This file was deleted.

3 changes: 1 addition & 2 deletions platforms/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use tur::ExecutionResult;
#[clap(author, version, about, long_about = None, arg_required_else_help = true)]
struct Cli {
/// The Turing machine program file to execute
#[clap(short, long)]
program: String,

/// The input to the Turing machine
Expand Down Expand Up @@ -111,7 +110,7 @@ fn read_tape_inputs(inputs: &[String]) -> Result<Vec<String>, String> {

for line in stdin.lock().lines() {
match line {
Ok(content) => tape_inputs.push(content),
Ok(content) => tape_inputs.push(content.trim().to_string()),
Err(e) => return Err(format!("Error reading from stdin: {}", e)),
}
}
Expand Down
4 changes: 4 additions & 0 deletions platforms/web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,10 @@ body {
border-color: var(--danger-color);
}

.program-status.error pre {
white-space: pre-wrap;
}

.program-status svg {
height: 1.25rem;
width: 1.25rem;
Expand Down
208 changes: 162 additions & 46 deletions src/analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub enum AnalysisError {
UnreachableStates(Vec<String>),
/// Indicates that the initial tape contains symbols for which no transitions are defined.
InvalidTapeSymbols(Vec<char>),
/// Indicates structural problems with the program (empty tapes, mismatched head positions, etc.).
StructuralError(String),
}

impl From<AnalysisError> for TuringMachineError {
Expand Down Expand Up @@ -48,13 +50,16 @@ impl From<AnalysisError> for TuringMachineError {
symbols
))
}
AnalysisError::StructuralError(msg) => TuringMachineError::ValidationError(msg),
}
}
}

/// Analyzes a given Turing Machine `Program` for common errors and inconsistencies.
/// Analyzes a given Turing Machine `Program` for structural and logical errors.
///
/// This function orchestrates a series of checks, collecting all detected `AnalysisError`s.
/// This function orchestrates a comprehensive series of checks, performing both
/// structural validation (basic consistency) and logical analysis (reachability,
/// symbol handling, etc.).
///
/// # Arguments
///
Expand All @@ -63,13 +68,12 @@ impl From<AnalysisError> for TuringMachineError {
/// # Returns
///
/// * `Ok(())` if no errors are found.
/// * `Err(Vec<AnalysisError>)` if one or more errors are detected, containing a list of all errors.
pub fn analyze(program: &Program) -> Result<(), Vec<AnalysisError>> {
/// * `Err(TuringMachineError::ValidationError)` if any validation rule is violated.
pub fn analyze(program: &Program) -> Result<(), TuringMachineError> {
let errors = [
check_structure,
check_head,
check_valid_start_state,
check_valid_stop_states,
check_undefined_next_states,
check_unreachable_states,
check_tape_symbols,
]
Expand All @@ -78,10 +82,63 @@ pub fn analyze(program: &Program) -> Result<(), Vec<AnalysisError>> {
.collect::<Vec<_>>();

if !errors.is_empty() {
Err(errors)
} else {
Ok(())
// Return the first error
if let Some(first_error) = errors.first() {
return Err((*first_error).clone().into());
}
}

Ok(())
}

/// Checks basic structural requirements of the program.
///
/// This validates fundamental structural consistency like:
/// - Tapes are defined (non-empty)
/// - Head positions match number of tapes
/// - Transitions have consistent tape counts
///
/// # Arguments
///
/// * `program` - A reference to the `Program` to check.
///
/// # Returns
///
/// * `Ok(())` if the structure is valid.
/// * `Err(AnalysisError::StructuralError)` if structural issues are found.
fn check_structure(program: &Program) -> Result<(), AnalysisError> {
// Check for empty tapes
if program.tapes.is_empty() {
return Err(AnalysisError::StructuralError(
"No tapes defined".to_string(),
));
}

// Check that head positions match number of tapes
if program.heads.len() != program.tapes.len() {
return Err(AnalysisError::StructuralError(format!(
"Number of head positions ({}) does not match number of tapes ({})",
program.heads.len(),
program.tapes.len()
)));
}

// Check that all transitions have consistent tape counts
for (state, transitions) in &program.rules {
for transition in transitions {
if transition.read.len() != program.tapes.len()
|| transition.write.len() != program.tapes.len()
|| transition.directions.len() != program.tapes.len()
{
return Err(AnalysisError::StructuralError(format!(
"Transition in state '{}' has inconsistent tape counts",
state
)));
}
}
}

Ok(())
}

/// Checks if the initial head position(s) are valid for the program's tape(s).
Expand All @@ -99,25 +156,15 @@ pub fn analyze(program: &Program) -> Result<(), Vec<AnalysisError>> {
/// * `Ok(())` if the head position(s) are valid.
/// * `Err(AnalysisError::InvalidHead)` if an invalid head position is found.
fn check_head(program: &Program) -> Result<(), AnalysisError> {
// For single-tape programs, check the head position
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 multi-tape programs, check all head positions
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(())
}
program
.heads
.iter()
.zip(&program.tapes)
.find_map(|(&head_pos, tape)| {
(head_pos >= tape.len() && !tape.is_empty())
.then_some(AnalysisError::InvalidHead(head_pos))
})
.map_or(Ok(()), Err)
}

/// Checks whether the initial state is defined as a source state in any of the transition rules.
Expand Down Expand Up @@ -154,6 +201,7 @@ fn check_valid_start_state(program: &Program) -> Result<(), AnalysisError> {
///
/// * `Ok(())` if all stop states are properly referenced or if there are no unreferenced stop states.
/// * `Err(AnalysisError::StopStatesNotFound)` if stop states are found that are not referenced.
#[allow(dead_code)]
fn check_valid_stop_states(program: &Program) -> Result<(), AnalysisError> {
// Collect all states that have no outgoing transitions (potential stop states)
let stop_states: HashSet<String> = program
Expand Down Expand Up @@ -196,6 +244,7 @@ fn check_valid_stop_states(program: &Program) -> Result<(), AnalysisError> {
///
/// * `Ok(())` if all next states are defined or are the "halt" state.
/// * `Err(AnalysisError::UndefinedNextStates)` if transitions reference undefined states.
#[allow(dead_code)]
fn check_undefined_next_states(program: &Program) -> Result<(), AnalysisError> {
let defined_states: HashSet<String> = program.rules.keys().cloned().collect();

Expand Down Expand Up @@ -628,25 +677,21 @@ mod tests {
let result = analyze(&program);

assert!(result.is_err());
let errors = result.unwrap_err();

// Should have at least 3 errors: undefined next state, unreachable state, and invalid tape symbol
assert!(errors.len() >= 3);

// Check for specific error types
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");
// Since analyze() now returns the first error found, we just check that an error occurred
if let Err(TuringMachineError::ValidationError(msg)) = result {
// Should contain one of the expected error types
let has_error = msg.contains("No tapes defined")
|| msg.contains("Number of head positions")
|| msg.contains("Invalid start state")
|| msg.contains("Stop states not found")
|| msg.contains("undefined state")
|| msg.contains("Unreachable states")
|| msg.contains("not handled by any transition");
assert!(has_error, "Expected a validation error, got: {}", msg);
} else {
panic!("Expected ValidationError");
}
}

#[test]
Expand Down Expand Up @@ -682,4 +727,75 @@ mod tests {

assert!(result.is_ok());
}

#[test]
fn test_analyze_success() {
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 = analyze(&program);
assert!(result.is_ok());
}

#[test]
fn test_analyze_structural_error() {
let mut rules = HashMap::new();
// Initial state "start" is not defined in rules
rules.insert("other".to_string(), Vec::new());

let program = create_test_program("start", "a", rules);
let result = analyze(&program);

assert!(result.is_err());
if let Err(TuringMachineError::ValidationError(msg)) = result {
assert!(msg.contains("Invalid start state: start"));
} else {
panic!("Expected ValidationError");
}
}

#[test]
fn test_analyze_empty_tapes() {
let mut rules = HashMap::new();
rules.insert("start".to_string(), Vec::new());

let mut program = create_test_program("start", "a", rules);
program.tapes.clear(); // Remove all tapes

let result = analyze(&program);

assert!(result.is_err());
if let Err(TuringMachineError::ValidationError(msg)) = result {
assert!(msg.contains("No tapes defined"));
} else {
panic!("Expected ValidationError");
}
}

#[test]
fn test_analyze_inconsistent_head_positions() {
let mut rules = HashMap::new();
rules.insert("start".to_string(), Vec::new());

let mut program = create_test_program("start", "a", rules);
program.heads.push(1); // Add extra head position

let result = analyze(&program);

assert!(result.is_err());
if let Err(TuringMachineError::ValidationError(msg)) = result {
assert!(msg.contains("Number of head positions"));
} else {
panic!("Expected ValidationError");
}
}
}
Loading