├── .github ├── FUNDING.yml ├── tur-logo.png ├── tur-logo.free ├── assets │ ├── tur-tui.png │ └── tur-web.png ├── tur-logo-small.png ├── .release-please-manifest.json ├── release-please-config.json └── workflows │ ├── ci.yml │ ├── github-pages.yml │ └── release.yml ├── platforms ├── action │ ├── src │ │ ├── lib.rs │ │ └── action.rs │ └── Cargo.toml ├── web │ ├── Trunk.toml │ ├── src │ │ ├── components │ │ │ ├── mod.rs │ │ │ ├── program_selector.rs │ │ │ ├── share_button.rs │ │ │ ├── tape_view.rs │ │ │ ├── graph_view.rs │ │ │ └── program_editor.rs │ │ ├── main.rs │ │ ├── url_sharing.rs │ │ └── app.rs │ ├── Cargo.toml │ ├── index.html │ ├── README.md │ └── turing-editor.js ├── cli │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ │ └── main.rs └── tui │ ├── CHANGELOG.md │ ├── Cargo.toml │ └── src │ ├── main.rs │ └── app.rs ├── examples ├── busy-beaver-3.tur ├── multi-tape-example.tur ├── binary-addition.tur ├── event-number-checker.tur ├── even-zeros-and-ones.tur ├── multi-tape-copy.tur ├── multi-tape-compare.tur ├── subtraction.tur ├── multi-tape-addition.tur ├── palindrome.tur └── README.md ├── CHANGELOG.md ├── .gitignore ├── LICENSE ├── src ├── lib.rs ├── grammar.pest ├── types.rs ├── loader.rs ├── programs.rs ├── encoder.rs └── machine.rs ├── Cargo.toml └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rezigned 2 | buy_me_a_coffee: rezigned 3 | -------------------------------------------------------------------------------- /platforms/action/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod action; 2 | 3 | pub use action::Action; 4 | -------------------------------------------------------------------------------- /.github/tur-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezigned/tur/HEAD/.github/tur-logo.png -------------------------------------------------------------------------------- /.github/tur-logo.free: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezigned/tur/HEAD/.github/tur-logo.free -------------------------------------------------------------------------------- /.github/assets/tur-tui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezigned/tur/HEAD/.github/assets/tur-tui.png -------------------------------------------------------------------------------- /.github/assets/tur-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezigned/tur/HEAD/.github/assets/tur-web.png -------------------------------------------------------------------------------- /.github/tur-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rezigned/tur/HEAD/.github/tur-logo-small.png -------------------------------------------------------------------------------- /.github/.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.1.0", 3 | "platforms/cli": "0.1.0", 4 | "platforms/tui": "0.1.0" 5 | } 6 | -------------------------------------------------------------------------------- /platforms/action/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "action" 3 | version = "0.0.1" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [dependencies] 9 | keymap = { version = "1.0.0-rc.3" } 10 | -------------------------------------------------------------------------------- /examples/busy-beaver-3.tur: -------------------------------------------------------------------------------- 1 | name: Busy Beaver 3-State 2 | tape: _ 3 | rules: 4 | A: 5 | _ -> 1, R, B 6 | 1 -> 1, L, C 7 | B: 8 | _ -> 1, L, A 9 | 1 -> 1, R, B 10 | C: 11 | _ -> 1, L, B 12 | 1 -> 1, R, halt 13 | halt: -------------------------------------------------------------------------------- /platforms/web/Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "index.html" 3 | dist = "dist" 4 | assets = ["turing-editor.js"] 5 | 6 | [watch] 7 | watch = ["src", "styles.css", "turing-editor.js"] 8 | ignore = ["dist"] 9 | 10 | [serve] 11 | address = "127.0.0.1" 12 | port = 8080 13 | open = true -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0](https://github.com/rezigned/tur/compare/tur-v0.0.1...tur-v0.1.0) (2025-08-10) 4 | 5 | 6 | ### Features 7 | 8 | * initial release of Tur - Turing Machine Language ([1db363e](https://github.com/rezigned/tur/commit/1db363e0f19758afa17bd3e52e423645cff57b4b)) 9 | -------------------------------------------------------------------------------- /platforms/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0](https://github.com/rezigned/tur/compare/tur-cli-v0.0.1...tur-cli-v0.1.0) (2025-08-10) 4 | 5 | 6 | ### Features 7 | 8 | * initial release of Tur - Turing Machine Language ([1db363e](https://github.com/rezigned/tur/commit/1db363e0f19758afa17bd3e52e423645cff57b4b)) 9 | -------------------------------------------------------------------------------- /platforms/tui/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0](https://github.com/rezigned/tur/compare/tur-tui-v0.0.1...tur-tui-v0.1.0) (2025-08-10) 4 | 5 | 6 | ### Features 7 | 8 | * initial release of Tur - Turing Machine Language ([1db363e](https://github.com/rezigned/tur/commit/1db363e0f19758afa17bd3e52e423645cff57b4b)) 9 | -------------------------------------------------------------------------------- /examples/multi-tape-example.tur: -------------------------------------------------------------------------------- 1 | name: Multi-Tape Example 2 | heads: [0, 0] 3 | tapes: 4 | [a, b, c] 5 | [x, y, z] 6 | rules: 7 | start: 8 | [a, x] -> [b, y], [R, R], middle 9 | [b, y] -> [c, z], [L, R], start 10 | middle: 11 | [b, y] -> [c, z], [R, S], end 12 | [c, z] -> [a, x], [L, L], start 13 | end: 14 | -------------------------------------------------------------------------------- /examples/binary-addition.tur: -------------------------------------------------------------------------------- 1 | name: Binary addition 2 | tape: $, 0, 0, 1, 1, 1 3 | rules: 4 | start: 5 | $ -> $, R, s1 6 | s1: 7 | 0 -> 0, R, s1 8 | 1 -> 1, R, s1 9 | _ -> _, L, s2 10 | s2: 11 | 1 -> 0, L, s2 12 | 0 -> 1, L, stop 13 | $ -> 1, L, s3 14 | s3: 15 | _ -> $, R, stop 16 | stop: 17 | -------------------------------------------------------------------------------- /platforms/web/src/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod graph_view; 2 | mod program_editor; 3 | mod program_selector; 4 | mod share_button; 5 | mod tape_view; 6 | 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub enum MachineState { 9 | Running, 10 | Halted, 11 | } 12 | 13 | pub use graph_view::GraphView; 14 | pub use program_editor::ProgramEditor; 15 | pub use program_selector::ProgramSelector; 16 | pub use share_button::ShareButton; 17 | pub use tape_view::TapeView; 18 | -------------------------------------------------------------------------------- /examples/event-number-checker.tur: -------------------------------------------------------------------------------- 1 | name: Even Number Checker 2 | tape: 1, 0, 1, 0 3 | rules: 4 | start: 5 | 0 -> 0, R, start 6 | 1 -> 1, R, start 7 | _ -> _, L, check # Reached end, go back to last digit 8 | check: 9 | 0 -> 0, S, accept # Last digit is 0 - number is even 10 | 1 -> 1, S, reject # Last digit is 1 - number is odd 11 | accept: 12 | # Machine halts - INPUT IS EVEN 13 | reject: 14 | # Machine halts - INPUT IS ODD 15 | -------------------------------------------------------------------------------- /examples/even-zeros-and-ones.tur: -------------------------------------------------------------------------------- 1 | # https://www.youtube.com/watch?v=PLVCscCY4xI 2 | name: Even Number of 0s and 1s 3 | tape: 0, 0, 1, 1 4 | rules: 5 | start: 6 | X, R, start 7 | 0 -> X, R, B 8 | 1 -> X, R, C 9 | _, R, accept 10 | B: 11 | X, R, B 12 | 0, R, B 13 | 1 -> X, L, D 14 | C: 15 | X, R, C 16 | 1, R, C 17 | 0 -> X, L, D 18 | D: 19 | X, L, D 20 | 1, L, D 21 | 0, L, D 22 | _, R, start 23 | accept: 24 | -------------------------------------------------------------------------------- /examples/multi-tape-copy.tur: -------------------------------------------------------------------------------- 1 | name: Multi-Tape Copy 2 | heads: [0, 0] 3 | tapes: 4 | [a, b, c] 5 | [_, _, _] 6 | rules: 7 | start: 8 | [a, _] -> [a, a], [R, R], start 9 | [b, _] -> [b, b], [R, R], start 10 | [c, _] -> [c, c], [R, R], start 11 | [_, _] -> [_, _], [L, L], rewind 12 | rewind: 13 | [a, a] -> [a, a], [L, L], rewind 14 | [b, b] -> [b, b], [L, L], rewind 15 | [c, c] -> [c, c], [L, L], rewind 16 | [_, _] -> [_, _], [S, S], halt 17 | halt: 18 | -------------------------------------------------------------------------------- /.github/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "include-v-in-tag": true, 4 | "packages": { 5 | ".": { 6 | "release-type": "rust", 7 | "component": "tur" 8 | }, 9 | "platforms/cli": { 10 | "release-type": "rust", 11 | "component": "tur-cli" 12 | }, 13 | "platforms/tui": { 14 | "release-type": "rust", 15 | "component": "tur-tui" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Devenv 27 | .devenv* 28 | devenv.local.nix 29 | 30 | # direnv 31 | .direnv 32 | 33 | # Nix 34 | result 35 | result-* 36 | 37 | # Rust 38 | /target 39 | -------------------------------------------------------------------------------- /platforms/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tur-cli" 3 | version = "0.1.0" 4 | description = "Command-line interface for Turing machine simulator" 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | 13 | [[bin]] 14 | name = "tur-cli" 15 | path = "src/main.rs" 16 | 17 | [dependencies] 18 | tur = { path = "../../", version = "0.1.0" } 19 | clap = { version = "4.0", features = ["derive"] } 20 | atty = "0.2.14" 21 | -------------------------------------------------------------------------------- /platforms/action/src/action.rs: -------------------------------------------------------------------------------- 1 | use keymap::KeyMap; 2 | 3 | #[derive(KeyMap, Clone, Copy, Debug, PartialEq)] 4 | pub enum Action { 5 | /// Quit the application 6 | #[key("q")] 7 | Quit, 8 | /// Reset the machine to its initial state 9 | #[key("r")] 10 | Reset, 11 | /// Advance the machine by one step 12 | #[key("space")] 13 | Step, 14 | /// Toggle auto-play 15 | #[key("p")] 16 | ToggleAutoPlay, 17 | /// Toggle help display 18 | #[key("h")] 19 | ToggleHelp, 20 | /// Load the previous program 21 | #[key("left")] 22 | PreviousProgram, 23 | /// Load the next program 24 | #[key("right")] 25 | NextProgram, 26 | } 27 | -------------------------------------------------------------------------------- /platforms/tui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tur-tui" 3 | version = "0.1.0" 4 | description = "Terminal user interface for Turing machine simulator" 5 | authors.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | 13 | [[bin]] 14 | name = "tur-tui" 15 | path = "src/main.rs" 16 | 17 | [dependencies] 18 | tur = { path = "../../", version = "0.1.0" } 19 | action = { path = "../action/", version = "0.0.1" } 20 | crossterm = "0.29" 21 | ratatui = "0.29.0" 22 | keymap = { version = "1.0.0-rc.3", features = ["crossterm"] } 23 | atty = "0.2" 24 | clap = { version = "4.0", features = ["derive"] } 25 | -------------------------------------------------------------------------------- /platforms/web/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod components; 3 | mod url_sharing; 4 | 5 | use app::App; 6 | use web_sys::wasm_bindgen::JsCast; 7 | 8 | fn main() { 9 | wasm_logger::init(wasm_logger::Config::default()); 10 | console_error_panic_hook::set_once(); 11 | 12 | // Initialize the ProgramManager with embedded programs 13 | if let Err(e) = tur::ProgramManager::load() { 14 | web_sys::console::error_1(&format!("Failed to initialize ProgramManager: {}", e).into()); 15 | } 16 | 17 | // Get the specific element by ID 18 | let window = web_sys::window().unwrap(); 19 | let document = window.document().unwrap(); 20 | let root = document 21 | .get_element_by_id("app") 22 | .unwrap() 23 | .dyn_into::() 24 | .unwrap(); 25 | 26 | // Render the app onto that specific element 27 | yew::Renderer::::with_root(root).render(); 28 | } 29 | -------------------------------------------------------------------------------- /examples/multi-tape-compare.tur: -------------------------------------------------------------------------------- 1 | name: Multi-Tape Compare 2 | heads: [0, 0, 0] 3 | tapes: 4 | [a, b, c] 5 | [a, b, c] 6 | [_] 7 | rules: 8 | start: 9 | [a, a, _] -> [a, a, _], [R, R, S], start 10 | [b, b, _] -> [b, b, _], [R, R, S], start 11 | [c, c, _] -> [c, c, _], [R, R, S], start 12 | [_, _, _] -> [_, _, Y], [S, S, S], halt 13 | [a, b, _] -> [a, b, N], [S, S, S], halt 14 | [a, c, _] -> [a, c, N], [S, S, S], halt 15 | [b, a, _] -> [b, a, N], [S, S, S], halt 16 | [b, c, _] -> [b, c, N], [S, S, S], halt 17 | [c, a, _] -> [c, a, N], [S, S, S], halt 18 | [c, b, _] -> [c, b, N], [S, S, S], halt 19 | [a, _, _] -> [a, _, N], [S, S, S], halt 20 | [b, _, _] -> [b, _, N], [S, S, S], halt 21 | [c, _, _] -> [c, _, N], [S, S, S], halt 22 | [_, a, _] -> [_, a, N], [S, S, S], halt 23 | [_, b, _] -> [_, b, N], [S, S, S], halt 24 | [_, c, _] -> [_, c, N], [S, S, S], halt 25 | halt: 26 | -------------------------------------------------------------------------------- /platforms/web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tur-web" 3 | version = "0.1.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | description = "Web frontend for Turing machine simulator" 8 | 9 | [dependencies] 10 | tur = { path = "../../", version = "0.1.0" } 11 | action = { path = "../action/", version = "0.0.1" } 12 | wasm-bindgen = "0.2" 13 | js-sys = "0.3" 14 | web-sys = { version = "0.3", features = ["Window", "Document", "Element", "KeyboardEvent", "HtmlSelectElement", "Clipboard"] } 15 | console_error_panic_hook = { version = "0.1", optional = true } 16 | serde_json = "1.0" 17 | serde = { version = "1.0", features = ["derive"] } 18 | base64 = "0.22" 19 | yew = { version = "0.21", features = ["csr"] } 20 | wasm-bindgen-futures = "0.4" 21 | wasm-logger = "0.2" 22 | gloo-timers = { version = "0.3", features = ["futures"] } 23 | gloo-events = "0.2" 24 | regex = "1.10" 25 | keymap = { version = "1.0.0-rc.3", features = ["wasm"] } 26 | 27 | [features] 28 | default = ["console_error_panic_hook"] 29 | -------------------------------------------------------------------------------- /examples/subtraction.tur: -------------------------------------------------------------------------------- 1 | name: Subtraction 2 | tape: 1, 1, 1, -, 1, 1 3 | head: 0 4 | 5 | rules: 6 | start: 7 | 1 -> 1, R, start 8 | - -> -, R, s1 9 | _ -> _, L, stop 10 | s1: 11 | 1 -> 1, R, s1 12 | _ -> _, L, s2 13 | s2: 14 | 1 -> _, L, s3 15 | _ -> _, L, s2 16 | - -> -, R, s5 17 | s3: 18 | 1 -> 1, L, s3 19 | - -> -, L, s8 20 | _ -> _, L, s3 21 | s4: 22 | 1 -> 1, R, s4 23 | - -> -, R, s5 24 | _ -> _, R, s4 25 | s5: 26 | 1 -> 1, R, s5 27 | _ -> _, L, s6 28 | s6: 29 | _ -> _, L, s6 30 | 1 -> 1, L, s6 31 | - -> -, L, s7 32 | s7: 33 | _ -> _, R, s9 34 | 1 -> 1, L, s7 35 | - -> -, R, s9 36 | s8: 37 | 1 -> _, R, start 38 | - -> _, R, s4 39 | _ -> _, L, s8 40 | s9: 41 | _ -> _, R, s10 42 | 1 -> 1, L, stop 43 | s10: 44 | _ -> _, R, s10 45 | 1 -> 1, R, s10 46 | - -> _, L, s11 47 | s11: 48 | _ -> _, L, s11 49 | 1 -> 1, L, s12 50 | s12: 51 | 1 -> 1, L, s12 52 | _ -> _, R, stop 53 | stop: 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marut Khumtong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/multi-tape-addition.tur: -------------------------------------------------------------------------------- 1 | name: Multi-Tape Addition (LSB) 2 | tapes: 3 | [1, 1] 4 | [1, 0] 5 | [_, _] 6 | rules: 7 | start: 8 | [1, 1, _] -> [1, 1, 0], [R, R, R], carry # 1+1=0 with carry 9 | [1, 0, _] -> [1, 0, 1], [R, R, R], start # 1+0=1 no carry 10 | [0, 1, _] -> [0, 1, 1], [R, R, R], start # 0+1=1 no carry 11 | [0, 0, _] -> [0, 0, 0], [R, R, R], start # 0+0=0 no carry 12 | [1, _, _] -> [1, _, 1], [R, R, R], start # 1+blank=1 13 | [_, 1, _] -> [_, 1, 1], [R, R, R], start # blank+1=1 14 | [0, _, _] -> [0, _, 0], [R, R, R], start # 0+blank=0 15 | [_, 0, _] -> [_, 0, 0], [R, R, R], start # blank+0=0 16 | [_, _, _] -> [_, _, _], [S, S, S], halt # All done 17 | carry: 18 | [1, 1, _] -> [1, 1, 1], [R, R, R], carry # 1+1+carry=1 with carry 19 | [1, 0, _] -> [1, 0, 0], [R, R, R], carry # 1+0+carry=0 with carry 20 | [0, 1, _] -> [0, 1, 0], [R, R, R], carry # 0+1+carry=0 with carry 21 | [0, 0, _] -> [0, 0, 1], [R, R, R], start # 0+0+carry=1 no carry 22 | [1, _, _] -> [1, _, 0], [R, R, R], carry # 1+blank+carry=0 with carry 23 | [_, 1, _] -> [_, 1, 0], [R, R, R], carry # blank+1+carry=0 with carry 24 | [0, _, _] -> [0, _, 1], [R, R, R], start # 0+blank+carry=1 no carry 25 | [_, 0, _] -> [_, 0, 1], [R, R, R], start # blank+0+carry=1 no carry 26 | [_, _, _] -> [_, _, 1], [S, S, S], halt # Just carry remaining 27 | halt: 28 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides the core logic for a Turing Machine simulator. 2 | //! It includes modules for parsing Turing Machine programs, simulating their execution, 3 | //! analyzing program correctness, and managing a collection of predefined programs. 4 | 5 | pub mod analyzer; 6 | pub mod encoder; 7 | pub mod loader; 8 | pub mod machine; 9 | pub mod parser; 10 | pub mod programs; 11 | pub mod types; 12 | 13 | /// Re-exports the `Rule` enum from the parser module, used by the `pest` grammar. 14 | pub use crate::parser::Rule; 15 | /// Re-exports the `analyze` function and `AnalysisError` enum from the analyzer module. 16 | pub use analyzer::{analyze, AnalysisError}; 17 | /// Re-exports the encoding functions from the encoder module. 18 | pub use encoder::{decode, encode}; 19 | /// Re-exports the `ProgramLoader` struct from the loader module. 20 | pub use loader::ProgramLoader; 21 | /// Re-exports the `TuringMachine` struct from the machine module. 22 | pub use machine::TuringMachine; 23 | /// Re-exports the `parse` function from the parser module. 24 | pub use parser::parse; 25 | /// Re-exports `ProgramInfo`, `ProgramManager`, and `PROGRAMS` from the programs module. 26 | pub use programs::{ProgramInfo, ProgramManager, PROGRAMS}; 27 | /// Re-exports various types related to Turing Machine definition and execution from the types module. 28 | pub use types::{Direction, Program, Step, Transition, TuringMachineError, MAX_PROGRAM_SIZE}; 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'src/**' 8 | - 'platforms/**' 9 | - 'examples/**' 10 | - 'Cargo.toml' 11 | - 'Cargo.lock' 12 | - '.github/workflows/**' 13 | pull_request: 14 | branches: [ main ] 15 | paths: 16 | - 'src/**' 17 | - 'platforms/**' 18 | - 'examples/**' 19 | - 'Cargo.toml' 20 | - 'Cargo.lock' 21 | - '.github/workflows/**' 22 | 23 | jobs: 24 | test: 25 | name: Test 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Install Rust 31 | uses: dtolnay/rust-toolchain@stable 32 | with: 33 | targets: wasm32-unknown-unknown 34 | components: clippy, rustfmt 35 | 36 | - name: Cache dependencies 37 | uses: Swatinem/rust-cache@v2 38 | 39 | - name: Check formatting 40 | run: cargo fmt --all -- --check 41 | 42 | - name: Run clippy 43 | run: cargo clippy --all-targets --all-features -- -D warnings 44 | 45 | - name: Run tests 46 | run: cargo test --workspace 47 | 48 | - name: Check CLI builds 49 | run: cargo check --package tur-cli 50 | 51 | - name: Check TUI builds 52 | run: cargo check --package tur-tui 53 | 54 | - name: Check web builds 55 | run: cargo check --package tur-web --target wasm32-unknown-unknown 56 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "platforms/tui", 4 | "platforms/web", 5 | "platforms/cli", 6 | ] 7 | resolver = "2" 8 | 9 | [workspace.package] 10 | authors = ["Marut K "] 11 | edition = "2021" 12 | license = "MIT OR Apache-2.0" 13 | repository = "https://github.com/rezigned/tur" 14 | homepage = "https://github.com/rezigned/tur" 15 | documentation = "https://docs.rs/tur" 16 | keywords = ["turing-machine", "language", "interpreter", "parser", "dsl"] 17 | categories = ["compilers", "command-line-utilities", "parser-implementations"] 18 | 19 | [workspace.dependencies] 20 | # Shared dependencies 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | thiserror = "1.0" 24 | lazy_static = "1.4" 25 | tempfile = "3.8" 26 | regex = "1.10" 27 | 28 | # Core library as root package 29 | [package] 30 | name = "tur" 31 | version = "0.1.0" 32 | description = "Turing Machine Language - Parser, interpreter, and execution engine" 33 | authors.workspace = true 34 | edition.workspace = true 35 | license.workspace = true 36 | repository.workspace = true 37 | homepage.workspace = true 38 | documentation.workspace = true 39 | keywords.workspace = true 40 | categories.workspace = true 41 | 42 | [dependencies] 43 | serde.workspace = true 44 | serde_json.workspace = true 45 | thiserror.workspace = true 46 | lazy_static = "1.4" 47 | pest = "2.0" 48 | pest_derive = "2.0" 49 | 50 | [dev-dependencies] 51 | tempfile = "3.8" 52 | -------------------------------------------------------------------------------- /examples/palindrome.tur: -------------------------------------------------------------------------------- 1 | name: Palindrome Checker 2 | tape: a, b, b, a 3 | rules: 4 | start: 5 | a -> X, R, find_end_a # Mark first a and find matching last a 6 | b -> Y, R, find_end_b # Mark first b and find matching last b 7 | _ -> _, S, accept # Empty string or single char - palindrome 8 | X -> X, R, start # Skip already marked symbols 9 | Y -> Y, R, start 10 | find_end_a: 11 | a -> a, R, find_end_a 12 | b -> b, R, find_end_a 13 | X -> X, R, find_end_a 14 | Y -> Y, R, find_end_a 15 | _ -> _, L, check_last_a # Found end, check last symbol 16 | find_end_b: 17 | a -> a, R, find_end_b 18 | b -> b, R, find_end_b 19 | X -> X, R, find_end_b 20 | Y -> Y, R, find_end_b 21 | _ -> _, L, check_last_b # Found end, check last symbol 22 | check_last_a: 23 | a -> X, L, return_start # Match found, mark and return 24 | b -> b, S, reject # No match - not palindrome 25 | X -> X, L, check_last_a # Skip already marked, continue left 26 | Y -> Y, L, check_last_a 27 | check_last_b: 28 | b -> Y, L, return_start # Match found, mark and return 29 | a -> a, S, reject # No match - not palindrome 30 | X -> X, L, check_last_b # Skip already marked, continue left 31 | Y -> Y, L, check_last_b 32 | return_start: 33 | a -> a, L, return_start 34 | b -> b, L, return_start 35 | X -> X, L, return_start 36 | Y -> Y, L, return_start 37 | _ -> _, R, start # Back at start, check next pair 38 | accept: 39 | # Machine halts - INPUT IS PALINDROME 40 | reject: 41 | # Machine halts - INPUT IS NOT PALINDROME -------------------------------------------------------------------------------- /platforms/web/src/components/program_selector.rs: -------------------------------------------------------------------------------- 1 | use tur::ProgramManager; 2 | use yew::prelude::*; 3 | 4 | #[derive(Properties, PartialEq)] 5 | pub struct ProgramSelectorProps { 6 | pub current_program: usize, 7 | pub on_select: Callback, 8 | } 9 | 10 | #[function_component(ProgramSelector)] 11 | pub fn program_selector(props: &ProgramSelectorProps) -> Html { 12 | let on_change = { 13 | let on_select = props.on_select.clone(); 14 | Callback::from(move |e: Event| { 15 | let target = e.target_dyn_into::().unwrap(); 16 | let value = target.value(); 17 | if value.is_empty() { 18 | on_select.emit(usize::MAX); 19 | } else { 20 | let index = value.parse::().unwrap_or(0); 21 | on_select.emit(index); 22 | } 23 | }) 24 | }; 25 | 26 | let is_custom = props.current_program == usize::MAX; 27 | 28 | html! { 29 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/github-pages.yml: -------------------------------------------------------------------------------- 1 | # See https://book.leptos.dev/deployment/csr.html#github-pages 2 | name: Release to Github Pages 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | paths: 8 | - 'platforms/web/**' 9 | - '.github/workflows/github-pages.yml' 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: write # for committing to gh-pages branch. 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: false 22 | 23 | env: 24 | WASM_PATH: ./platforms/web 25 | 26 | jobs: 27 | Github-Pages-Release: 28 | defaults: 29 | run: 30 | working-directory: ${{ env.WASM_PATH }} 31 | 32 | timeout-minutes: 10 33 | 34 | environment: 35 | name: github-pages 36 | url: ${{ steps.deployment.outputs.page_url }} 37 | 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - uses: actions/checkout@v4 # repo checkout 42 | 43 | # Install Rust Nightly Toolchain, with Clippy & Rustfmt 44 | - name: Install nightly Rust 45 | uses: dtolnay/rust-toolchain@nightly 46 | 47 | - name: Add WASM target 48 | run: rustup target add wasm32-unknown-unknown 49 | 50 | - name: Download and install Trunk binary 51 | run: wget -qO- https://github.com/trunk-rs/trunk/releases/download/v0.21.14/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- 52 | 53 | - name: Build with Trunk 54 | # The behavior of the --public-url argument has changed: it no longer includes a leading '/'. 55 | # We have to specify it explicitly. See https://github.com/trunk-rs/trunk/issues/668 56 | run: ./trunk build --release --public-url "/${GITHUB_REPOSITORY#*/}" 57 | 58 | # Deploy with Github Static Pages 59 | - name: Setup Pages 60 | uses: actions/configure-pages@v5 61 | with: 62 | enablement: true 63 | # token: 64 | 65 | - name: Upload artifact 66 | uses: actions/upload-pages-artifact@v3 67 | with: 68 | # Upload dist dir 69 | path: "${{ env.WASM_PATH }}/dist" 70 | 71 | - name: Deploy to GitHub Pages 🚀 72 | id: deployment 73 | uses: actions/deploy-pages@v4 74 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Turing Machine Programs 2 | 3 | This directory contains example Turing machine programs in `.tur` format. These programs demonstrate both single-tape and multi-tape Turing machine capabilities and can be loaded and executed by the Turing machine simulator across all platforms (CLI, TUI, and Web). 4 | 5 | ## File Format 6 | 7 | Turing machine programs are defined in a simple text format with the `.tur` extension. The format consists of three main sections: 8 | 9 | 1. **Name**: The name of the program 10 | 2. **Tape**: The initial tape content 11 | 3. **Transitions**: The state transition rules 12 | 13 | ### Example 14 | 15 | ```tur 16 | name: Simple Test 17 | tape: a 18 | rules: 19 | start: 20 | a -> b, R, halt 21 | halt: 22 | ``` 23 | 24 | ### Syntax 25 | 26 | The `.tur` file format uses a structured syntax parsed by a Pest grammar with comprehensive validation: 27 | 28 | - **Name**: Specified with `name:` followed by the program name 29 | - **Tape Configuration**: 30 | - **Single-tape**: `tape: symbol1, symbol2, symbol3` 31 | - **Multi-tape**: 32 | ```tur 33 | tapes: 34 | [a, b, c] 35 | [x, y, z] 36 | ``` 37 | - **Head Positions** (optional): 38 | - **Single-tape**: `head: 0` (defaults to 0) 39 | - **Multi-tape**: `heads: [0, 0]` (defaults to all zeros) 40 | - **Blank Symbol**: `blank: _` (defaults to space character) 41 | - **Transition Rules**: Specified with `rules:` followed by state definitions 42 | - Each state is defined by its name followed by a colon 43 | - Each transition rule is indented and specifies: 44 | - **Single-tape**: `symbol -> new_symbol, direction, next_state` 45 | - **Multi-tape**: `[sym1, sym2] -> [new1, new2], [dir1, dir2], next_state` 46 | - **Directions**: `R`/`>` (right), `L`/`<` (left), `S`/`-` (stay) 47 | - **Write-only transitions**: If `-> new_symbol` is omitted, the read symbol is preserved 48 | - The first state defined is automatically the initial state 49 | - **Comments**: Use `#` for line comments and inline comments 50 | - **Special Symbols**: 51 | - `_` represents the blank symbol in program definitions 52 | - Any Unicode character can be used as tape symbols 53 | - The blank symbol can be customized with the `blank:` directive 54 | 55 | ## Available Examples 56 | 57 | ### Single-Tape Programs 58 | - **binary-addition.tur**: Adds two binary numbers 59 | - **busy-beaver-3.tur**: Classic 3-state busy beaver 60 | - **event-number-checker.tur**: Checks if a number is even 61 | - **palindrome.tur**: Checks if input is a palindrome 62 | - **subtraction.tur**: Subtracts two numbers 63 | 64 | ### Multi-Tape Programs 65 | - **multi-tape-addition.tur**: Addition using multiple tapes 66 | - **multi-tape-compare.tur**: Compares content across tapes 67 | - **multi-tape-copy.tur**: Copies content from one tape to another 68 | - **multi-tape-example.tur**: Basic multi-tape demonstration 69 | 70 | Each program includes comprehensive comments explaining the algorithm and state transitions. 71 | -------------------------------------------------------------------------------- /platforms/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::io::{self, BufRead}; 3 | use std::path::Path; 4 | use tur::loader::ProgramLoader; 5 | use tur::machine::TuringMachine; 6 | use tur::Step; 7 | 8 | #[derive(Parser)] 9 | #[clap(author, version, about, long_about = None, arg_required_else_help = true)] 10 | struct Cli { 11 | /// The Turing machine program file to execute 12 | program: String, 13 | 14 | /// The input to the Turing machine 15 | #[clap(short, long)] 16 | input: Vec, 17 | 18 | /// Print each step of the execution 19 | #[clap(short = 'd', long)] 20 | debug: bool, 21 | } 22 | 23 | fn main() { 24 | let cli = Cli::parse(); 25 | 26 | let program = match ProgramLoader::load_program(Path::new(&cli.program)) { 27 | Ok(p) => p, 28 | Err(e) => { 29 | eprintln!("Error loading program: {}", e); 30 | std::process::exit(1); 31 | } 32 | }; 33 | let mut machine = TuringMachine::new(program); 34 | 35 | // Get tape inputs from either CLI args or stdin 36 | let tapes = match read_tape_inputs(&cli.input) { 37 | Ok(inputs) => inputs, 38 | Err(e) => { 39 | eprintln!("{}", e); 40 | std::process::exit(1); 41 | } 42 | }; 43 | 44 | // Set tape contents if any inputs were provided 45 | if !tapes.is_empty() { 46 | if let Err(e) = machine.set_tapes_content(&tapes) { 47 | eprintln!("Error setting tape content: {}", e); 48 | std::process::exit(1); 49 | } 50 | } 51 | 52 | if cli.debug { 53 | run_with_debug(&mut machine); 54 | } else { 55 | machine.run(); 56 | } 57 | 58 | println!("{}", format_tapes(machine.tapes()).join("\n")); 59 | } 60 | 61 | /// Runs the Turing machine with debug output, printing each step. 62 | fn run_with_debug(machine: &mut TuringMachine) { 63 | let print_state = |machine: &TuringMachine| { 64 | println!( 65 | "Step: {}, State: {}, Tapes: [{}], Heads: {:?}", 66 | machine.step_count(), 67 | machine.state(), 68 | format_tapes(machine.tapes()).join(", "), 69 | machine.heads() 70 | ); 71 | }; 72 | 73 | print_state(machine); 74 | 75 | loop { 76 | match machine.step() { 77 | Step::Continue => { 78 | print_state(machine); 79 | } 80 | Step::Halt(_) => { 81 | println!("\nMachine halted."); 82 | break; 83 | } 84 | } 85 | } 86 | 87 | println!("\nFinal tapes:"); 88 | } 89 | 90 | /// Gets tape input from either command line arguments or stdin. 91 | /// Returns a vector of strings representing the content for each tape. 92 | fn read_tape_inputs(inputs: &[String]) -> Result, String> { 93 | if !inputs.is_empty() { 94 | // Use command line inputs 95 | Ok(inputs.to_vec()) 96 | } else if !atty::is(atty::Stream::Stdin) { 97 | // Read from stdin, each line represents a tape 98 | let stdin = io::stdin(); 99 | let mut tape_inputs = Vec::new(); 100 | 101 | for line in stdin.lock().lines() { 102 | match line { 103 | Ok(content) => tape_inputs.push(content.trim().to_string()), 104 | Err(e) => return Err(format!("Error reading from stdin: {}", e)), 105 | } 106 | } 107 | 108 | Ok(tape_inputs) 109 | } else { 110 | // No input provided 111 | Ok(Vec::new()) 112 | } 113 | } 114 | 115 | /// Returns the content of all tapes as a vector of `String`s. 116 | pub fn format_tapes(tapes: &[Vec]) -> Vec { 117 | tapes.iter().map(|tape| tape.iter().collect()).collect() 118 | } 119 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | issues: write # For creating label 11 | 12 | jobs: 13 | # Create release PR and handle releases 14 | release: 15 | name: Release Please 16 | runs-on: ubuntu-latest 17 | outputs: 18 | release_created: ${{ steps.release.outputs.release_created }} 19 | steps: 20 | - uses: googleapis/release-please-action@v4 21 | id: release 22 | with: 23 | config-file: .github/release-please-config.json 24 | manifest-file: .github/.release-please-manifest.json 25 | 26 | # Publish to crates.io 27 | publish: 28 | name: Publish to crates.io 29 | runs-on: ubuntu-latest 30 | needs: release 31 | if: ${{ needs.release.outputs.release_created }} 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | - name: Install Rust 36 | uses: dtolnay/rust-toolchain@stable 37 | with: 38 | targets: wasm32-unknown-unknown 39 | 40 | - name: Cache dependencies 41 | uses: Swatinem/rust-cache@v2 42 | 43 | - name: Publish tur (core library) 44 | run: cargo publish --package tur --token ${{ secrets.CARGO_REGISTRY_TOKEN }} 45 | 46 | - name: Wait for tur to be available 47 | run: sleep 30 48 | 49 | - name: Publish CLI 50 | run: cargo publish --package tur-cli --token ${{ secrets.CARGO_REGISTRY_TOKEN }} 51 | 52 | # - name: Wait for CLI to be available 53 | # run: sleep 30 54 | # 55 | # - name: Publish TUI 56 | # run: cargo publish --package tur-tui --token ${{ secrets.CARGO_REGISTRY_TOKEN }} 57 | 58 | # Build release binaries 59 | # build-binaries: 60 | # name: Build Release Binaries 61 | # runs-on: ${{ matrix.os }} 62 | # if: github.event_name == 'release' 63 | # strategy: 64 | # matrix: 65 | # include: 66 | # - os: ubuntu-latest 67 | # target: x86_64-unknown-linux-gnu 68 | # suffix: "" 69 | # - os: macos-latest 70 | # target: x86_64-apple-darwin 71 | # suffix: "" 72 | # - os: macos-latest 73 | # target: aarch64-apple-darwin 74 | # suffix: "" 75 | # - os: windows-latest 76 | # target: x86_64-pc-windows-msvc 77 | # suffix: ".exe" 78 | # 79 | # steps: 80 | # - uses: actions/checkout@v4 81 | # 82 | # - name: Install Rust 83 | # uses: dtolnay/rust-toolchain@stable 84 | # with: 85 | # targets: ${{ matrix.target }} 86 | # 87 | # - name: Cache dependencies 88 | # uses: Swatinem/rust-cache@v2 89 | # with: 90 | # key: ${{ matrix.target }} 91 | # 92 | # - name: Build CLI 93 | # run: cargo build --release --bin tur-cli --target ${{ matrix.target }} 94 | # 95 | # - name: Build TUI 96 | # run: cargo build --release --bin tur-tui --target ${{ matrix.target }} 97 | # 98 | # - name: Package binaries (Unix) 99 | # if: matrix.os != 'windows-latest' 100 | # run: | 101 | # mkdir -p dist 102 | # cp target/${{ matrix.target }}/release/tur-cli dist/tur-cli-${{ matrix.target }} 103 | # cp target/${{ matrix.target }}/release/tur-tui dist/tur-tui-${{ matrix.target }} 104 | # 105 | # - name: Package binaries (Windows) 106 | # if: matrix.os == 'windows-latest' 107 | # run: | 108 | # mkdir dist 109 | # cp target/${{ matrix.target }}/release/tur-cli.exe dist/tur-cli-${{ matrix.target }}.exe 110 | # cp target/${{ matrix.target }}/release/tur-tui.exe dist/tur-tui-${{ matrix.target }}.exe 111 | # 112 | # - name: Upload binaeies to release 113 | # uses: softprops/action-gh-release@v1 114 | # with: 115 | # tag_name: ${{ needs.release-please.outputs.tag_name }} 116 | # files: dist/* 117 | # env: 118 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | -------------------------------------------------------------------------------- /platforms/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Turing Machine Simulator 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 | GitHub stars 53 | Static Badge 54 |
55 |
56 |

57 | 58 |

59 |

Turing Machine Language Simulator

60 |

Bringing Alan Turing's foundational concepts to life through interactive visualization.

61 |
62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /platforms/web/src/url_sharing.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | use web_sys::window; 5 | 6 | #[derive(Serialize, Deserialize, Clone, Debug)] 7 | pub struct SharedProgram { 8 | pub name: String, 9 | pub code: String, 10 | } 11 | 12 | pub struct UrlSharing; 13 | 14 | impl UrlSharing { 15 | /// Encode a program into a URL-safe base64 string 16 | pub fn encode_program(name: &str, code: &str) -> Result { 17 | let shared_program = SharedProgram { 18 | name: name.to_string(), 19 | code: code.to_string(), 20 | }; 21 | 22 | let json = serde_json::to_string(&shared_program) 23 | .map_err(|e| format!("Failed to serialize program: {}", e))?; 24 | 25 | Ok(URL_SAFE_NO_PAD.encode(json.as_bytes())) 26 | } 27 | 28 | /// Decode a program from a URL-safe base64 string 29 | pub fn decode_program(encoded: &str) -> Result { 30 | let decoded_bytes = URL_SAFE_NO_PAD 31 | .decode(encoded.as_bytes()) 32 | .map_err(|e| format!("Failed to decode base64: {}", e))?; 33 | 34 | let json = String::from_utf8(decoded_bytes) 35 | .map_err(|e| format!("Invalid UTF-8 in decoded data: {}", e))?; 36 | 37 | serde_json::from_str(&json).map_err(|e| format!("Failed to deserialize program: {}", e)) 38 | } 39 | 40 | /// Generate a shareable URL for the current program 41 | pub fn generate_share_url(name: &str, code: &str) -> Result { 42 | let window = window().ok_or("No window object available")?; 43 | let location = window.location(); 44 | 45 | let base_url = format!( 46 | "{}//{}{}", 47 | location.protocol().map_err(|_| "Failed to get protocol")?, 48 | location.host().map_err(|_| "Failed to get host")?, 49 | location.pathname().map_err(|_| "Failed to get pathname")? 50 | ); 51 | 52 | let encoded_program = Self::encode_program(name, code)?; 53 | Ok(format!("{}?share={}", base_url, encoded_program)) 54 | } 55 | 56 | /// Extract shared program from current URL 57 | pub fn extract_from_url() -> Option { 58 | let window = window()?; 59 | let location = window.location(); 60 | let search = location.search().ok()?; 61 | 62 | if search.is_empty() { 63 | return None; 64 | } 65 | 66 | // Parse URL parameters 67 | let params = Self::parse_url_params(&search); 68 | let encoded_program = params.get("share")?; 69 | 70 | Self::decode_program(encoded_program).ok() 71 | } 72 | 73 | /// Parse URL search parameters into a HashMap 74 | fn parse_url_params(search: &str) -> HashMap { 75 | let mut params = HashMap::new(); 76 | 77 | // Remove leading '?' if present 78 | let search = search.strip_prefix('?').unwrap_or(search); 79 | 80 | for pair in search.split('&') { 81 | if let Some((key, value)) = pair.split_once('=') { 82 | // URL decode the value 83 | if let Ok(decoded_value) = js_sys::decode_uri_component(value) { 84 | params.insert( 85 | key.to_string(), 86 | decoded_value.as_string().unwrap_or_default(), 87 | ); 88 | } 89 | } 90 | } 91 | 92 | params 93 | } 94 | 95 | /// Copy text to clipboard 96 | pub fn copy_to_clipboard(text: &str) -> Result<(), String> { 97 | let window = window().ok_or("No window object available")?; 98 | 99 | // Use the clipboard API if available 100 | if let Ok(navigator) = js_sys::Reflect::get(&window, &"navigator".into()) { 101 | if let Ok(clipboard) = js_sys::Reflect::get(&navigator, &"clipboard".into()) { 102 | if !clipboard.is_undefined() { 103 | let clipboard: web_sys::Clipboard = clipboard.into(); 104 | let _promise = clipboard.write_text(text); 105 | return Ok(()); 106 | } 107 | } 108 | } 109 | 110 | Err("Clipboard API not available".to_string()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/grammar.pest: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // MAIN PROGRAM STRUCTURE 3 | // ============================================================================= 4 | program = { 5 | SOI 6 | ~ ( 7 | (LEADING* ~ name) 8 | | (LEADING+ ~ mode) 9 | | (LEADING+ ~ head) 10 | | (LEADING+ ~ heads) 11 | | (LEADING+ ~ blank) 12 | | (LEADING+ ~ tape) 13 | | (LEADING+ ~ tapes) 14 | | (LEADING+ ~ states) 15 | | (LEADING+ ~ rules) 16 | )* 17 | ~ EOI 18 | } 19 | 20 | // ============================================================================= 21 | // TOP-LEVEL SECTIONS 22 | // ============================================================================= 23 | name = { "name:" ~ string } 24 | mode = { "mode:" ~ string } 25 | head = { "head:" ~ index } 26 | heads = { "heads:" ~ "[" ~ index ~ ("," ~ index)* ~ "]" } 27 | blank = { "blank:" ~ symbol } 28 | tape = { "tape:" ~ tape_line } 29 | tapes = ${ "tapes:" ~ tape_list } 30 | tape_list = _{ (NEWLINE ~ INDENT ~ tape_line)+ } 31 | tape_line = _{ ("[" ~ symbols ~ "]") | symbols } 32 | 33 | // ============================================================================= 34 | // STATES SECTION 35 | // ============================================================================= 36 | states = ${ "states:" ~ states_block } 37 | states_block = _{ 38 | (NEWLINE ~ INDENT ~ state_start)? ~ (NEWLINE ~ INDENT ~ state_stop)? 39 | } 40 | state_start = { "start:" ~ state } 41 | state_stop = { "stop:" ~ state ~ ("," ~ state)* } 42 | 43 | // ============================================================================= 44 | // TRANSITIONS SECTION 45 | // ============================================================================= 46 | rules = ${ "rules:" ~ transition_block ~ (block_sep ~ last_transition)? } 47 | transition_block = _{ block_start ~ transition ~ (block_sep ~ (comment | transition))* ~ block_end ~ transition_block* } 48 | transition = ${ state ~ ":" ~ actions+ } 49 | last_transition = ${ state ~ ":" } 50 | 51 | // ============================================================================= 52 | // ACTIONS AND TRANSITIONS 53 | // ============================================================================= 54 | actions = _{ block_start ~ (comment | action+) ~ trailing_comment? ~ block_end } 55 | action = !{ (multi_tape_action | single_tape_action) } 56 | multi_tape_action = { multi_tape_symbols ~ ("->" ~ multi_tape_symbols)? ~ "," ~ directions ~ "," ~ state } 57 | multi_tape_symbols = { "[" ~ symbol ~ ("," ~ symbol)* ~ "]" } 58 | single_tape_action = { symbol ~ ("->" ~ symbol)? ~ "," ~ direction ~ "," ~ state } 59 | directions = { "[" ~ direction ~ ("," ~ direction)* ~ "]" } 60 | direction = { "<" | ">" | "-" | "L" | "R" | "S" } 61 | 62 | // ============================================================================= 63 | // BLOCK STRUCTURE (INDENTATION) - RESTORED 64 | // ============================================================================= 65 | block_start = _{ NEWLINE ~ PEEK_ALL ~ PUSH(INDENT) } 66 | block_end = _{ DROP } 67 | block_sep = _{ NEWLINE+ ~ INDENT } 68 | 69 | // ============================================================================= 70 | // BASIC ELEMENTS 71 | // ============================================================================= 72 | symbols = !{ symbol ~ (separator ~ symbol)* } 73 | symbol = @{ 74 | "'" ~ ANY ~ "'" 75 | | (!reserved_char ~ ANY) 76 | } 77 | separator = _{ ("," | "|") } 78 | reserved_char = { "#" | " " | "," | ">" | "<" } 79 | state = ${ ident } 80 | string = @{ (!NEWLINE ~ ANY)+ } 81 | ident = { !ASCII_DIGIT ~ (ASCII_ALPHANUMERIC | "_" | "-")+ } 82 | index = { ASCII_DIGIT+ } 83 | 84 | // ============================================================================= 85 | // COMMENTS 86 | // ============================================================================= 87 | comment = _{ "#" ~ (!NEWLINE ~ ANY)* } 88 | trailing_comment = _{ WHITESPACE+ ~ "#" ~ (!NEWLINE ~ ANY)* } 89 | 90 | // ============================================================================= 91 | // WHITESPACE AND INDENTATION 92 | // ============================================================================= 93 | LEADING = _{ NEWLINE | comment } 94 | INDENT = _{ (" " | "\t")+ } 95 | WHITESPACE = _{ " " } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tur - Turing Machine Language 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/tur.svg)](https://crates.io/crates/tur) [![Docs.rs](https://docs.rs/tur/badge.svg)](https://docs.rs/tur) [![CI](https://github.com/rezigned/tur/actions/workflows/ci.yml/badge.svg)](https://github.com/rezigned/tur/actions/workflows/ci.yml) 4 | 5 |

6 | 7 |

8 | 9 | **Tur** is a language for defining and executing Turing machines, complete with parser, interpreter, and multi-platform visualization tools. It supports both single-tape and multi-tape configurations. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 26 | 27 | 28 |
WebTUI
21 | 22 | 24 | 25 |
29 | 30 | ## Language Overview 31 | 32 | Tur (`.tur` files) provides a clean, readable syntax for defining both single-tape and multi-tape Turing machines. The language includes: 33 | 34 | - **Declarative syntax** for states, transitions, and tape configurations 35 | - **Multi-tape support** with synchronized head movements 36 | - **Built-in validation** and static analysis 37 | - **Cross-platform execution** via CLI, TUI, and web interfaces 38 | 39 | ## Syntax Examples 40 | 41 | ### Single-Tape Turing Machine 42 | 43 | ```tur 44 | name: Binary Counter 45 | tape: 1, 0, 1, 1 46 | rules: 47 | start: 48 | 0 -> 0, R, start 49 | 1 -> 1, R, start 50 | _ -> _, L, inc # Reached end, go back to start incrementing 51 | inc: 52 | 0 -> 1, S, done # 0 becomes 1, no carry needed 53 | 1 -> 0, L, inc # 1 becomes 0, carry to next position 54 | _ -> 1, S, done # Reached beginning with carry, add new 1 55 | done: 56 | ``` 57 | 58 | ### Multi-Tape Turing Machine 59 | 60 | ```tur 61 | name: Copy Tape 62 | tapes: 63 | [a, b, c] 64 | [_, _, _] 65 | rules: 66 | copy: 67 | [a, _] -> [a, a], [R, R], copy 68 | [b, _] -> [b, b], [R, R], copy 69 | [c, _] -> [c, c], [R, R], copy 70 | [_, _] -> [_, _], [S, S], done 71 | done: 72 | ``` 73 | 74 | ## Language Specification 75 | 76 | ### Program Structure 77 | 78 | Every `.tur` program consists of: 79 | 80 | ```tur 81 | name: Program Name 82 | 83 | # Single-tape configuration 84 | tape: symbol1, symbol2, symbol3 85 | head: 0 # optional, defaults to 0 86 | 87 | # OR multi-tape configuration 88 | tapes: 89 | [tape1_symbol1, tape1_symbol2] 90 | [tape2_symbol1, tape2_symbol2] 91 | heads: [0, 0] # optional, defaults to [0, 0, ...] 92 | 93 | # Optional blank symbol (defaults to ' ') 94 | blank: ' ' 95 | 96 | # Transition rules 97 | rules: 98 | state_a: 99 | input -> output, direction, next_state 100 | # ... more transitions 101 | state_b: 102 | # ... transitions 103 | ``` 104 | 105 | > [!NOTE] 106 | > The first state defined in the `rules:` section is automatically considered the start state. In the examples above, `state_a` is the initial state because it appears first. 107 | 108 | ### Transition Rules 109 | 110 | **Single-tape format:** 111 | ```tur 112 | current_symbol -> write_symbol, direction, next_state 113 | ``` 114 | 115 | **Multi-tape format:** 116 | ```tur 117 | [sym1, sym2, sym3] -> [write1, write2, write3], [dir1, dir2, dir3], next_state 118 | ``` 119 | 120 | ### Directions 121 | 122 | - `L` or `<` - Move left 123 | - `R` or `>` - Move right 124 | - `S` or `-` - Stay (no movement) 125 | 126 | ### Comments 127 | 128 | ```tur 129 | # This is a comment 130 | state: 131 | a -> b, R, next # Inline comment 132 | ``` 133 | 134 | ### Special Symbols 135 | 136 | - The underscore (`_`) is a special symbol used to represent a blank character in program definitions. 137 | - Any other character or unicode string can be used as tape symbols. 138 | - The blank symbol can be customized using the `blank:` directive in the program. 139 | 140 | ## Platforms 141 | 142 | ### Command Line Interface (CLI) 143 | 144 | ```bash 145 | # Run a program 146 | cargo run -p tur-cli -- examples/binary-addition.tur 147 | 148 | # Pipe input to a program 149 | echo '$011' | cargo run -p tur-cli -- examples/binary-addition.tur 150 | 151 | # Chaining programs with pipes 152 | echo '$011' | cargo run -p tur-cli -- examples/binary-addition.tur | cargo run -p tur-cli -- examples/binary-addition.tur 153 | ``` 154 | 155 | ### Terminal User Interface (TUI) 156 | 157 | ```bash 158 | # Interactive TUI with program selection 159 | cargo run --package tur-tui 160 | ``` 161 | 162 | ## Documentation 163 | 164 | - **API Documentation**: [docs.rs/tur](https://docs.rs/tur) 165 | - **Repository**: [github.com/rezigned/tur](https://github.com/rezigned/tur) 166 | - **Examples**: See the `examples/` directory for sample `.tur` programs 167 | 168 | ## License 169 | 170 | - MIT License ([LICENSE](LICENSE)) 171 | -------------------------------------------------------------------------------- /platforms/tui/src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | 3 | use action::Action; 4 | use app::App; 5 | use clap::Parser; 6 | use crossterm::{ 7 | event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEvent}, 8 | execute, 9 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 10 | }; 11 | use ratatui::{backend::CrosstermBackend, Terminal}; 12 | use std::io::Read; 13 | use std::{error::Error, fs, io, time::Duration}; 14 | 15 | /// A Turing Machine simulator with a Terminal User Interface. 16 | #[derive(Parser, Debug)] 17 | #[clap(author, version, about, long_about = None)] 18 | #[clap(after_help = "EXAMPLES: 19 | tur-tui examples/simple.tur 20 | cat examples/binary-addition.tur | tur-tui")] 21 | struct Cli { 22 | /// Path to a Turing machine program file (.tur). 23 | /// If not provided, the application will load built-in example programs. 24 | /// Can also pipe program content via stdin. 25 | program_file: Option, 26 | } 27 | 28 | /// Represents the state of the application loop. 29 | #[derive(PartialEq)] 30 | enum AppState { 31 | Running, 32 | ShouldQuit, 33 | } 34 | 35 | /// A wrapper around the terminal to ensure it's restored on drop. 36 | struct Tui { 37 | terminal: Terminal>, 38 | } 39 | 40 | impl Tui { 41 | /// Creates a new TUI. 42 | fn new() -> io::Result { 43 | enable_raw_mode()?; 44 | let mut stdout = io::stdout(); 45 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 46 | let terminal = Terminal::new(CrosstermBackend::new(stdout))?; 47 | Ok(Self { terminal }) 48 | } 49 | } 50 | 51 | impl Drop for Tui { 52 | fn drop(&mut self) { 53 | // Restore the terminal to its original state. 54 | // The results are ignored as we can't do much about errors during drop. 55 | let _ = disable_raw_mode(); 56 | let _ = execute!( 57 | self.terminal.backend_mut(), 58 | LeaveAlternateScreen, 59 | DisableMouseCapture 60 | ); 61 | let _ = self.terminal.show_cursor(); 62 | } 63 | } 64 | 65 | fn main() -> Result<(), Box> { 66 | let cli = Cli::parse(); 67 | 68 | // Load the program before initializing the TUI. 69 | // This way, if loading fails, we can print the error to stderr without 70 | // interfering with the terminal's alternate screen. 71 | let app = match load_program(&cli) { 72 | Ok(app) => app, 73 | Err(e) => { 74 | eprintln!("Error: {}", e); 75 | std::process::exit(1); 76 | } 77 | }; 78 | 79 | // Initialize the TUI. The `Tui` struct will handle cleanup on drop. 80 | let mut tui = Tui::new()?; 81 | 82 | // Run the application. 83 | run_app(&mut tui.terminal, app)?; 84 | 85 | Ok(()) 86 | } 87 | 88 | /// Loads a Turing machine program based on CLI arguments. 89 | /// 90 | /// It tries to load from a file path, then from stdin, and finally 91 | /// falls back to the default built-in programs. 92 | fn load_program(cli: &Cli) -> Result { 93 | if let Some(file_path) = &cli.program_file { 94 | fs::read_to_string(file_path) 95 | .map_err(|e| format!("Failed to read file '{}': {}", file_path, e)) 96 | .and_then(App::new_from_program_string) 97 | } else if atty::isnt(atty::Stream::Stdin) { 98 | let mut buffer = String::new(); 99 | io::stdin() 100 | .read_to_string(&mut buffer) 101 | .map_err(|e| format!("Failed to read from stdin: {}", e)) 102 | .and_then(|_| App::new_from_program_string(buffer)) 103 | } else { 104 | Ok(App::new_default()) 105 | } 106 | } 107 | 108 | /// Runs the main application loop. 109 | fn run_app( 110 | terminal: &mut Terminal, 111 | mut app: App, 112 | ) -> io::Result<()> { 113 | loop { 114 | terminal.draw(|f| app.render(f))?; 115 | 116 | let timeout = if app.is_auto_playing() { 117 | Duration::from_millis(500) // Faster updates during auto-play 118 | } else { 119 | Duration::from_millis(100) // Slower updates when idle 120 | }; 121 | 122 | if event::poll(timeout)? { 123 | if let Event::Key(key) = event::read()? { 124 | if handle_key_event(&mut app, key) == AppState::ShouldQuit { 125 | return Ok(()); 126 | } 127 | } 128 | } 129 | 130 | if app.is_auto_playing() { 131 | app.step_machine(); 132 | } 133 | } 134 | } 135 | 136 | /// Handles key events and updates the application state. 137 | fn handle_key_event(app: &mut App, key: KeyEvent) -> AppState { 138 | if let Some(action) = app.keymap.get(&key) { 139 | match action { 140 | Action::Quit => return AppState::ShouldQuit, 141 | Action::Reset => app.reset_machine(), 142 | Action::Step => app.step_machine(), 143 | Action::ToggleAutoPlay => app.toggle_auto_play(), 144 | Action::ToggleHelp => app.toggle_help(), 145 | Action::PreviousProgram => app.previous_program(), 146 | Action::NextProgram => app.next_program(), 147 | } 148 | } 149 | AppState::Running 150 | } 151 | -------------------------------------------------------------------------------- /platforms/web/README.md: -------------------------------------------------------------------------------- 1 | # Turing Machine Web Platform 2 | 3 | This is the web frontend for the Turing Machine simulator, built with Yew (Rust WebAssembly) and CodeMirror for syntax highlighting. 4 | 5 | ## Architecture Overview 6 | 7 | The web platform uses a hybrid architecture combining: 8 | - **Yew** for the main UI components and state management 9 | - **CodeMirror** for the program editor with syntax highlighting 10 | - **JavaScript bridge** to connect CodeMirror with Yew's event system 11 | - **Cytoscape.js** for interactive state transition graph visualization 12 | 13 | ## Program Editor Flow Diagrams 14 | 15 | ### User Typing Flow 16 | 17 | When a user types in the program editor, the following flow occurs: 18 | 19 | ``` 20 | ┌─────────────────┐ 21 | │ User types 'a' │ 22 | └─────────┬───────┘ 23 | │ 24 | ▼ 25 | ┌─────────────────────────────┐ 26 | │ CodeMirror "change" event │ 27 | │ fires │ 28 | └─────────┬───────────────────┘ 29 | │ 30 | ▼ 31 | ┌─────────────────────────────┐ 32 | │ JavaScript change handler: │ 33 | │ - Check programmaticUpdate │ 34 | │ - Update textarea.value │ 35 | │ - Dispatch "input" event │ 36 | └─────────┬───────────────────┘ 37 | │ 38 | ▼ 39 | ┌─────────────────────────────┐ 40 | │ Yew "oninput" handler │ 41 | │ receives InputEvent │ 42 | └─────────┬───────────────────┘ 43 | │ 44 | ▼ 45 | ┌─────────────────────────────┐ 46 | │ Yew sends UpdateText │ 47 | │ message to component │ 48 | └─────────┬───────────────────┘ 49 | │ 50 | ▼ 51 | ┌─────────────────────────────┐ 52 | │ ProgramEditor::update() │ 53 | │ - Updates program_text │ 54 | │ - Calls on_text_change │ 55 | │ - Starts validation timer │ 56 | └─────────┬───────────────────┘ 57 | │ 58 | ▼ 59 | ┌─────────────────────────────┐ 60 | │ App receives text change │ 61 | │ - Updates editor_text │ 62 | │ - Triggers re-render │ 63 | └─────────┬───────────────────┘ 64 | │ 65 | ▼ 66 | ┌─────────────────────────────┐ 67 | │ Validation timer fires │ 68 | │ - Parses program │ 69 | │ - Updates tapes/graph │ 70 | │ - Shows errors │ 71 | └─────────────────────────────┘ 72 | ``` 73 | 74 | ### Program Switch Flow 75 | 76 | When a user selects a different program from the dropdown: 77 | 78 | ``` 79 | ┌─────────────────────────────┐ 80 | │ User selects program from │ 81 | │ dropdown │ 82 | └─────────┬───────────────────┘ 83 | │ 84 | ▼ 85 | ┌─────────────────────────────┐ 86 | │ App::SelectProgram │ 87 | │ - Updates current_program │ 88 | │ - Updates editor_text │ 89 | └─────────┬───────────────────┘ 90 | │ 91 | ▼ 92 | ┌─────────────────────────────┐ 93 | │ ProgramEditor::changed() │ 94 | │ detects program change │ 95 | └─────────┬───────────────────┘ 96 | │ 97 | ▼ 98 | ┌─────────────────────────────┐ 99 | │ Calls updateCodeMirrorValue │ 100 | │ via js_sys::eval │ 101 | └─────────┬───────────────────┘ 102 | │ 103 | ▼ 104 | ┌─────────────────────────────┐ 105 | │ JavaScript: │ 106 | │ - Set programmaticUpdate=true│ 107 | │ - Call editor.setValue() │ 108 | └─────────┬───────────────────┘ 109 | │ 110 | ▼ 111 | ┌─────────────────────────────┐ 112 | │ CodeMirror "change" event │ 113 | │ fires │ 114 | └─────────┬───────────────────┘ 115 | │ 116 | ▼ 117 | ┌─────────────────────────────┐ 118 | │ JavaScript change handler: │ 119 | │ - See programmaticUpdate=true│ 120 | │ - Set programmaticUpdate=false│ 121 | │ - Return early (no input) │ 122 | └─────────────────────────────┘ 123 | ``` 124 | 125 | ## Key Components 126 | 127 | ### JavaScript Bridge (`turing-editor.js`) 128 | 129 | The JavaScript bridge handles: 130 | - CodeMirror initialization and configuration 131 | - Custom Turing machine syntax highlighting mode with comprehensive token recognition 132 | - Event translation between CodeMirror and Yew 133 | - Preventing infinite update loops with `programmaticUpdate` flag 134 | - Graph theme initialization for Cytoscape.js visualization 135 | 136 | ### Core Rust Components 137 | 138 | **Main Application (`src/main.rs`)**: 139 | - Initializes the ProgramManager with embedded programs 140 | - Sets up error handling and logging 141 | - Renders the main App component 142 | 143 | **App Component (`src/app.rs`)**: 144 | - Main application state management 145 | - Handles Turing machine execution and control 146 | - Manages program selection and editor state 147 | - Coordinates between all UI components 148 | 149 | **Program Editor (`src/components/program_editor.rs`)**: 150 | - Manages program text state with debounced validation 151 | - Integrates with CodeMirror for syntax highlighting 152 | - Handles program parsing and error reporting 153 | - Only updates CodeMirror during program switches (not user typing) 154 | 155 | **Tape View (`src/components/tape_view.rs`)**: 156 | - Advanced multi-tape visualization with smooth animations and integrated controls 157 | - Sliding tape animation with fixed head pointer for intuitive visual feedback 158 | - Stable visual buffer architecture using 4x padding cells (80 cells total) for seamless infinite tape experience 159 | - Improved positioning algorithm with enhanced variable naming and logic clarity for better maintainability 160 | - Centered mapping algorithm that maps logical tape content onto visual buffer with head at stable visual coordinate 161 | - Hardware-accelerated animations with mathematical precision for exact head positioning 162 | - Eliminates visual discontinuities during tape expansion with stable buffer architecture 163 | - Integrated execution controls (Step, Reset, Auto/Pause) embedded in tape header 164 | - Built-in speed control with 5 preset speeds (0.25x to 4x) for execution control 165 | - State information display showing current state, steps, symbols, and execution status 166 | - Multi-tape support with individual head tracking and labeling 167 | - Optimized rendering using logical positions for efficient React reconciliation 168 | - Clean code architecture with improved variable naming and documentation for easier maintenance 169 | 170 | ### Key Design Decisions 171 | 172 | 1. **CodeMirror as Source of Truth for User Input**: When users type, CodeMirror maintains cursor position and handles all editing operations. 173 | 174 | 2. **Yew as Source of Truth for Program Switching**: When switching between predefined programs, Yew updates CodeMirror content. 175 | 176 | 3. **Separation of Concerns**: User typing and program switching are handled differently to prevent cursor jumping issues. 177 | 178 | 4. **Safe String Escaping**: Uses `serde_json::to_string` to safely pass program text from Rust to JavaScript, handling special characters like quotes, newlines, and backticks. 179 | 180 | ## Development 181 | 182 | ```bash 183 | # Install dependencies 184 | cargo build 185 | 186 | # Run development server 187 | trunk serve --port 8080 188 | 189 | # Build for production 190 | trunk build --release 191 | ``` 192 | 193 | ## Troubleshooting 194 | 195 | ### Cursor Jumping Issues 196 | If you experience cursor jumping while typing, check: 197 | 1. The `programmaticUpdate` flag is working correctly 198 | 2. `ProgramEditor::changed()` only updates CodeMirror on program switches 199 | 3. CodeMirror initialization is properly synced with textarea content 200 | 201 | ### Validation Not Working 202 | If real-time validation stops working: 203 | 1. Check that the `change` event handler is dispatching input events 204 | 2. Verify the validation timer is being set up correctly 205 | 3. Ensure the parsing logic in `ValidateProgram` is functioning -------------------------------------------------------------------------------- /platforms/web/src/components/share_button.rs: -------------------------------------------------------------------------------- 1 | use crate::url_sharing::UrlSharing; 2 | use wasm_bindgen_futures::spawn_local; 3 | use yew::prelude::*; 4 | 5 | #[derive(Properties, PartialEq)] 6 | pub struct ShareButtonProps { 7 | pub program_name: String, 8 | pub program_code: String, 9 | pub is_enabled: bool, 10 | } 11 | 12 | pub enum ShareButtonMsg { 13 | GenerateShareUrl, 14 | CopyToClipboard(String), 15 | ShowSuccess, 16 | ShowError(String), 17 | ClearMessage, 18 | } 19 | 20 | pub struct ShareButton { 21 | share_url: Option, 22 | message: Option, 23 | is_success: bool, 24 | is_generating: bool, 25 | clear_timeout: Option, 26 | } 27 | 28 | impl Component for ShareButton { 29 | type Message = ShareButtonMsg; 30 | type Properties = ShareButtonProps; 31 | 32 | fn create(_ctx: &Context) -> Self { 33 | Self { 34 | share_url: None, 35 | message: None, 36 | is_success: false, 37 | is_generating: false, 38 | clear_timeout: None, 39 | } 40 | } 41 | 42 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 43 | match msg { 44 | ShareButtonMsg::GenerateShareUrl => { 45 | if !ctx.props().is_enabled { 46 | self.message = Some("Please fix program errors before sharing".to_string()); 47 | self.is_success = false; 48 | return true; 49 | } 50 | 51 | self.is_generating = true; 52 | self.message = None; 53 | 54 | let name = ctx.props().program_name.clone(); 55 | let code = ctx.props().program_code.clone(); 56 | let link = ctx.link().clone(); 57 | 58 | spawn_local(async move { 59 | match UrlSharing::generate_share_url(&name, &code) { 60 | Ok(url) => { 61 | link.send_message(ShareButtonMsg::CopyToClipboard(url)); 62 | } 63 | Err(e) => { 64 | link.send_message(ShareButtonMsg::ShowError(format!( 65 | "Failed to generate share URL: {}", 66 | e 67 | ))); 68 | } 69 | } 70 | }); 71 | 72 | true 73 | } 74 | ShareButtonMsg::CopyToClipboard(url) => { 75 | self.is_generating = false; 76 | self.share_url = Some(url.clone()); 77 | 78 | // Try to copy to clipboard 79 | let link = ctx.link().clone(); 80 | spawn_local(async move { 81 | match UrlSharing::copy_to_clipboard(&url) { 82 | Ok(_) => { 83 | link.send_message(ShareButtonMsg::ShowSuccess); 84 | // Clear the URL display after a delay 85 | let link_clone = link.clone(); 86 | gloo_timers::future::TimeoutFuture::new(3000).await; 87 | link_clone.send_message(ShareButtonMsg::ClearMessage); 88 | } 89 | Err(e) => { 90 | link.send_message(ShareButtonMsg::ShowError(format!( 91 | "Failed to copy to clipboard: {}", 92 | e 93 | ))); 94 | } 95 | } 96 | }); 97 | 98 | true 99 | } 100 | ShareButtonMsg::ShowSuccess => { 101 | self.message = Some("Share URL copied to clipboard!".to_string()); 102 | self.is_success = true; 103 | 104 | // Cancel any existing timeout 105 | if let Some(timeout) = self.clear_timeout.take() { 106 | timeout.cancel(); 107 | } 108 | 109 | // Clear message after 3 seconds 110 | let link = ctx.link().clone(); 111 | self.clear_timeout = Some(gloo_timers::callback::Timeout::new(3000, move || { 112 | link.send_message(ShareButtonMsg::ClearMessage); 113 | })); 114 | 115 | true 116 | } 117 | ShareButtonMsg::ShowError(error) => { 118 | self.message = Some(error); 119 | self.is_success = false; 120 | 121 | // Cancel any existing timeout 122 | if let Some(timeout) = self.clear_timeout.take() { 123 | timeout.cancel(); 124 | } 125 | 126 | // Clear message after 5 seconds 127 | let link = ctx.link().clone(); 128 | self.clear_timeout = Some(gloo_timers::callback::Timeout::new(5000, move || { 129 | link.send_message(ShareButtonMsg::ClearMessage); 130 | })); 131 | 132 | true 133 | } 134 | ShareButtonMsg::ClearMessage => { 135 | self.message = None; 136 | self.is_success = false; 137 | self.clear_timeout = None; 138 | true 139 | } 140 | } 141 | } 142 | 143 | fn view(&self, ctx: &Context) -> Html { 144 | let link = ctx.link(); 145 | let props = ctx.props(); 146 | 147 | let on_share_click = link.callback(|_| ShareButtonMsg::GenerateShareUrl); 148 | 149 | let button_class = if props.is_enabled { 150 | "btn btn-primary btn-sm" 151 | } else { 152 | "btn btn-disabled btn-sm" 153 | }; 154 | 155 | html! { 156 | 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the core data structures and types used throughout the Turing Machine 2 | //! simulator, including program representation, transitions, execution results, and error types. 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | use thiserror::Error; 7 | 8 | use crate::Rule; 9 | 10 | /// The default blank symbol used on the Turing Machine tape. 11 | pub const DEFAULT_BLANK_SYMBOL: char = ' '; 12 | /// A special input symbol used in program definitions to represent the blank symbol. 13 | pub const INPUT_BLANK_SYMBOL: char = '_'; 14 | /// The maximum allowed size for a Turing Machine program in bytes. 15 | pub const MAX_PROGRAM_SIZE: usize = 65536; // 64KB 16 | /// The maximum number of steps to execute before halting. 17 | pub const MAX_EXECUTION_STEPS: usize = 10000; 18 | 19 | /// Represents a Turing Machine program, supporting both single and multi-tape configurations. 20 | /// 21 | /// A program defines the initial setup of the machine and its transition rules. 22 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 23 | pub struct Program { 24 | /// The name of the Turing Machine program. 25 | pub name: String, 26 | /// Execution mode of the simulator. 27 | pub mode: Mode, 28 | /// The initial state of the Turing Machine. 29 | pub initial_state: String, 30 | /// A vector of strings, where each string represents the initial content of a tape. 31 | pub tapes: Vec, 32 | /// A vector of head positions, one for each tape, indicating the initial position of the head. 33 | pub heads: Vec, 34 | /// The blank symbol used on the tapes. 35 | pub blank: char, 36 | /// A hash map representing the transition rules. The key is the current state, 37 | /// and the value is a vector of possible `Transition`s from that state. 38 | pub rules: HashMap>, 39 | } 40 | 41 | /// The execution mode for a Turing Machine program. 42 | /// 43 | /// Controls how the simulator handles undefined transitions: 44 | /// - `Normal` (default): undefined transitions halt the machine normally (faithful to TM theory). 45 | /// - `Strict`: undefined transitions trigger an error, useful for debugging or catching missing rules. 46 | #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] 47 | pub enum Mode { 48 | /// Undefined transitions halt normally. 49 | #[default] 50 | Normal, 51 | /// Undefined transitions are treated as errors. 52 | Strict, 53 | } 54 | 55 | impl Program { 56 | /// Returns the initial content of the first tape as a `String`. 57 | /// This is a convenience method for single-tape compatibility. 58 | pub fn initial_tape(&self) -> String { 59 | self.tapes.first().cloned().unwrap_or_default() 60 | } 61 | 62 | /// Returns the initial head position of the first tape. 63 | /// This is a convenience method for single-tape compatibility. 64 | pub fn head_position(&self) -> usize { 65 | self.heads.first().cloned().unwrap_or(0) 66 | } 67 | 68 | /// Checks if the program is configured for a single-tape Turing Machine. 69 | pub fn is_single_tape(&self) -> bool { 70 | self.tapes.len() == 1 71 | } 72 | 73 | pub fn tapes(&self) -> Vec> { 74 | self.tapes 75 | .iter() 76 | .map(|tape| tape.chars().collect()) 77 | .collect() 78 | } 79 | } 80 | 81 | /// Represents a single transition rule for a Turing Machine. 82 | /// 83 | /// A transition defines how the machine behaves when it is in a certain state 84 | /// and reads specific symbols from its tapes. 85 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 86 | pub struct Transition { 87 | /// A vector of characters to be read from each tape. 88 | pub read: Vec, 89 | /// A vector of characters to be written to each tape. 90 | pub write: Vec, 91 | /// A vector of directions for each tape's head to move after the transition. 92 | pub directions: Vec, 93 | /// The next state the machine transitions to. 94 | pub next_state: String, 95 | } 96 | 97 | /// Represents the possible directions a Turing Machine head can move. 98 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 99 | pub enum Direction { 100 | /// Move the head one position to the left. 101 | Left, 102 | /// Move the head one position to the right. 103 | Right, 104 | /// Keep the head in the same position. 105 | Stay, 106 | } 107 | 108 | /// Represents the outcome of a Turing Machine execution step. 109 | #[derive(Debug, Clone, PartialEq)] 110 | pub enum Step { 111 | /// The machine successfully performed a step and continues execution. 112 | Continue, 113 | /// The machine has halted (reached a state with no outgoing transitions). 114 | Halt(Halt), 115 | } 116 | 117 | #[derive(Debug, Clone, PartialEq)] 118 | pub enum Halt { 119 | /// Halted in a specific state (no outgoing transitions). 120 | Ok, 121 | 122 | Err(TuringMachineError), 123 | } 124 | 125 | /// Details of a rejection outcome. 126 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 127 | pub struct Rejection { 128 | pub state: String, 129 | pub symbols: Vec, 130 | } 131 | 132 | /// Represents various errors that can occur during Turing Machine operations. 133 | #[derive(Debug, Clone, PartialEq, Error)] 134 | pub enum TuringMachineError { 135 | /// Indicates an attempt to transition to an invalid or undefined state. 136 | #[error("Invalid state: {0}")] 137 | InvalidState(String), 138 | /// Indicates that there's no rule defined for a particular set of symbols. 139 | #[error("No rule defined for state {0} and symbols {1:?}")] 140 | UndefinedTransition(String, Vec), 141 | /// Indicates that a tape head attempted to move beyond the defined tape boundaries. 142 | #[error("Tape boundary exceeded")] 143 | TapeBoundary, 144 | /// Indicates an error during the parsing of a Turing Machine program definition. 145 | #[error("Program parsing error: {0}")] 146 | ParseError(#[from] Box>), 147 | /// Indicates an error during the validation of a Turing Machine program's structure or logic. 148 | #[error("Program validation error: {0}")] 149 | ValidationError(String), 150 | /// Indicates an error related to file system operations, such as reading or writing program files. 151 | #[error("File error: {0}")] 152 | FileError(String), 153 | } 154 | 155 | #[cfg(test)] 156 | mod tests { 157 | use super::*; 158 | 159 | #[test] 160 | fn test_direction_serialization() { 161 | let left = Direction::Left; 162 | let right = Direction::Right; 163 | 164 | let left_json = serde_json::to_string(&left).unwrap(); 165 | let right_json = serde_json::to_string(&right).unwrap(); 166 | 167 | assert_eq!(left_json, "\"Left\""); 168 | assert_eq!(right_json, "\"Right\""); 169 | 170 | let left_deserialized: Direction = serde_json::from_str(&left_json).unwrap(); 171 | let right_deserialized: Direction = serde_json::from_str(&right_json).unwrap(); 172 | 173 | assert_eq!(left, left_deserialized); 174 | assert_eq!(right, right_deserialized); 175 | } 176 | 177 | #[test] 178 | fn test_transition_creation() { 179 | let transition = Transition { 180 | read: vec!['A'], 181 | write: vec!['X'], 182 | directions: vec![Direction::Right], 183 | next_state: "q1".to_string(), 184 | }; 185 | 186 | assert_eq!(transition.write, vec!['X']); 187 | assert_eq!(transition.directions, vec![Direction::Right]); 188 | assert_eq!(transition.next_state, "q1"); 189 | } 190 | 191 | #[test] 192 | fn test_error_display() { 193 | let error = TuringMachineError::InvalidState("q0".to_string()); 194 | 195 | let error_msg = format!("{}", error); 196 | assert!(error_msg.contains("Invalid state")); 197 | assert!(error_msg.contains("q0")); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/loader.rs: -------------------------------------------------------------------------------- 1 | //! This module provides the `ProgramLoader` struct, responsible for loading Turing Machine 2 | //! programs from various sources, including files and strings. 3 | 4 | use crate::parser::parse; 5 | use crate::types::{Program, TuringMachineError}; 6 | use std::fs; 7 | use std::path::{Path, PathBuf}; 8 | 9 | /// `ProgramLoader` is a utility struct for loading Turing Machine programs. 10 | /// It provides methods to load programs from individual files, from string content, 11 | /// and to discover and load all `.tur` files within a specified directory. 12 | pub struct ProgramLoader; 13 | 14 | impl ProgramLoader { 15 | /// Loads a single Turing Machine program from the specified file path. 16 | /// 17 | /// # Arguments 18 | /// 19 | /// * `path` - A reference to the `Path` of the `.tur` file to load. 20 | /// 21 | /// # Returns 22 | /// 23 | /// * `Ok(Program)` if the file is successfully read and parsed into a `Program`. 24 | /// * `Err(TuringMachineError::FileError)` if the file cannot be read. 25 | /// * `Err(TuringMachineError::ParseError)` if the file content is not a valid program. 26 | pub fn load_program(path: &Path) -> Result { 27 | let content = fs::read_to_string(path).map_err(|e| { 28 | TuringMachineError::FileError(format!("Failed to read file {}: {}", path.display(), e)) 29 | })?; 30 | 31 | parse(&content) 32 | } 33 | 34 | /// Loads a single Turing Machine program from the provided string content. 35 | /// 36 | /// This is useful for parsing programs that are not stored in files, e.g., from user input. 37 | /// 38 | /// # Arguments 39 | /// 40 | /// * `content` - A string slice containing the Turing Machine program definition. 41 | /// 42 | /// # Returns 43 | /// 44 | /// * `Ok(Program)` if the content is successfully parsed into a `Program`. 45 | /// * `Err(TuringMachineError::ParseError)` if the content is not a valid program. 46 | pub fn load_program_from_string(content: &str) -> Result { 47 | parse(content) 48 | } 49 | 50 | /// Loads all valid Turing Machine program files (`.tur` extension) from a given directory. 51 | /// 52 | /// It iterates through the directory, attempts to load each `.tur` file, and collects 53 | /// the results. Directories and non-`.tur` files are skipped. 54 | /// 55 | /// # Arguments 56 | /// 57 | /// * `directory` - A reference to the `Path` of the directory to scan for programs. 58 | /// 59 | /// # Returns 60 | /// 61 | /// * `Vec>` - A vector where each element 62 | /// is a `Result` indicating whether a program was successfully loaded (containing its 63 | /// path and the `Program` itself) or if an error occurred during loading (containing 64 | /// a `TuringMachineError`). 65 | pub fn load_programs(directory: &Path) -> Vec> { 66 | if !directory.exists() { 67 | return vec![Err(TuringMachineError::FileError(format!( 68 | "Directory {} does not exist", 69 | directory.display() 70 | )))]; 71 | } 72 | 73 | let entries = match fs::read_dir(directory) { 74 | Ok(entries) => entries, 75 | Err(e) => { 76 | return vec![Err(TuringMachineError::FileError(format!( 77 | "Failed to read directory {}: {}", 78 | directory.display(), 79 | e 80 | )))] 81 | } 82 | }; 83 | 84 | entries 85 | .filter_map(|entry| { 86 | let entry = match entry { 87 | Ok(entry) => entry, 88 | Err(e) => { 89 | return Some(Err(TuringMachineError::FileError(format!( 90 | "Failed to read directory entry: {}", 91 | e 92 | )))) 93 | } 94 | }; 95 | 96 | let path = entry.path(); 97 | 98 | // Skip directories and non-.tur files 99 | if path.is_dir() || path.extension().is_none_or(|ext| ext != "tur") { 100 | return None; 101 | } 102 | 103 | match Self::load_program(&path) { 104 | Ok(program) => Some(Ok((path, program))), 105 | Err(e) => Some(Err(TuringMachineError::FileError(format!( 106 | "Failed to load program from {}: {}", 107 | path.display(), 108 | e 109 | )))), 110 | } 111 | }) 112 | .collect() 113 | } 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use super::*; 119 | use std::fs::File; 120 | use std::io::Write; 121 | use tempfile::tempdir; 122 | 123 | #[test] 124 | fn test_load_valid_program() { 125 | let dir = tempdir().unwrap(); 126 | let file_path = dir.path().join("test.tur"); 127 | 128 | let program_content = 129 | "name: Test Program\ntape: a\nrules:\n start:\n a -> b, R, stop\n stop:"; 130 | 131 | let mut file = File::create(&file_path).unwrap(); 132 | file.write_all(program_content.as_bytes()).unwrap(); 133 | 134 | let result = ProgramLoader::load_program(&file_path); 135 | assert!(result.is_ok()); 136 | 137 | let program = result.unwrap(); 138 | assert_eq!(program.name, "Test Program"); 139 | assert_eq!(program.initial_tape(), "a"); 140 | assert!(program.rules.contains_key("start")); 141 | assert!(program.rules.contains_key("stop")); 142 | } 143 | 144 | #[test] 145 | fn test_load_invalid_program() { 146 | let dir = tempdir().unwrap(); 147 | let file_path = dir.path().join("invalid.tur"); 148 | 149 | let invalid_content = "This is not a valid program"; 150 | 151 | let mut file = File::create(&file_path).unwrap(); 152 | file.write_all(invalid_content.as_bytes()).unwrap(); 153 | 154 | let result = ProgramLoader::load_program(&file_path); 155 | assert!(result.is_err()); 156 | } 157 | 158 | #[test] 159 | fn test_load_programs_from_directory() { 160 | let dir = tempdir().unwrap(); 161 | 162 | // Create a valid program file 163 | let valid_path = dir.path().join("valid.tur"); 164 | let valid_content = 165 | "name: Valid Program\ntape: a\nrules:\n start:\n a -> b, R, stop\n stop:"; 166 | let mut valid_file = File::create(&valid_path).unwrap(); 167 | valid_file.write_all(valid_content.as_bytes()).unwrap(); 168 | 169 | // Create an invalid program file 170 | let invalid_path = dir.path().join("invalid.tur"); 171 | let invalid_content = "This is not a valid program"; 172 | let mut invalid_file = File::create(&invalid_path).unwrap(); 173 | invalid_file.write_all(invalid_content.as_bytes()).unwrap(); 174 | 175 | // Create a non-.tur file that should be ignored 176 | let ignored_path = dir.path().join("ignored.txt"); 177 | let ignored_content = "This file should be ignored"; 178 | let mut ignored_file = File::create(&ignored_path).unwrap(); 179 | ignored_file.write_all(ignored_content.as_bytes()).unwrap(); 180 | 181 | let results = ProgramLoader::load_programs(dir.path()); 182 | 183 | // We should have 2 results: 1 success and 1 error 184 | assert_eq!(results.len(), 2); 185 | 186 | let mut success_count = 0; 187 | let mut error_count = 0; 188 | 189 | for result in results { 190 | match result { 191 | Ok(_) => success_count += 1, 192 | Err(_) => error_count += 1, 193 | } 194 | } 195 | 196 | assert_eq!(success_count, 1); 197 | assert_eq!(error_count, 1); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /platforms/web/turing-editor.js: -------------------------------------------------------------------------------- 1 | window.programmaticUpdate = false; 2 | 3 | window.addEventListener("load", () => { 4 | console.log("Cytoscape version:", cytoscape.version); 5 | console.log("Cytoscape loaded successfully"); 6 | 7 | // Set up graph theme immediately - don't wait for CodeMirror 8 | window.graphTheme = { 9 | defaultNodeColor: "#00d3bb", 10 | activeNodeColor: "#fdcf2b", 11 | edgeColor: "#ccc", 12 | edgeHighlightColor: "#ffaf80", 13 | nodeTextColor: "white", 14 | edgeTextBackgroundColor: "white", 15 | }; 16 | 17 | console.log("Graph theme initialized"); 18 | 19 | // Initialize CodeMirror-based syntax highlighting 20 | if (typeof CodeMirror !== "undefined") { 21 | console.log("CodeMirror loaded successfully"); 22 | 23 | // Define Turing machine mode for CodeMirror 24 | CodeMirror.defineMode("turing", function () { 25 | return { 26 | startState: function () { 27 | return { 28 | inName: false, 29 | inTape: false, 30 | }; 31 | }, 32 | token: function (stream, state) { 33 | // Reset state at the beginning of a new line 34 | if (stream.sol()) { 35 | state.inName = false; 36 | state.inTape = false; 37 | } 38 | 39 | // Comments 40 | if (stream.match(/^#.*/)) { 41 | return "comment"; 42 | } 43 | 44 | // Consume any leading whitespace before other tokens 45 | if (stream.eatSpace()) { 46 | return null; 47 | } 48 | 49 | // Section headers 50 | if ( 51 | stream.match( 52 | /^(name|mode|head|heads|blank|tape|tapes|states|rules):/, 53 | ) 54 | ) { 55 | const matched = stream.current(); 56 | if (matched.startsWith("name:")) { 57 | state.inName = true; 58 | } else if ( 59 | matched.startsWith("tape:") || 60 | matched.startsWith("tapes:") 61 | ) { 62 | state.inTape = true; 63 | } 64 | return "keyword"; 65 | } 66 | 67 | // Handle content after 'name:' 68 | if (state.inName) { 69 | if (stream.eatSpace()) { 70 | return null; 71 | } 72 | stream.skipToEnd(); 73 | return "string"; 74 | } 75 | 76 | // Handle content after 'tape:' or 'tapes:' 77 | if (state.inTape) { 78 | if (stream.match(/'[^']*'/)) { 79 | // Quoted symbols 80 | return "string"; 81 | } 82 | // Unquoted symbols: match any single character that is not a reserved character 83 | // Reserved characters: #, space, comma, >, < 84 | if (stream.match(/^[^#\s,><]/)) { 85 | return "number"; // Treat as number as per user request 86 | } 87 | if (stream.match(/[,|]/)) { 88 | // Separators 89 | return "punctuation"; 90 | } 91 | // If nothing matched, consume character and continue 92 | stream.next(); 93 | return null; 94 | } 95 | 96 | // Arrows 97 | if (stream.match(/->/)) { 98 | return "operator"; 99 | } 100 | 101 | // Directions 102 | if (stream.match(/\b[LRS<>]\b/)) { 103 | return "atom"; 104 | } 105 | 106 | // State names (definitions or within transitions) 107 | // Match an identifier that starts with non-digit and contains alphanumeric/underscore. 108 | // This needs to be before general symbol matching to prioritize state names. 109 | if (stream.match(/^[a-zA-Z_][a-zA-Z0-9_]*/)) { 110 | // Check if it's a state definition (ends with ':') 111 | if (stream.peek() === ":") { 112 | stream.next(); // Consume the colon 113 | return "def"; // State definition 114 | } 115 | return "def"; // State name within a transition 116 | } 117 | 118 | // Quoted symbols (general, e.g., 'a', '$') 119 | if (stream.match(/'[^']*'/)) { 120 | return "string"; 121 | } 122 | 123 | // Numbers 124 | if (stream.match(/\b\d+\b/)) { 125 | return "number"; 126 | } 127 | 128 | // Brackets 129 | if (stream.match(/[\[\]]/)) { 130 | return "bracket"; 131 | } 132 | 133 | // Comma (as punctuation) 134 | if (stream.match(/,/)) { 135 | return "punctuation"; 136 | } 137 | 138 | // Unquoted symbols (general) 139 | // Match a single character that is not a reserved character or part of other tokens. 140 | // Reserved characters from grammar.pest: #, space, comma, >, < 141 | // Also exclude: [, ] (handled by other rules) 142 | // So, match any character NOT in: #, \s, ,, >, <, [, ] 143 | if (stream.match(/^[^#\s,><\[\]]/)) { 144 | return "number"; // User requested to treat symbols as "number" 145 | } 146 | 147 | // Default: consume character and return null (no special styling) 148 | stream.next(); 149 | return null; 150 | }, 151 | }; 152 | }); 153 | 154 | // Function to initialize CodeMirror 155 | const initCodeMirror = () => { 156 | const textarea = document.getElementById("turing-program-editor"); 157 | if (textarea && !textarea.dataset.codemirrorInitialized) { 158 | console.log("Initializing CodeMirror for textarea"); 159 | 160 | const editor = CodeMirror.fromTextArea(textarea, { 161 | mode: "turing", 162 | theme: "cobalt", 163 | lineNumbers: false, 164 | lineWrapping: true, 165 | indentUnit: 2, 166 | tabSize: 2, 167 | extraKeys: { 168 | Tab: function (cm) { 169 | cm.replaceSelection(" "); 170 | }, 171 | }, 172 | }); 173 | 174 | window.codeMirrorEditor = editor; 175 | 176 | // Sync with Yew component 177 | editor.on("change", function () { 178 | if (window.programmaticUpdate) { 179 | window.programmaticUpdate = false; // Reset flag after programmatic update is handled 180 | return; // Do not dispatch input event for programmatic changes 181 | } 182 | textarea.value = editor.getValue(); 183 | textarea.dispatchEvent(new Event("input", { bubbles: true })); 184 | }); 185 | 186 | // Ensure the editor is properly synced with the textarea's initial value 187 | // This prevents cursor jumping on first edit after initialization 188 | const initialValue = textarea.value; 189 | if (initialValue && editor.getValue() !== initialValue) { 190 | window.programmaticUpdate = true; 191 | editor.setValue(initialValue); 192 | } 193 | 194 | textarea.dataset.codemirrorInitialized = "true"; 195 | console.log("CodeMirror initialized successfully"); 196 | } 197 | }; 198 | 199 | window.updateCodeMirrorValue = (newValue) => { 200 | if (window.codeMirrorEditor) { 201 | // Only update if the value is actually different 202 | if (window.codeMirrorEditor.getValue() !== newValue) { 203 | window.programmaticUpdate = true; // Set flag before programmatic update 204 | window.codeMirrorEditor.setValue(newValue); 205 | } 206 | } else { 207 | // If CodeMirror isn't initialized yet, update the textarea directly 208 | // This ensures the value is there when CodeMirror initializes 209 | const textarea = document.getElementById("turing-program-editor"); 210 | if (textarea) { 211 | textarea.value = newValue; 212 | } 213 | } 214 | }; 215 | 216 | // Try to initialize immediately 217 | setTimeout(initCodeMirror, 500); 218 | 219 | // Also watch for DOM changes 220 | const observer = new MutationObserver(() => { 221 | initCodeMirror(); 222 | }); 223 | 224 | observer.observe(document.body, { 225 | childList: true, 226 | subtree: true, 227 | }); 228 | } else { 229 | console.error("CodeMirror failed to load"); 230 | } 231 | }); 232 | -------------------------------------------------------------------------------- /platforms/web/src/components/tape_view.rs: -------------------------------------------------------------------------------- 1 | use crate::components::MachineState; 2 | use yew::{function_component, html, Callback, Event, Html, Properties, TargetCast}; 3 | 4 | #[derive(Properties, PartialEq)] 5 | pub struct TapeViewProps { 6 | pub tapes: Vec>, 7 | pub head_positions: Vec, 8 | pub auto_play: bool, 9 | pub machine_state: MachineState, 10 | pub is_program_ready: bool, 11 | pub blank_symbol: char, 12 | pub state: String, 13 | pub step_count: usize, 14 | pub current_symbols: Vec, 15 | pub on_step: Callback<()>, 16 | pub on_reset: Callback<()>, 17 | pub on_toggle_auto: Callback<()>, 18 | pub speed: u64, 19 | pub on_speed_change: Callback, 20 | pub tape_left_offsets: Vec, 21 | pub message: String, 22 | } 23 | 24 | #[function_component(TapeView)] 25 | pub fn tape_view(props: &TapeViewProps) -> Html { 26 | let cell_width = 42; // Width of each cell (no gap) 27 | let padding_cells = 15; // Number of blank cells to show on each side for infinite tape effect 28 | 29 | let on_speed_change = props.on_speed_change.clone(); 30 | let is_machine_running = props.machine_state == MachineState::Running; 31 | 32 | html! { 33 |
34 |
35 |

{"Tapes"}

36 |
37 | 44 | 51 | 58 |
59 | 60 | 70 |
71 |
72 |
73 |
74 | {props.tapes.iter().enumerate().map(|(tape_index, tape)| { 75 | let head_position = props.head_positions.get(tape_index).cloned().unwrap_or(0); 76 | let left_offset = *props.tape_left_offsets.get(tape_index).unwrap_or(&0); 77 | 78 | // Add padding cells on the left 79 | let mut visible_tape = vec![props.blank_symbol; padding_cells - left_offset]; 80 | 81 | // Add the actual tape content 82 | for &symbol in tape { 83 | visible_tape.push(symbol); 84 | } 85 | 86 | // Add padding cells on the right 87 | visible_tape.extend(std::iter::repeat_n(props.blank_symbol, padding_cells)); 88 | 89 | // Calculate the transform to center the active cell under the head pointer 90 | let active_cell_index = padding_cells + head_position - left_offset; 91 | let cell_offset = active_cell_index as i32 * cell_width; 92 | 93 | // js_sys::eval(&format!("console.log({active_cell_index}, {cell_offset}, {left_offset})")).unwrap(); 94 | let transform_style = format!( 95 | "transform: translateX(calc(50% - {cell_offset}px - {}px)) translateY(-50%)", 96 | cell_width / 2 97 | ); 98 | 99 | html! { 100 |
101 | {html!{ 102 |
103 | {format!("#{} ", tape_index + 1)} 104 | 105 | {format!("(head: {head_position})")} 106 | 107 |
108 | }} 109 |
110 |
111 | {visible_tape.iter().enumerate().map(|(i, &symbol)| { 112 | // Check if this cell is under the head 113 | let is_under_head = i == active_cell_index; 114 | 115 | let class = if is_under_head { 116 | "tape-cell under-head" 117 | } else { 118 | "tape-cell" 119 | }; 120 | 121 | html! { 122 |
123 | {symbol} 124 |
125 | } 126 | }).collect::()} 127 |
128 |
129 |
130 |
131 |
132 |
133 | } 134 | }).collect::()} 135 |
136 | 137 |
138 |
139 | {"State"} 140 | {&props.state} 141 |
142 |
143 | {"Steps"} 144 | {props.step_count} 145 |
146 |
147 | {"Symbols"} 148 | 149 | {props.current_symbols.iter().map(|&symbol| { 150 | html! { {symbol} } 151 | }).collect::()} 152 | 153 |
154 |
155 | {"Status"} 156 | "value status halted", 158 | MachineState::Running if props.auto_play => "value status running", 159 | MachineState::Running => "value status ready", 160 | }}> 161 | {match props.machine_state { 162 | MachineState::Halted => "HALTED", 163 | MachineState::Running if props.auto_play => "RUNNING", 164 | MachineState::Running => "READY", 165 | }} 166 | 167 |
168 |
169 | {if !props.message.is_empty() { 170 | html! {
{&props.message}
} 171 | } else { 172 | html! {} 173 | }} 174 |
175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/programs.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{Program, TuringMachineError}; 2 | 3 | use std::sync::RwLock; 4 | 5 | // Default embedded programs 6 | const PROGRAM_TEXTS: [&str; 9] = [ 7 | include_str!("../examples/binary-addition.tur"), 8 | include_str!("../examples/even-zeros-and-ones.tur"), 9 | include_str!("../examples/event-number-checker.tur"), 10 | include_str!("../examples/palindrome.tur"), 11 | include_str!("../examples/subtraction.tur"), 12 | include_str!("../examples/busy-beaver-3.tur"), 13 | include_str!("../examples/multi-tape-copy.tur"), 14 | include_str!("../examples/multi-tape-addition.tur"), 15 | include_str!("../examples/multi-tape-compare.tur"), 16 | ]; 17 | 18 | lazy_static::lazy_static! { 19 | pub static ref PROGRAMS: RwLock> = RwLock::new(Vec::new()); 20 | } 21 | 22 | pub struct ProgramManager; 23 | 24 | impl ProgramManager { 25 | /// Initialize the ProgramManager with programs from the specified directory 26 | pub fn load() -> Result<(), TuringMachineError> { 27 | // Load embedded programs first 28 | let mut programs = Vec::new(); 29 | 30 | for program_text in PROGRAM_TEXTS { 31 | if let Ok(program) = crate::parser::parse(program_text) { 32 | programs.push(program); 33 | } else { 34 | eprintln!("Failed to parse program"); 35 | } 36 | } 37 | 38 | // Store the loaded programs and their texts 39 | if let Ok(mut write_guard) = PROGRAMS.write() { 40 | *write_guard = programs; 41 | } else { 42 | return Err(TuringMachineError::FileError( 43 | "Failed to acquire write lock".to_string(), 44 | )); 45 | } 46 | 47 | Ok(()) 48 | } 49 | 50 | /// Get the number of available programs 51 | pub fn count() -> usize { 52 | // Initialize with default programs if not already initialized 53 | let _ = Self::load(); 54 | 55 | PROGRAMS.read().map(|programs| programs.len()).unwrap_or(0) 56 | } 57 | 58 | /// Get a program by its index 59 | pub fn get_program_by_index(index: usize) -> Result { 60 | // Initialize with default programs if not already initialized 61 | let _ = Self::load(); 62 | 63 | PROGRAMS 64 | .read() 65 | .map_err(|_| TuringMachineError::FileError("Failed to acquire read lock".to_string()))? 66 | .get(index) 67 | .cloned() 68 | .ok_or_else(|| { 69 | TuringMachineError::ValidationError(format!("Program index {} out of range", index)) 70 | }) 71 | } 72 | 73 | /// Get a program by its name 74 | pub fn get_program_by_name(name: &str) -> Result { 75 | // Initialize with default programs if not already initialized 76 | let _ = Self::load(); 77 | 78 | PROGRAMS 79 | .read() 80 | .map_err(|_| TuringMachineError::FileError("Failed to acquire read lock".to_string()))? 81 | .iter() 82 | .find(|program| program.name == name) 83 | .cloned() 84 | .ok_or_else(|| { 85 | TuringMachineError::ValidationError(format!("Program '{}' not found", name)) 86 | }) 87 | } 88 | 89 | /// List all program names 90 | pub fn list_program_names() -> Vec { 91 | // Initialize with default programs if not already initialized 92 | let _ = Self::load(); 93 | 94 | PROGRAMS 95 | .read() 96 | .map(|programs| { 97 | programs 98 | .iter() 99 | .map(|program| program.name.clone()) 100 | .collect() 101 | }) 102 | .unwrap_or_else(|_| Vec::new()) 103 | } 104 | 105 | /// Get information about a program by its index 106 | pub fn get_program_info(index: usize) -> Result { 107 | let program = Self::get_program_by_index(index)?; 108 | 109 | Ok(ProgramInfo { 110 | index, 111 | name: program.name.clone(), 112 | initial_state: program.initial_state.clone(), 113 | initial_tape: program.initial_tape(), 114 | state_count: program.rules.len(), 115 | transition_count: program 116 | .rules 117 | .values() 118 | .map(|transitions| transitions.len()) 119 | .sum(), 120 | }) 121 | } 122 | 123 | /// Search for programs by name 124 | pub fn search_programs(query: &str) -> Vec { 125 | // Initialize with default programs if not already initialized 126 | let _ = Self::load(); 127 | 128 | PROGRAMS 129 | .read() 130 | .map(|programs| { 131 | programs 132 | .iter() 133 | .enumerate() 134 | .filter(|(_, program)| { 135 | program.name.to_lowercase().contains(&query.to_lowercase()) 136 | }) 137 | .map(|(index, _)| index) 138 | .collect() 139 | }) 140 | .unwrap_or_else(|_| Vec::new()) 141 | } 142 | 143 | /// Get the original text of a program by its index 144 | pub fn get_program_text_by_index(index: usize) -> Result<&'static str, TuringMachineError> { 145 | // Initialize with default programs if not already initialized 146 | let _ = Self::load(); 147 | 148 | PROGRAM_TEXTS.get(index).cloned().ok_or_else(|| { 149 | TuringMachineError::ValidationError(format!( 150 | "Program text index {} out of range", 151 | index 152 | )) 153 | }) 154 | } 155 | } 156 | 157 | #[derive(Debug, Clone)] 158 | pub struct ProgramInfo { 159 | pub index: usize, 160 | pub name: String, 161 | pub initial_state: String, 162 | pub initial_tape: String, 163 | pub state_count: usize, 164 | pub transition_count: usize, 165 | } 166 | 167 | #[cfg(test)] 168 | mod tests { 169 | use super::*; 170 | use crate::analyze; 171 | use crate::machine::TuringMachine; 172 | use crate::types::Step; 173 | use std::fs::File; 174 | use std::io::Write; 175 | use tempfile::tempdir; 176 | 177 | #[test] 178 | fn test_program_manager_initialization() { 179 | // Initialize with default programs 180 | let result = ProgramManager::load(); 181 | assert!(result.is_ok()); 182 | 183 | // Check that we have the expected number of programs 184 | assert!(ProgramManager::count() >= 4); 185 | } 186 | 187 | #[test] 188 | fn test_program_manager_with_custom_directory() { 189 | let dir = tempdir().unwrap(); 190 | 191 | // Create a custom program file 192 | let file_path = dir.path().join("custom.tur"); 193 | let content = r#" 194 | name: Custom Program 195 | tape: x 196 | rules: 197 | start: 198 | x -> y, R, stop 199 | stop:"#; 200 | 201 | let mut file = File::create(&file_path).unwrap(); 202 | file.write_all(content.as_bytes()).unwrap(); 203 | 204 | // Test that ProgramLoader can load the file directly 205 | let program = crate::loader::ProgramLoader::load_program(&file_path); 206 | assert!(program.is_ok()); 207 | 208 | let program = program.unwrap(); 209 | assert_eq!(program.name, "Custom Program"); 210 | assert_eq!(program.initial_tape(), "x"); 211 | 212 | // Test that ProgramLoader can load from directory 213 | let results = crate::loader::ProgramLoader::load_programs(dir.path()); 214 | assert_eq!(results.len(), 1); 215 | assert!(results[0].is_ok()); 216 | } 217 | 218 | #[test] 219 | fn test_all_programs_are_valid() { 220 | // Initialize with default programs 221 | let _ = ProgramManager::load(); 222 | 223 | let count = ProgramManager::count(); 224 | for i in 0..count { 225 | let program = ProgramManager::get_program_by_index(i).unwrap(); 226 | assert!( 227 | analyze(&program).is_ok(), 228 | "Program '{}' is invalid", 229 | program.name 230 | ); 231 | } 232 | } 233 | 234 | #[test] 235 | fn test_program_names() { 236 | // Initialize with default programs 237 | let _ = ProgramManager::load(); 238 | 239 | let names = ProgramManager::list_program_names(); 240 | assert!(names.contains(&"Binary addition".to_string())); 241 | assert!(names.contains(&"Palindrome Checker".to_string())); 242 | assert!(names.contains(&"Subtraction".to_string())); 243 | } 244 | 245 | #[test] 246 | fn test_programs_can_be_executed() { 247 | // Initialize with default programs 248 | let _ = ProgramManager::load(); 249 | 250 | let count = ProgramManager::count(); 251 | for i in 0..count { 252 | let program = ProgramManager::get_program_by_index(i).unwrap(); 253 | let mut machine = TuringMachine::new(program); 254 | let result = machine.step(); 255 | 256 | // Should either continue or halt, but not error or reject on first step 257 | match result { 258 | Step::Continue => {} 259 | Step::Halt(_) => {} 260 | } 261 | } 262 | } 263 | 264 | #[test] 265 | fn test_program_manager_get_program_by_index() { 266 | // Initialize with default programs 267 | let _ = ProgramManager::load(); 268 | 269 | let program = ProgramManager::get_program_by_index(0); 270 | assert!(program.is_ok()); 271 | 272 | let result = ProgramManager::get_program_by_index(999); 273 | assert!(result.is_err()); 274 | } 275 | 276 | #[test] 277 | fn test_program_manager_get_program_by_name() { 278 | // Initialize with default programs 279 | let _ = ProgramManager::load(); 280 | 281 | let program = ProgramManager::get_program_by_name("Binary addition"); 282 | assert!(program.is_ok()); 283 | assert_eq!(program.unwrap().initial_tape(), "$00111"); 284 | 285 | let result = ProgramManager::get_program_by_name("Nonexistent"); 286 | assert!(result.is_err()); 287 | } 288 | 289 | #[test] 290 | fn test_program_manager_list_program_names() { 291 | // Initialize with default programs 292 | let _ = ProgramManager::load(); 293 | 294 | let names = ProgramManager::list_program_names(); 295 | assert!(names.len() >= 4); 296 | assert!(names.contains(&"Binary addition".to_string())); 297 | assert!(names.contains(&"Palindrome Checker".to_string())); 298 | assert!(names.contains(&"Subtraction".to_string())); 299 | } 300 | 301 | #[test] 302 | fn test_program_manager_get_program_info() { 303 | // Initialize with default programs 304 | let _ = ProgramManager::load(); 305 | 306 | let info = ProgramManager::get_program_info(0); 307 | assert!(info.is_ok()); 308 | 309 | let info = info.unwrap(); 310 | assert_eq!(info.index, 0); 311 | assert!(!info.name.is_empty()); 312 | assert!(!info.initial_tape.is_empty()); 313 | assert!(info.state_count > 0); 314 | assert!(info.transition_count > 0); 315 | 316 | let result = ProgramManager::get_program_info(999); 317 | assert!(result.is_err()); 318 | } 319 | 320 | #[test] 321 | fn test_program_manager_search_programs() { 322 | // Initialize with default programs 323 | let _ = ProgramManager::load(); 324 | 325 | let results = ProgramManager::search_programs("binary"); 326 | assert!(!results.is_empty()); // "Binary addition" 327 | 328 | let results = ProgramManager::search_programs("palindrome"); 329 | assert!(!results.is_empty()); 330 | 331 | let results = ProgramManager::search_programs("nonexistent"); 332 | assert_eq!(results.len(), 0); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /platforms/web/src/components/graph_view.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use tur::{Program, Transition}; 3 | use yew::prelude::*; 4 | 5 | #[derive(Properties, Clone)] 6 | pub struct GraphViewProps { 7 | pub program: Program, 8 | pub current_state: String, 9 | pub previous_state: String, 10 | pub last_transition: Option, 11 | pub step_count: usize, 12 | } 13 | 14 | impl PartialEq for GraphViewProps { 15 | fn eq(&self, other: &Self) -> bool { 16 | self.current_state == other.current_state 17 | && self.previous_state == other.previous_state 18 | && self.last_transition == other.last_transition 19 | && self.step_count == other.step_count 20 | && self.program == other.program 21 | } 22 | } 23 | 24 | pub struct GraphView { 25 | container_ref: NodeRef, 26 | } 27 | 28 | pub enum Msg {} 29 | 30 | impl Component for GraphView { 31 | type Message = Msg; 32 | type Properties = GraphViewProps; 33 | 34 | fn create(_ctx: &Context) -> Self { 35 | Self { 36 | container_ref: NodeRef::default(), 37 | } 38 | } 39 | 40 | fn update(&mut self, _ctx: &Context, _msg: Self::Message) -> bool { 41 | false 42 | } 43 | 44 | fn view(&self, _ctx: &Context) -> Html { 45 | html! { 46 |
47 |
52 |
53 |
54 | } 55 | } 56 | 57 | fn rendered(&mut self, ctx: &Context, first_render: bool) { 58 | if first_render { 59 | self.init_graph(ctx); 60 | } 61 | // Always update node styles on render to reflect current state 62 | self.update_node_styles(ctx); 63 | } 64 | 65 | fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { 66 | if ctx.props().program != old_props.program { 67 | self.init_graph(ctx); // Re-initialize if the program changes 68 | } else { 69 | self.update_node_styles(ctx); 70 | } 71 | 72 | if ctx.props().last_transition.is_some() && ctx.props().step_count != old_props.step_count { 73 | // Find the specific edge_id that corresponds to the last transition 74 | if let Some(last_trans) = &ctx.props().last_transition { 75 | let from_state = &ctx.props().previous_state; 76 | let to_state = &last_trans.next_state; 77 | 78 | // Iterate through the program rules to find the matching edge_id 79 | let mut found_edge_id: Option = None; 80 | if let Some(transitions_for_state) = ctx.props().program.rules.get(from_state) { 81 | for (i, transition) in transitions_for_state.iter().enumerate() { 82 | // Compare the full transition struct to ensure exact match 83 | if transition == last_trans { 84 | found_edge_id = Some(format!("{}-{}-{}", from_state, to_state, i)); 85 | break; 86 | } 87 | } 88 | } 89 | 90 | if let Some(edge_id) = found_edge_id { 91 | self.animate_state_transition(&edge_id); 92 | } 93 | } 94 | } 95 | 96 | true 97 | } 98 | } 99 | 100 | impl GraphView { 101 | fn get_graph_elements_json(&self, ctx: &Context) -> String { 102 | let props = ctx.props(); 103 | let mut elements = Vec::new(); 104 | 105 | // Add nodes 106 | let mut all_states = std::collections::HashSet::new(); 107 | props.program.rules.keys().for_each(|s| { 108 | all_states.insert(s.clone()); 109 | }); 110 | props.program.rules.values().flatten().for_each(|t| { 111 | all_states.insert(t.next_state.clone()); 112 | }); 113 | if !props.current_state.is_empty() { 114 | all_states.insert(props.current_state.clone()); 115 | } 116 | if all_states.is_empty() { 117 | all_states.insert("start".to_string()); 118 | } 119 | 120 | for state in &all_states { 121 | let classes = String::new(); 122 | // Initial classes, will be updated by update_node_styles 123 | elements.push(json!({ 124 | "data": { 125 | "id": state, 126 | }, 127 | "classes": classes.trim() 128 | })); 129 | } 130 | 131 | // Add edges 132 | for (from_state, transitions) in &props.program.rules { 133 | for (i, transition) in transitions.iter().enumerate() { 134 | let to_state = &transition.next_state; 135 | let edge_id = format!("{}-{}-{}", from_state, to_state, i); 136 | let label = self.format_transition_label(transition); 137 | 138 | elements.push(json!({ 139 | "data": { 140 | "id": edge_id, 141 | "source": from_state, 142 | "target": to_state, 143 | "label": label 144 | } 145 | })); 146 | } 147 | } 148 | 149 | serde_json::to_string(&elements).unwrap_or_default() 150 | } 151 | 152 | fn init_graph(&self, ctx: &Context) { 153 | let elements_json = self.get_graph_elements_json(ctx); 154 | let init_code = format!( 155 | r#" 156 | (function initGraphWithRetry() {{ 157 | let retryCount = 0; 158 | const maxRetries = 10; 159 | 160 | function tryInit() {{ 161 | if (typeof cytoscape === 'undefined') {{ 162 | if (retryCount < maxRetries) {{ 163 | retryCount++; 164 | setTimeout(tryInit, 200); 165 | }} 166 | return; 167 | }} 168 | 169 | if (typeof window.graphTheme === 'undefined') {{ 170 | if (retryCount < maxRetries) {{ 171 | retryCount++; 172 | setTimeout(tryInit, 200); 173 | }} 174 | return; 175 | }} 176 | 177 | const container = document.getElementById('graph-container'); 178 | if (!container) {{ 179 | if (retryCount < maxRetries) {{ 180 | retryCount++; 181 | setTimeout(tryInit, 200); 182 | }} 183 | return; 184 | }} 185 | 186 | try {{ 187 | const gt = window.graphTheme; 188 | 189 | window.graphCy = cytoscape({{ 190 | container: container, 191 | elements: JSON.parse(`{}`), 192 | style: [ 193 | {{ selector: 'node', style: {{ 'background-color': gt.defaultNodeColor, 'label': 'data(id)', 'color': 'white', 'text-valign': 'center', 'text-halign': 'center', 'font-size': '14px', 'width': '50px', 'height': '50px', 'border-width': '0px' }} }}, 194 | {{ selector: '.current', style: {{ 'background-color': gt.activeNodeColor }} }}, 195 | {{ selector: '.previous', style: {{ 'background-color': gt.defaultNodeColor }} }}, 196 | {{ selector: '.halt', style: {{ 'background-color': gt.defaultNodeColor }} }}, 197 | {{ selector: 'edge', style: {{ 'width': 2, 'line-color': gt.edgeColor, 'target-arrow-color': gt.edgeColor, 'target-arrow-shape': 'triangle', 'curve-style': 'bezier', 'font-family': '"Fira Code", monospace', 'font-size': '12px', 'color': '#444', 'text-background-color': '#F5F7FA', 'text-background-opacity': 0.8 }} }}, 198 | {{ selector: 'edge[label]', style: {{ 'label': 'data(label)', 'text-wrap': 'wrap', 'text-max-width': '120px' }} }} 199 | ], 200 | layout: {{ name: 'circle', padding: 30 }}, 201 | ready: function() {{ 202 | this.fit(null, 20); 203 | const startNode = this.getElementById('start'); 204 | if (startNode.length > 0) {{ 205 | startNode.addClass('current'); 206 | }} 207 | }} 208 | }}); 209 | }} catch (error) {{ 210 | if (retryCount < maxRetries) {{ 211 | retryCount++; 212 | setTimeout(tryInit, 500); 213 | }} 214 | }} 215 | }} 216 | 217 | tryInit(); 218 | }})(); 219 | "#, 220 | elements_json 221 | ); 222 | let _ = js_sys::eval(&init_code); 223 | } 224 | 225 | fn update_node_styles(&self, ctx: &Context) { 226 | let props = ctx.props(); 227 | let update_code = format!( 228 | r#" 229 | if (window.graphCy && window.graphTheme) {{ 230 | const gt = window.graphTheme; 231 | const allNodes = window.graphCy.nodes(); 232 | const previousNode = window.graphCy.getElementById('{}'); 233 | const currentNode = window.graphCy.getElementById('{}'); 234 | 235 | allNodes.stop(true, true).style({{ 236 | 'background-color': gt.defaultNodeColor, 237 | 'border-width': '0px' 238 | }}); 239 | allNodes.removeClass('current previous'); 240 | 241 | if ('{}' !== '{}') {{ 242 | if (previousNode.length > 0) {{ 243 | previousNode.style({{ 244 | 'background-color': gt.defaultNodeColor 245 | }}); 246 | previousNode.addClass('previous'); 247 | }} 248 | if (currentNode.length > 0) {{ 249 | currentNode.animate({{ 250 | style: {{ 'background-color': gt.activeNodeColor }}, 251 | duration: 400 252 | }}); 253 | currentNode.addClass('current'); 254 | }} 255 | }} else {{ 256 | if (currentNode.length > 0) {{ 257 | currentNode.animate({{ 258 | style: {{ 'background-color': gt.activeNodeColor }}, 259 | duration: 400 260 | }}); 261 | currentNode.addClass('current'); 262 | }} 263 | }} 264 | }} 265 | "#, 266 | props.previous_state, props.current_state, props.previous_state, props.current_state 267 | ); 268 | let _ = js_sys::eval(&update_code); 269 | } 270 | 271 | fn animate_state_transition(&self, edge_id: &str) { 272 | let animate_code = format!( 273 | r#" 274 | if (window.graphCy && window.graphTheme) {{ 275 | const gt = window.graphTheme; 276 | // Stop all ongoing animations on all edges and reset their styles 277 | window.graphCy.edges().stop(true, true).style({{'line-color': gt.edgeColor, 'target-arrow-color': gt.edgeColor, 'width': 2}}); 278 | 279 | const edgeToAnimate = window.graphCy.getElementById('{}'); 280 | 281 | if (edgeToAnimate.length > 0) {{ 282 | edgeToAnimate.animate({{ 283 | style: {{'line-color': gt.edgeHighlightColor, 'target-arrow-color': gt.edgeHighlightColor, 'width': 4}}, 284 | duration: 1, 285 | complete: function() {{ 286 | edgeToAnimate.animate({{ 287 | style: {{'line-color': gt.edgeColor, 'target-arrow-color': gt.edgeColor, 'width': 2}}, 288 | duration: 400 289 | }}); 290 | }} 291 | }}); 292 | }} else {{ 293 | console.warn(`Edge with ID '{}' not found for animation.`, edge_id); 294 | }} 295 | }} 296 | "#, 297 | edge_id, edge_id 298 | ); 299 | let _ = js_sys::eval(&animate_code); 300 | } 301 | 302 | fn format_transition_label(&self, transition: &Transition) -> String { 303 | let read_str: String = transition.read.iter().collect(); 304 | let write_str: String = transition.write.iter().collect(); 305 | let dir_str: String = transition 306 | .directions 307 | .iter() 308 | .map(|d| match d { 309 | tur::Direction::Left => "L", 310 | tur::Direction::Right => "R", 311 | tur::Direction::Stay => "S", 312 | }) 313 | .collect::>() 314 | .join(","); 315 | 316 | format!("{} -> {}, {}", read_str, write_str, dir_str) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /platforms/web/src/components/program_editor.rs: -------------------------------------------------------------------------------- 1 | use crate::components::ProgramSelector; 2 | use tur::{parser::parse, Program, TuringMachineError, MAX_PROGRAM_SIZE}; 3 | use web_sys::HtmlTextAreaElement; 4 | use yew::prelude::*; 5 | 6 | #[derive(Properties, PartialEq)] 7 | pub struct ProgramEditorProps { 8 | pub program_text: String, 9 | pub is_ready: bool, 10 | pub on_program_submit: Callback, 11 | pub on_error: Callback, 12 | pub on_text_change: Callback, 13 | pub current_program: usize, 14 | pub on_select_program: Callback, 15 | pub program_name: String, 16 | pub show_help_modal_from_parent: bool, 17 | pub on_close_help_modal: Callback<()>, 18 | } 19 | 20 | pub enum ProgramEditorMsg { 21 | UpdateText(String), 22 | ValidateProgram, 23 | OpenHelpModal, 24 | CloseHelpModal, 25 | } 26 | 27 | pub struct ProgramEditor { 28 | program_text: String, 29 | parse_error: Option, 30 | is_valid: bool, 31 | validation_timeout: Option, 32 | show_help_modal: bool, 33 | } 34 | 35 | impl Component for ProgramEditor { 36 | type Message = ProgramEditorMsg; 37 | type Properties = ProgramEditorProps; 38 | 39 | fn create(ctx: &Context) -> Self { 40 | Self { 41 | program_text: ctx.props().program_text.clone(), 42 | parse_error: None, 43 | is_valid: ctx.props().is_ready, 44 | validation_timeout: None, 45 | show_help_modal: false, 46 | } 47 | } 48 | 49 | fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { 50 | let mut should_render = false; 51 | 52 | if old_props.current_program != ctx.props().current_program { 53 | self.program_text = ctx.props().program_text.clone(); 54 | self.is_valid = ctx.props().is_ready; 55 | self.parse_error = None; 56 | 57 | if let Some(timeout) = self.validation_timeout.take() { 58 | timeout.cancel(); 59 | } 60 | 61 | let escaped_value = serde_json::to_string(&self.program_text).unwrap(); 62 | js_sys::eval(&format!("window.updateCodeMirrorValue({})", escaped_value)).unwrap(); 63 | 64 | should_render = true; 65 | } 66 | 67 | if old_props.show_help_modal_from_parent != ctx.props().show_help_modal_from_parent { 68 | self.show_help_modal = ctx.props().show_help_modal_from_parent; 69 | should_render = true; 70 | } 71 | 72 | should_render 73 | } 74 | 75 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 76 | match msg { 77 | ProgramEditorMsg::UpdateText(text) => { 78 | if text.len() > MAX_PROGRAM_SIZE { 79 | self.parse_error = Some(format!( 80 | "Program too large: {} characters (max: {}) 81 | 82 | Please reduce the program size.", 83 | text.len(), 84 | MAX_PROGRAM_SIZE 85 | )); 86 | self.is_valid = false; 87 | return true; 88 | } 89 | 90 | self.program_text = text; 91 | 92 | // Cancel previous validation timeout 93 | if let Some(timeout) = self.validation_timeout.take() { 94 | timeout.cancel(); 95 | } 96 | 97 | // Set up debounced validation and parent notification 98 | let delay = if self.program_text.len() > 1000 { 99 | 500 100 | } else { 101 | 300 102 | }; 103 | let link = ctx.link().clone(); 104 | let text_for_callback = self.program_text.clone(); 105 | let on_text_change_callback = ctx.props().on_text_change.clone(); 106 | self.validation_timeout = 107 | Some(gloo_timers::callback::Timeout::new(delay, move || { 108 | on_text_change_callback.emit(text_for_callback); 109 | link.send_message(ProgramEditorMsg::ValidateProgram); 110 | })); 111 | 112 | true 113 | } 114 | ProgramEditorMsg::ValidateProgram => { 115 | self.validation_timeout = None; 116 | 117 | if self.program_text.trim().is_empty() { 118 | self.parse_error = None; 119 | self.is_valid = false; 120 | ctx.props().on_error.emit("Program is empty.".to_string()); 121 | } else { 122 | match parse(&self.program_text) { 123 | Ok(program) => { 124 | self.parse_error = None; 125 | self.is_valid = true; 126 | ctx.props().on_program_submit.emit(program); 127 | } 128 | Err(e) => { 129 | let error_msg = format_parse_error(&e); 130 | self.parse_error = Some(error_msg.clone()); 131 | self.is_valid = false; 132 | ctx.props().on_error.emit(error_msg); 133 | } 134 | } 135 | } 136 | true 137 | } 138 | ProgramEditorMsg::OpenHelpModal => { 139 | self.show_help_modal = true; 140 | true 141 | } 142 | ProgramEditorMsg::CloseHelpModal => { 143 | self.show_help_modal = false; 144 | ctx.props().on_close_help_modal.emit(()); 145 | true 146 | } 147 | } 148 | } 149 | 150 | fn view(&self, ctx: &Context) -> Html { 151 | let link = ctx.link(); 152 | 153 | let on_input = { 154 | let link = link.clone(); 155 | Callback::from(move |e: InputEvent| { 156 | let target = e.target_dyn_into::().unwrap(); 157 | link.send_message(ProgramEditorMsg::UpdateText(target.value())); 158 | }) 159 | }; 160 | 161 | let on_keydown = { 162 | let _link = link.clone(); 163 | Callback::from(move |_e: KeyboardEvent| { 164 | // No longer needed as program updates live 165 | }) 166 | }; 167 | 168 | let on_modal_keydown = { 169 | let link = link.clone(); 170 | Callback::from(move |e: KeyboardEvent| { 171 | if e.key() == "Escape" { 172 | link.send_message(ProgramEditorMsg::CloseHelpModal); 173 | } 174 | }) 175 | }; 176 | 177 | html! { 178 |
179 | 184 |
185 |