tur 0.1.0

Turing Machine Language - Parser, interpreter, and execution engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
//! This module provides the parser for Turing Machine programs, utilizing the `pest` crate.
//! It defines the grammar for `.tur` files and functions to parse the input into a `Program` struct.

use crate::types::{
    Direction, Program, Transition, TuringMachineError, DEFAULT_BLANK_SYMBOL, INPUT_BLANK_SYMBOL,
};
use pest::{
    error::{Error, ErrorVariant},
    iterators::{Pair, Pairs},
    Parser as PestParser, Span,
};
use pest_derive::Parser as PestParser;
use std::collections::{HashMap, HashSet};

type Tape = Vec<char>;

/// Derives a `PestParser` for the Turing Machine grammar defined in `grammar.pest`.
#[derive(PestParser)]
#[grammar = "grammar.pest"]
pub struct TuringMachineParser;

/// Parses the given input string into a `Program` struct.
///
/// This is the main entry point for parsing Turing Machine program definitions.
/// It trims the input, parses it using the `TuringMachineParser`, and then processes
/// the resulting parse tree into a structured `Program`.
///
/// # Arguments
///
/// * `input` - A string slice containing the Turing Machine program definition.
///
/// # Returns
///
/// * `Ok(Program)` if the input is successfully parsed into a `Program`.
/// * `Err(TuringMachineError::ParseError)` if there are any syntax errors or structural issues.
pub fn parse(input: &str) -> Result<Program, TuringMachineError> {
    let root = TuringMachineParser::parse(Rule::program, input.trim())
        .map_err(|e| TuringMachineError::ParseError(e.into()))? //
        .next()
        .unwrap();

    parse_program(root)
}

/// Parses the top-level structure of a Turing Machine program from a `Pair<Rule::program>`.
///
/// This function extracts the program's name, tapes, heads, blank symbol, rules, and initial state.
/// It also performs initial validation checks for uniqueness and consistency of sections.
fn parse_program(pair: Pair<Rule>) -> Result<Program, TuringMachineError> {
    let mut name: Option<String> = None;
    let mut tapes: Option<(Vec<Tape>, Vec<Vec<usize>>)> = None;
    let mut heads: Option<Vec<usize>> = None;
    let mut blank: Option<char> = None;
    let mut rules: Option<HashMap<String, Vec<Transition>>> = None;
    let mut initial_state: Option<String> = None;
    let mut seen = HashSet::new();

    // Parse top-level rules
    for p in pair.into_inner() {
        let span = p.as_span();
        let rule = p.as_rule();

        check_unique_rule(rule, span, &mut seen)?;

        match rule {
            Rule::name => name = Some(parse_inner_string(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 => {
                check_exclusive_rule(tapes, vec!["tape", "tapes"], span)?;
                tapes = Some(parse_tapes(p));
            }
            Rule::head | Rule::heads => {
                check_exclusive_rule(heads, vec!["head", "heads"], span)?;
                heads = Some(parse_heads(p));
            }
            _ => {} // Skip other rules
        }
    }

    // Handle mandatory checks
    let name = check_required_rule(name, vec!["name"])?;
    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"])?;
    let blank = blank.unwrap_or(DEFAULT_BLANK_SYMBOL);

    // Rewrite blank symbol
    let tapes = rewrite_tapes(tapes, blank);
    let heads = heads.unwrap_or_else(|| vec![0; tapes.len()]);

    check_head_tape_consistency(&heads, &tapes)?;

    Ok(Program {
        name,
        tapes: tapes
            .into_iter()
            .map(|tape| tape.into_iter().collect())
            .collect(),
        heads,
        blank,
        rules,
        initial_state,
    })
}

/// Parses tape definitions from a `Pair<Rule::tape>` or `Pair<Rule::tapes>`.
///
/// It extracts the symbols for each tape and records the positions of any `INPUT_BLANK_SYMBOL`s
/// for later rewriting.
fn parse_tapes(pair: Pair<Rule>) -> (Vec<Vec<char>>, Vec<Vec<usize>>) {
    let mut tapes = Vec::new();
    let mut blank_indices = Vec::new();

    // Rule: (tape | tapes) > symbols > [symbol]
    for tape_pair in pair.into_inner() {
        if tape_pair.as_rule() == Rule::symbols {
            let mut line = vec![];
            let mut tape_blank_indices = Vec::new();
            for tape_item in tape_pair.into_inner() {
                // Stores blank indices of each tape for rewriting purpose.
                let symbol = parse_symbol(tape_item.as_str());
                if symbol == INPUT_BLANK_SYMBOL {
                    tape_blank_indices.push(line.len());
                }
                line.push(symbol);
            }

            tapes.push(line);
            blank_indices.push(tape_blank_indices);
        }
    }

    (tapes, blank_indices)
}

/// Creates a `TuringMachineError::ParseError` from a message and a `Span`.
fn parse_error(msg: &str, span: Span) -> TuringMachineError {
    TuringMachineError::ParseError(Box::new(Error::new_from_span(
        ErrorVariant::CustomError {
            message: msg.to_string(),
        },
        span,
    )))
}

/// Parses head position definitions from a `Pair<Rule::head>` or `Pair<Rule::heads>`.
///
/// If no head positions are explicitly defined, it defaults to `[0]` for each tape.
fn parse_heads(pair: Pair<Rule>) -> Vec<usize> {
    let mut positions = Vec::new();

    // Rule: (head | heads) > [index]
    for pos_pair in pair.into_inner() {
        if pos_pair.as_rule() == Rule::index {
            let pos = pos_pair.as_str().parse::<usize>().unwrap_or(0);
            positions.push(pos);
        }
    }

    if positions.is_empty() {
        positions.push(0);
    }

    positions
}

/// Parses the transition rules section from a `Pair<Rule::rules>`.
///
/// It extracts each state's transitions and sets the first encountered state as the initial state.
/// It also checks for duplicate transition rules for the same state.
fn parse_transitions(
    pair: Pair<Rule>,
    initial_state: &mut Option<String>,
) -> Result<HashMap<String, Vec<Transition>>, TuringMachineError> {
    let mut transitions = HashMap::new();

    for transition_pair in pair.into_inner() {
        let span = transition_pair.as_span();
        let (state, actions) = parse_multi_tape_transition(transition_pair)?;

        // Set first state as initial state
        if initial_state.is_none() {
            *initial_state = Some(state.clone());
        }

        // Prevent duplicated transition rule
        if transitions.contains_key(&state) {
            return Err(parse_error(
                &format!("Duplicate transition rule: {state}"),
                span,
            ));
        }

        transitions.insert(state, actions);
    }

    Ok(transitions)
}

/// Parses a single multi-tape transition rule from a `Pair<Rule::transition>`.
///
/// It extracts the source state and a list of actions (transitions) associated with that state.
fn parse_multi_tape_transition(
    pair: Pair<Rule>,
) -> Result<(String, Vec<Transition>), TuringMachineError> {
    let mut pairs = pair.into_inner();
    let state = parse_string(&mut pairs);
    let mut actions = Vec::new();

    for p in pairs {
        if p.as_rule() == Rule::action {
            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,
                        });
                    }
                    Rule::multi_tape_action => {
                        actions.push(parse_multi_tape_action(inner)?);
                    }
                    _ => {} //
                }
            }
        }
    }

    Ok((state, actions))
}

/// Parses a single-tape action from a `Pair<Rule::single_tape_action>`.
///
/// It extracts the read symbol, write symbol (defaults to read if omitted), direction, and next state.
fn parse_single_tape_action(pair: Pair<Rule>) -> Result<ParsedAction, TuringMachineError> {
    let mut pairs = pair.into_inner();
    let read = parse_symbol_from_pairs(&mut pairs);

    // If `write` is omitted, we'll make `write` equal to `read`
    let write = match pairs.peek().unwrap().as_rule() {
        Rule::direction => read,
        _ => parse_symbol_from_pairs(&mut pairs),
    };

    let direction = parse_direction(pairs.next().unwrap())?;
    let next = parse_string(&mut pairs);

    Ok(ParsedAction {
        read,
        write,
        direction,
        next,
    })
}

/// Parses a multi-tape action from a `Pair<Rule::multi_tape_action>`.
///
/// It extracts the read symbols, write symbols, directions, and next state for all tapes.
/// It also validates that the number of read symbols, write symbols, and directions are consistent.
fn parse_multi_tape_action(pair: Pair<Rule>) -> Result<Transition, TuringMachineError> {
    let span = pair.as_span();
    let mut pairs = pair.into_inner();

    // Parse read symbols
    let read_symbols = parse_multi_tape_symbols(pairs.next().unwrap())?;

    // Parse write symbols (or use read symbols if omitted)
    let write_symbols = parse_multi_tape_symbols(pairs.next().unwrap())?;

    // Parse directions
    let directions = parse_directions(pairs.next().unwrap())?;

    // Parse next state
    let next_state = parse_string(&mut pairs);

    // Validate that all arrays have the same length
    if read_symbols.len() != write_symbols.len() || read_symbols.len() != directions.len() {
        return Err(parse_error(
            &format!(
                "Inconsistent multi-tape action: read={}, write={}, directions={}",
                read_symbols.len(),
                write_symbols.len(),
                directions.len()
            ),
            span,
        ));
    }

    Ok(Transition {
        read: read_symbols,
        write: write_symbols,
        directions,
        next_state,
    })
}

/// Parses a list of multi-tape symbols from a `Pair<Rule::multi_tape_symbols>`.
fn parse_multi_tape_symbols(pair: Pair<Rule>) -> Result<Vec<char>, TuringMachineError> {
    let mut symbols = Vec::new();

    for symbol_pair in pair.into_inner() {
        if symbol_pair.as_rule() == Rule::symbol {
            symbols.push(parse_symbol(symbol_pair.as_str()));
        }
    }

    Ok(symbols)
}

/// Parses a list of directions from a `Pair<Rule::directions>`.
fn parse_directions(pair: Pair<Rule>) -> Result<Vec<Direction>, TuringMachineError> {
    let mut directions = Vec::new();

    for dir_pair in pair.into_inner() {
        if dir_pair.as_rule() == Rule::direction {
            directions.push(parse_direction(dir_pair)?);
        }
    }

    Ok(directions)
}

/// Parses a single direction from a `Pair<Rule::direction>`.
///
/// Supports '<' or 'L' for Left, '>' or 'R' for Right, and '-' or 'S' for Stay.
fn parse_direction(pair: Pair<Rule>) -> Result<Direction, TuringMachineError> {
    let span = pair.as_span();
    match pair.as_str() {
        "<" | "L" => Ok(Direction::Left),
        ">" | "R" => Ok(Direction::Right),
        "-" | "S" => Ok(Direction::Stay),
        _ => Err(parse_error(
            &format!("Unsupported direction: {}", pair.as_str()),
            span,
        )),
    }
}

/// Parses a single character symbol from a string, handling quoted and unquoted symbols.
fn parse_symbol(input: &str) -> char {
    input
        .trim_matches('\'')
        .chars()
        .next()
        .unwrap_or(DEFAULT_BLANK_SYMBOL)
}

/// Parses a single character symbol from a `Pairs` iterator.
fn parse_symbol_from_pairs(pairs: &mut Pairs<Rule>) -> char {
    parse_symbol(&parse_string(pairs))
}

/// Extracts the inner string content from a `Pair`.
fn parse_inner_string(pair: Pair<Rule>) -> String {
    pair.into_inner().next().unwrap().as_str().into()
}

/// Extracts the string content from the current `Pair` in a `Pairs` iterator.
fn parse_string(pairs: &mut Pairs<Rule>) -> String {
    pairs.next().unwrap().as_str().into()
}

/// Checks if a given rule has already been declared, ensuring uniqueness for top-level sections.
fn check_unique_rule(
    rule: Rule,
    span: Span,
    seen: &mut HashSet<Rule>,
) -> Result<(), TuringMachineError> {
    if !matches!(
        rule,
        Rule::name
            | Rule::blank
            | Rule::tape
            | Rule::tapes
            | Rule::head
            | Rule::heads
            | Rule::rules
    ) {
        return Ok(());
    };

    if seen.contains(&rule) {
        return Err(parse_error(
            &format!("Duplicate \"{rule:?}:\" declaration"),
            span,
        ));
    }

    seen.insert(rule);

    Ok(())
}

/// Checks if an exclusive rule (e.g., `tape` vs. `tapes`) has been violated.
fn check_exclusive_rule<T>(
    value: Option<T>,
    names: Vec<&str>,
    span: Span,
) -> Result<(), TuringMachineError> {
    if value.is_some() {
        return Err(parse_error(
            &format!("Only one of {} is allowed", format_rules(names)),
            span,
        ));
    }

    Ok(())
}

/// Checks if a required rule is present, returning an `Err` if it's missing.
fn check_required_rule<T>(value: Option<T>, names: Vec<&str>) -> Result<T, TuringMachineError> {
    value.ok_or_else(|| {
        TuringMachineError::ValidationError(format!("Missing {} section", format_rules(names)))
    })
}

/// Checks for consistency between the number of head positions and the number of tapes.
fn check_head_tape_consistency(heads: &[usize], tapes: &[Tape]) -> Result<(), TuringMachineError> {
    if heads.len() != tapes.len() {
        return Err(TuringMachineError::ValidationError(format!(
            "Number of head positions ({}) does not match number of tapes ({})",
            heads.len(),
            tapes.len()
        )));
    }
    Ok(())
}

/// Replace INPUT_BLANK_SYMBOL with the actual blank symbol in tapes using collected indices
fn rewrite_tapes(
    (mut tapes, blank_indices): (Vec<Tape>, Vec<Vec<usize>>),
    blank: char,
) -> Vec<Tape> {
    for (tape_idx, indices) in blank_indices.into_iter().enumerate() {
        for idx in indices {
            tapes[tape_idx][idx] = blank;
        }
    }

    tapes
}

/// Formats a list of rule names into a human-readable string for error messages.
fn format_rules(names: Vec<&str>) -> String {
    names
        .iter()
        .map(|s| format!("'{s}'"))
        .collect::<Vec<_>>()
        .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::*;

    #[test]
    fn test_parse_simple_program() {
        let input = r#"
name: Simple Test
tape: a
rules:
  start:
    a -> b, R, halt
  halt:
"#;

        let result = parse(input);
        assert!(result.is_ok());

        let program = result.unwrap();
        assert_eq!(program.name, "Simple Test");
        assert_eq!(program.initial_tape(), "a");
        assert!(program.rules.contains_key("start"));
        assert!(program.rules.contains_key("halt"));
    }

    #[test]
    fn test_parse_simple_multi_tape_program() {
        let input = r#"
name: Simple Multi-Tape
heads: [0, 0]
tapes:
  [a, b, c]
  [d, e, f]
rules:
  start:
    [a, d] -> [b, e], [R, R], halt
  halt:
"#;

        let result = parse(input);
        assert!(result.is_ok());

        let program = result.unwrap();
        assert_eq!(program.name, "Simple Multi-Tape");
        assert_eq!(program.tapes, vec!["abc", "def"]);
        assert_eq!(
            program.rules["start"][0],
            Transition {
                read: vec!['a', 'd'],
                write: vec!['b', 'e'],
                directions: vec![Direction::Right, Direction::Right],
                next_state: "halt".into(),
            }
        );
        assert!(program.rules.contains_key("start"));
        assert!(program.rules.contains_key("halt"));
    }

    #[test]
    fn test_parse_duplicate_section() {
        let input = r#"
name: First Name
name: Second Name
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("Duplicate \"name:\" declaration"));
    }

    #[test]
    fn test_parse_missing_name() {
        let input = r#"
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(_)));
    }

    #[test]
    fn test_parse_missing_tape() {
        let input = r#"
name: Missing Tape
rules:
  start:
    a -> b, R, halt
"#;
        let result = parse(input);
        assert!(result.is_err());
        let error = result.unwrap_err();
        assert!(matches!(error, TuringMachineError::ValidationError(_)));
        assert_eq!(
            error.to_string(),
            "Program validation error: Missing 'tape' or 'tapes' section"
        );
    }

    #[test]
    fn test_parse_missing_transitions() {
        let input = r#"
name: Missing Transitions
tape: a
"#;
        let result = parse(input);
        assert!(result.is_err());
        let error = result.unwrap_err();
        assert!(matches!(error, TuringMachineError::ValidationError(_)));
        assert_eq!(
            error.to_string(),
            "Program validation error: Missing 'rules' section"
        );
    }

    #[test]
    fn test_parse_exclusive_tape_and_tapes() {
        let input = r#"
name: Exclusive Tapes
tape: a
tapes:
  [b]
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("Only one of 'tape' or 'tapes' is allowed"));
    }

    #[test]
    fn test_parse_exclusive_head_and_heads() {
        let input = r#"
name: Exclusive Heads
head: 0
heads: [1]
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("Only one of 'head' or 'heads' is allowed"));
    }

    #[test]
    fn test_parse_mismatched_heads_and_tapes() {
        let input = r#"
name: Mismatched
heads: [0, 1]
tapes:
  [a]
rules:
  start:
    a -> b, R, halt
"#;
        let result = parse(input);
        assert!(result.is_err());
        let error = result.unwrap_err();
        assert!(matches!(error, TuringMachineError::ValidationError(_)));
        assert_eq!(
            error.to_string(),
            "Program validation error: Number of head positions (2) does not match number of tapes (1)"
        );
    }

    #[test]
    fn test_parse_duplicate_transition_rule() {
        let input = r#"
name: Duplicate Transition
tape: a
rules:
  start:
    a -> b, R, halt
  start:
    b -> a, L, start
"#;
        let result = parse(input);
        assert!(result.is_err());
        let error = result.unwrap_err();
        assert!(matches!(error, TuringMachineError::ParseError(_)));
        assert!(error
            .to_string()
            .contains("Duplicate transition rule: start"));
    }

    #[test]
    fn test_parse_inconsistent_multi_tape_action() {
        let input = r#"
name: Inconsistent Action
tapes:
  [a]
  [b]
rules:
  start:
    [a, b] -> [c], [R, 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("Inconsistent multi-tape action: read=2, write=1, directions=2"));
    }

    #[test]
    fn test_parse_unsupported_direction() {
        let input = r#"
name: Bad Direction
tape: a
rules:
  start:
    a -> b, X, halt
"#;
        let result = parse(input);
        assert!(result.is_err());
        if let Err(e) = &result {
            eprintln!("Parsing error for unsupported direction: {:?}", e);
        }
        let error = result.unwrap_err();
        assert!(matches!(error, TuringMachineError::ParseError(_)));
    }

    #[test]
    fn test_parse_with_custom_blank() {
        let input = r#"
name: Custom Blank
blank: '_'
tape: a
rules:
  start:
    a -> b, R, halt
"#;
        let result = parse(input);
        assert!(result.is_ok());
        let program = result.unwrap();
        assert_eq!(program.blank, '_');
    }

    #[test]
    fn test_parse_with_default_blank() {
        let input = r#"
name: Default Blank
tape: a
rules:
  start:
    a -> b, R, halt
"#;
        let result = parse(input);
        assert!(result.is_ok());
        let program = result.unwrap();
        assert_eq!(program.blank, DEFAULT_BLANK_SYMBOL);
    }

    #[test]
    fn test_parse_with_single_head() {
        let input = r#"
name: Single Head
head: 5
tape: "123456789"
rules:
  start:
    '6' -> 'X', R, halt
"#;
        let result = parse(input);
        if let Err(e) = &result {
            eprintln!("Parsing error for single head: {:?}", e);
        }
        assert!(result.is_err());
        let error = result.unwrap_err();
        assert!(matches!(error, TuringMachineError::ParseError(_)));
        assert!(error.to_string().contains("tape"));
    }

    #[test]
    fn test_parse_omitted_write_symbol() {
        let input = r#"
name: Omitted Write
tape: a
rules:
  start:
    a, R, halt
"#;
        let result = parse(input);
        if let Err(e) = &result {
            eprintln!("Parsing error: {:?}", e);
        }
        assert!(result.is_ok());
        let program = result.unwrap();
        let transition = &program.rules["start"][0];
        assert_eq!(transition.read, vec!['a']);
        assert_eq!(transition.write, vec!['a']); // Should write what it read
        assert_eq!(transition.directions, vec![Direction::Right]);
        assert_eq!(transition.next_state, "halt");
    }

    #[test]
    fn test_parse_tape_with_blank_symbol() {
        let input = r#"
name: Blank Tape Test
tape: a, _, b
rules:
  start:
    a -> a, R, halt
  halt:
"#;

        let result = parse(input);
        assert!(result.is_ok());

        let program = result.unwrap();
        assert_eq!(
            program.tapes[0].chars().nth(1).unwrap(),
            DEFAULT_BLANK_SYMBOL
        );
    }
}