From 132380e777878072f81432bd6bb1f86585097047 Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:20:42 -0500 Subject: [PATCH 1/4] sync --- web/root/src/visualizer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index a945b88..ac3df28 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -277,8 +277,6 @@ function renderNode({ }: any) { return { drawNode() { - // @ts-expect-error bad library - const node: vis.Node = nodes.get(id)!; const t = getGraphTheme(); const r = Math.max(14, style?.size ?? 18); @@ -355,6 +353,8 @@ function renderNode({ // } // } + // @ts-expect-error bad library + const node: vis.Node = nodes.get(id)!; const physicsOff = node.physics === false; if (physicsOff) { drawPinIndicator(ctx, x, y, r, t.node_anchor); From 34b20ec1fe0a6c9c492b1405493d84e0778e33e6 Mon Sep 17 00:00:00 2001 From: ParkerTenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Fri, 9 Jan 2026 20:13:09 -0500 Subject: [PATCH 2/4] changed how machines are parsed/represented --- Cargo.lock | 403 ++++++++++++++++++++++++++++++++++ Cargo.toml | 8 +- src/automatan/fa.rs | 278 +++++++++++++++++++++++ src/automatan/mod.rs | 43 ++++ src/automatan/pda.rs | 399 +++++++++++++++++++++++++++++++++ src/automatan/tm.rs | 346 +++++++++++++++++++++++++++++ src/lib.rs | 2 +- src/loader/ast.rs | 81 ++----- src/loader/lexer.rs | 16 +- src/loader/log.rs | 12 +- src/loader/mod.rs | 101 +++++---- src/main.rs | 35 --- src/{automata => sim}/dfa.rs | 0 src/{automata => sim}/dpda.rs | 0 src/{automata => sim}/mod.rs | 0 src/{automata => sim}/nfa.rs | 0 src/{automata => sim}/npda.rs | 0 src/{automata => sim}/ntm.rs | 0 src/{automata => sim}/tm.rs | 0 web/Cargo.toml | 2 +- web/root/src/editor.ts | 6 +- web/root/src/visualizer.ts | 1 + web/src/lib.rs | 50 +---- 23 files changed, 1577 insertions(+), 206 deletions(-) create mode 100644 src/automatan/fa.rs create mode 100644 src/automatan/mod.rs create mode 100644 src/automatan/pda.rs create mode 100644 src/automatan/tm.rs delete mode 100644 src/main.rs rename src/{automata => sim}/dfa.rs (100%) rename src/{automata => sim}/dpda.rs (100%) rename src/{automata => sim}/mod.rs (100%) rename src/{automata => sim}/nfa.rs (100%) rename src/{automata => sim}/npda.rs (100%) rename src/{automata => sim}/ntm.rs (100%) rename src/{automata => sim}/tm.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index edc3887..b8bb181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,9 +2,28 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "automata" version = "0.1.0" +dependencies = [ + "serde", + "serde_with", +] [[package]] name = "automata-web" @@ -18,18 +37,46 @@ dependencies = [ "web-sys", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -40,6 +87,152 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + [[package]] name = "itoa" version = "1.0.17" @@ -56,18 +249,51 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -86,12 +312,56 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "serde" version = "1.0.228" @@ -135,6 +405,49 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.111" @@ -146,6 +459,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -207,6 +551,65 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "zmij" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 2f19bd1..9c14234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,13 @@ version = "0.1.0" edition = "2024" [dependencies] +serde = { version = "1.0", features = ["derive"], optional = true} +serde_with = { version = "3.0", features = ["default"], optional = true} +[features] +default = [] +serde = ["dep:serde", "dep:serde_with"] [workspace] -members = ["web"] \ No newline at end of file +members = ["web"] + diff --git a/src/automatan/fa.rs b/src/automatan/fa.rs new file mode 100644 index 0000000..44f0950 --- /dev/null +++ b/src/automatan/fa.rs @@ -0,0 +1,278 @@ +use std::collections::HashSet; + +use super::*; + +use crate::loader::{ + Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned, + ast::{self, Symbol as Sym}, +}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct TransitionFrom<'a> { + pub state: State<'a>, + pub letter: Option>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct TransitionTo<'a> { + pub state: State<'a>, + + pub transition: Span, + pub function: Span, +} + +#[derive(Clone, Debug)] +#[allow(unused)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde_with::serde_as)] +pub struct Fa<'a> { + pub initial_state: State<'a>, + pub states: HashMap, StateInfo>, + pub alphabet: HashMap, LetterInfo>, + pub final_states: HashMap, StateInfo>, + + #[cfg(feature = "serde")] + #[serde_as(as = "serde_with::Seq<(_, _)>")] + pub transitions: HashMap, HashSet>>, + #[cfg(not(feature = "serde"))] + pub transitions: HashMap, HashSet>>, +} + +impl<'a> Fa<'a> { + pub fn parse( + items: impl Iterator>>, + ctx: &mut Context<'a>, + options: Options, + ) -> Option> { + + let mut initial_state = None; + + let mut states = HashMap::new(); + let mut alphabet = HashMap::new(); + let mut final_states = HashMap::new(); + + let mut transitions: HashMap, HashSet>> = + HashMap::new(); + + for Spanned(element, span) in items { + use Spanned as S; + use ast::TopLevel as TL; + match element { + TL::Item(S("Q", _), list) => { + if !states.is_empty() { + ctx.emit_error("states already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + if states + .insert(State(ident), StateInfo { definition: item.1 }) + .is_some() + { + ctx.emit_error("state redefined", item.1); + } + } + + if list.is_empty() { + ctx.emit_error("states cannot be empty", span); + } + } + TL::Item(S("E" | SIGMA_UPPER | "sigma", _), list) => { + if !alphabet.is_empty() { + ctx.emit_error("alphabet already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + + if ident.chars().count() != 1 { + ctx.emit_error("letter cannot be longer than one char", item.1); + } + + if alphabet + .insert(Letter(ident), LetterInfo { definition: item.1 }) + .is_some() + { + ctx.emit_error("letter redefined", item.1); + } + } + if list.is_empty() { + ctx.emit_error("alphabet cannot be empty", span); + } + } + TL::Item(S("F", _), list) => { + if final_states.is_empty() { + ctx.emit_error("final states already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + if states.contains_key(&State(ident)) { + if final_states + .insert(State(ident), StateInfo { definition: item.1 }) + .is_none() + { + ctx.emit_error("final state redefined", item.1); + } + } else { + ctx.emit_error("final state not defined in set of states", item.1); + } + } + } + TL::Item(S("I" | "q0", _), S(src, src_d)) => match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if initial_state.is_some() { + ctx.emit_error("initial state already set", span); + } + if states.contains_key(&State(ident)) { + initial_state = Some(State(ident)) + } else { + ctx.emit_error("initial state symbol not defined as a state", src_d); + } + } + _ => ctx.emit_error("expected ident", src_d), + }, + TL::Item(S(name, dest_s), _) => { + ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s); + } + + TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => { + let list = list.set_weak(); + let Some((state, letter)) = tuple.as_ref().expect_fa_transition_function(ctx) + else { + continue; + }; + if !states.contains_key(&State(state.0)) { + ctx.emit_error("transition state not defined as state", state.1); + continue; + }; + + let letter: Option> = match letter.0 { + Sym::Epsilon => { + if !options.epsilon_moves { + ctx.emit_error("epsilon moves not permitted", letter.1); + } + None + } + Sym::Ident(val) => { + if !alphabet.contains_key(&Letter(val)) { + ctx.emit_error( + "transition letter not defined in alphabet", + letter.1, + ); + } + Some(Letter(val)) + } + }; + + for item in list { + let Some(next_state) = item.expect_ident(ctx) else { + continue; + }; + let next_state = Spanned(next_state, item.1); + + if !states.contains_key(&State(next_state.0)) { + ctx.emit_error("transition state not defined as state", next_state.1); + continue; + }; + + let entry: &mut _ = transitions + .entry(TransitionFrom { + letter, + state: State(state.0), + }) + .or_default(); + if !entry.is_empty() && !options.non_deterministic { + ctx.emit_error("transition already defined for this starting point (non determinism not permitted)", item.1); + } + if !entry.insert(TransitionTo { + state: State(next_state.0), + + function: tuple.1, + transition: item.1, + }) { + ctx.emit_warning("duplicate transition", item.1); + } + } + } + TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { + ctx.emit_error( + format!( + "unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'" + ), + dest_s, + ); + } + + TL::ProductionRule(_, _) => { + ctx.emit_error("unexpected production rule", span); + } + TL::Table() => ctx.emit_error("unexpected table", span), + } + } + + if alphabet.is_empty() { + ctx.emit_error_locless("alphabet never defined"); + } + + if states.is_empty() { + ctx.emit_error_locless("states never defined"); + } + + let initial_state = match initial_state { + Some(some) => some, + None => { + if states.contains_key(&State("q0")) { + ctx.emit_warning_locless("initial state not defined, defaulting to 'q0'"); + } else { + ctx.emit_error_locless("initial state not defined"); + } + State("q0") + } + }; + + if ctx.contains_errors() { + return None; + } + + Some(Fa { + initial_state, + states, + alphabet, + final_states, + transitions, + }) + } +} + +impl<'a> Spanned<&ast::Tuple<'a>> { + fn expect_fa_transition_function( + &self, + ctx: &mut Context<'a>, + ) -> Option<(Spanned<&'a str>, Spanned>)> { + match &self.0.0[..] { + [ + Spanned(ast::Item::Symbol(ast::Symbol::Ident(state)), state_span), + Spanned(ast::Item::Symbol(letter), letter_span), + ] => { + return Some((Spanned(state, *state_span), Spanned(*letter, *letter_span))); + } + _ => ctx.emit_error("expected FA transition function (ident, ident|~)", self.1), + } + None + } +} diff --git a/src/automatan/mod.rs b/src/automatan/mod.rs new file mode 100644 index 0000000..802816e --- /dev/null +++ b/src/automatan/mod.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use crate::loader::Span; + +pub mod fa; +pub mod pda; +pub mod tm; + +#[derive(Clone, Copy, Debug)] +pub struct Options { + pub non_deterministic: bool, + pub epsilon_moves: bool, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))] +pub struct State<'a>(pub &'a str); + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))] +pub struct Symbol<'a>(pub &'a str); + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))] +pub struct Letter<'a>(pub &'a str); + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct StateInfo { + pub definition: Span, +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct SymbolInfo { + pub definition: Span, +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct LetterInfo { + pub definition: Span, +} diff --git a/src/automatan/pda.rs b/src/automatan/pda.rs new file mode 100644 index 0000000..3ae1f07 --- /dev/null +++ b/src/automatan/pda.rs @@ -0,0 +1,399 @@ +use std::collections::HashSet; + +use super::*; + +use crate::loader::{ + Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned, + ast::{self, Symbol as Sym}, +}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct TransitionFrom<'a> { + pub state: State<'a>, + pub letter: Option>, + pub symbol: Symbol<'a>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct TransitionTo<'a> { + pub state: State<'a>, + pub stack: Vec>, + + pub transition: Span, + pub function: Span, +} + +#[derive(Clone, Debug)] +#[allow(unused)] +#[cfg_attr(feature = "serde", serde_with::serde_as)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct Pda<'a> { + pub initial_state: State<'a>, + pub initial_stack: Symbol<'a>, + pub states: HashMap, StateInfo>, + pub symbols: HashMap, SymbolInfo>, + pub alphabet: HashMap, LetterInfo>, + + pub final_states: Option, StateInfo>>, + + #[cfg(feature = "serde")] + #[serde_as(as = "serde_with::Seq<(_, _)>")] + pub transitions: HashMap, HashSet>>, + + #[cfg(not(feature = "serde"))] + pub transitions: HashMap, HashSet>>, +} + +impl<'a> Pda<'a> { + pub fn parse( + items: impl Iterator>>, + ctx: &mut Context<'a>, + options: Options, + ) -> Option> { + let mut initial_state = None; + let mut initial_stack = None; + + let mut states = HashMap::new(); + let mut symbols = HashMap::new(); + let mut alphabet = HashMap::new(); + let mut final_states = None; + + let mut transitions: HashMap, HashSet>> = + HashMap::new(); + + for Spanned(element, span) in items { + use Spanned as S; + use ast::TopLevel as TL; + match element { + TL::Item(S("Q", _), list) => { + if !states.is_empty() { + ctx.emit_error("states already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + if states + .insert(State(ident), StateInfo { definition: item.1 }) + .is_some() + { + ctx.emit_error("state redefined", item.1); + } + } + + if list.is_empty() { + ctx.emit_error("states cannot be empty", span); + } + } + TL::Item(S("E" | SIGMA_UPPER | "sigma", _), list) => { + if !alphabet.is_empty() { + ctx.emit_error("alphabet already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + + if ident.chars().count() != 1 { + ctx.emit_error("letter cannot be longer than one char", item.1); + } + + if alphabet + .insert(Letter(ident), LetterInfo { definition: item.1 }) + .is_some() + { + ctx.emit_error("letter redefined", item.1); + } + } + if list.is_empty() { + ctx.emit_error("alphabet cannot be empty", span); + } + } + TL::Item(S("F", _), list) => { + if final_states.is_some() { + ctx.emit_error("final states already set", span); + } + let mut map = HashMap::new(); + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + if states.contains_key(&State(ident)) { + if map + .insert(State(ident), StateInfo { definition: item.1 }) + .is_none() + { + ctx.emit_error("final state redefined", item.1); + } + } else { + ctx.emit_error("final state not defined in set of states", item.1); + } + } + final_states = Some(map); + } + TL::Item(S("T" | GAMMA_UPPER | "gamma", _), list) => { + if !symbols.is_empty() { + ctx.emit_error("stack symbols already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + + if symbols + .insert(Symbol(ident), SymbolInfo { definition: item.1 }) + .is_some() + { + ctx.emit_error("stack symbol redefined", item.1); + } + } + + if list.is_empty() { + ctx.emit_error("stack symbols cannot be empty", span); + } + } + TL::Item(S("I" | "q0", _), S(src, src_d)) => match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if initial_state.is_some() { + ctx.emit_error("initial state already set", span); + } + if states.contains_key(&State(ident)) { + initial_state = Some(State(ident)) + } else { + ctx.emit_error("initial state symbol not defined as a state", src_d); + } + } + _ => ctx.emit_error("expected ident", src_d), + }, + TL::Item(S("S" | "z0", _), S(src, src_d)) => match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if initial_stack.is_some() { + ctx.emit_error("initial stack already set", span); + } + if symbols.contains_key(&Symbol(ident)) { + initial_stack = Some(Symbol(ident)); + } else { + ctx.emit_error( + "initial stack symbol not defined as a stack symbol", + src_d, + ); + } + } + _ => ctx.emit_error("expected ident", src_d), + }, + TL::Item(S(name, dest_s), _) => { + ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s); + } + + TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => { + let list = list.set_weak(); + let Some((state, letter, stack_symbol)) = + tuple.as_ref().expect_pda_transition_function(ctx) + else { + continue; + }; + if !states.contains_key(&State(state.0)) { + ctx.emit_error("transition state not defined as state", state.1); + continue; + }; + if !symbols.contains_key(&Symbol(stack_symbol.0)) { + ctx.emit_error( + "transition stack symbol not defined as stack symbol", + stack_symbol.1, + ); + continue; + }; + + let letter: Option> = match letter.0 { + Sym::Epsilon => { + if !options.epsilon_moves { + ctx.emit_error("epsilon moves not permitted", letter.1); + } + None + } + Sym::Ident(val) => { + if !alphabet.contains_key(&Letter(val)) { + ctx.emit_error( + "transition letter not defined in alphabet", + letter.1, + ); + } + Some(Letter(val)) + } + }; + + for item in list { + let Some((next_state, stack)) = item + .expect_tuple(ctx) + .and_then(|item| item.expect_pda_transition(ctx)) + else { + continue; + }; + + if !states.contains_key(&State(next_state.0)) { + ctx.emit_error("transition state not defined as state", next_state.1); + continue; + }; + + let stack: Vec<_> = stack + .iter() + .rev() + .filter_map(|symbol| { + if matches!(symbol.0, ast::Item::Symbol(Sym::Epsilon)) { + return None; + } + let ident = symbol.expect_ident(ctx)?; + + if !symbols.contains_key(&Symbol(ident)) { + ctx.emit_error("transition stack symbol not defined", symbol.1); + return None; + }; + Some(Symbol(ident)) + }) + .collect(); + + let entry: &mut _ = transitions + .entry(TransitionFrom { + letter, + state: State(state.0), + symbol: Symbol(stack_symbol.0), + }) + .or_default(); + if !entry.is_empty() && !options.non_deterministic { + ctx.emit_error("transition already defined for this starting point (non determinism not permitted)", item.1); + } + if !entry.insert(TransitionTo { + state: State(next_state.0), + stack, + + function: tuple.1, + transition: item.1, + }) { + ctx.emit_warning("duplicate transition", item.1); + } + } + } + TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { + ctx.emit_error( + format!( + "unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'" + ), + dest_s, + ); + } + + TL::ProductionRule(_, _) => { + ctx.emit_error("unexpected production rule", span); + } + TL::Table() => ctx.emit_error("unexpected table", span), + } + } + + if symbols.is_empty() { + ctx.emit_error_locless("stack symbols never defined"); + } + + if alphabet.is_empty() { + ctx.emit_error_locless("alphabet never defined"); + } + + if states.is_empty() { + ctx.emit_error_locless("states never defined"); + } + + let initial_stack = match initial_stack { + Some(some) => some, + None => { + if symbols.contains_key(&Symbol("z0")) { + ctx.emit_warning_locless( + "initial stack symbol not defined, defaulting to 'z0'", + ); + } else { + ctx.emit_error_locless("initial stack symbol not defined"); + } + Symbol("z0") + } + }; + + let initial_state = match initial_state { + Some(some) => some, + None => { + if states.contains_key(&State("q0")) { + ctx.emit_warning_locless("initial state not defined, defaulting to 'q0'"); + } else { + ctx.emit_error_locless("initial state not defined"); + } + State("q0") + } + }; + + if ctx.contains_errors() { + return None; + } + + Some(Pda { + initial_state, + initial_stack, + states, + symbols, + alphabet, + final_states, + transitions, + }) + } +} + +impl<'a, 'b> Spanned<&'b ast::Tuple<'a>> { + fn expect_pda_transition_function( + &self, + ctx: &mut Context<'a>, + ) -> Option<(Spanned<&'a str>, Spanned>, Spanned<&'a str>)> { + match &self.0.0[..] { + [ + Spanned(ast::Item::Symbol(ast::Symbol::Ident(state)), state_span), + Spanned(ast::Item::Symbol(letter), letter_span), + Spanned(ast::Item::Symbol(ast::Symbol::Ident(symbol)), symbol_span), + ] => { + return Some(( + Spanned(state, *state_span), + Spanned(*letter, *letter_span), + Spanned(symbol, *symbol_span), + )); + } + _ => ctx.emit_error( + "expected PDA transition function (ident, ident|~, ident)", + self.1, + ), + } + None + } + fn expect_pda_transition( + &self, + ctx: &mut Context<'a>, + ) -> Option<(Spanned<&'a str>, &'b [Spanned>])> { + match &self.0.0[..] { + [ + Spanned(ast::Item::Symbol(ast::Symbol::Ident(state)), state_span), + list, + ] => { + return Some((Spanned(state, *state_span), list.list_weak())); + } + _ => ctx.emit_error("expected PDA transition (ident, item|[item])", self.1), + } + None + } +} diff --git a/src/automatan/tm.rs b/src/automatan/tm.rs new file mode 100644 index 0000000..90e2135 --- /dev/null +++ b/src/automatan/tm.rs @@ -0,0 +1,346 @@ +use std::collections::HashSet; + +use super::*; + +use crate::loader::{ + Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned, + ast::{self, Symbol as Sym}, +}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct TransitionFrom<'a> { + pub state: State<'a>, + pub symbol: Symbol<'a>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub enum Direction { + Left, + Right, + None, +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct TransitionTo<'a> { + pub state: State<'a>, + pub symbol: Symbol<'a>, + pub direction: Direction, + + pub transition: Span, + pub function: Span, +} + +#[derive(Clone, Debug)] +#[allow(unused)] +#[cfg_attr(feature = "serde", serde_with::serde_as)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct Tm<'a> { + pub initial_state: State<'a>, + pub initial_tape: Symbol<'a>, + pub states: HashMap, StateInfo>, + pub symbols: HashMap, SymbolInfo>, + + pub final_states: HashMap, StateInfo>, + + #[cfg(feature = "serde")] + #[serde_as(as = "serde_with::Seq<(_, _)>")] + pub transitions: HashMap, HashSet>>, + #[cfg(not(feature = "serde"))] + pub transitions: HashMap, HashSet>>, +} + +impl<'a> Tm<'a> { + pub fn parse( + items: impl Iterator>>, + ctx: &mut Context<'a>, + options: Options, + ) -> Option> { + let mut initial_state = None; + let mut initial_tape = None; + + let mut states = HashMap::new(); + let mut symbols = HashMap::new(); + let mut final_states = HashMap::new(); + + let mut transitions: HashMap, HashSet>> = + HashMap::new(); + + for Spanned(element, span) in items { + use Spanned as S; + use ast::TopLevel as TL; + match element { + TL::Item(S("Q", _), list) => { + if !states.is_empty() { + ctx.emit_error("states already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + if states + .insert(State(ident), StateInfo { definition: item.1 }) + .is_some() + { + ctx.emit_error("state redefined", item.1); + } + } + + if list.is_empty() { + ctx.emit_error("states cannot be empty", span); + } + } + TL::Item(S("F", _), list) => { + if !final_states.is_empty() { + ctx.emit_error("final states already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + if states.contains_key(&State(ident)) { + if final_states + .insert(State(ident), StateInfo { definition: item.1 }) + .is_none() + { + ctx.emit_error("final state redefined", item.1); + } + } else { + ctx.emit_error("final state not defined in set of states", item.1); + } + } + } + TL::Item(S("T" | GAMMA_UPPER | "gamma", _), list) => { + if !symbols.is_empty() { + ctx.emit_error("tape symbols already set", span); + } + let Some(list) = list.expect_set(ctx) else { + continue; + }; + for item in list { + let Some(ident) = item.expect_ident(ctx) else { + continue; + }; + + if symbols + .insert(Symbol(ident), SymbolInfo { definition: item.1 }) + .is_some() + { + ctx.emit_error("tape symbol redefined", item.1); + } + } + + if list.is_empty() { + ctx.emit_error("tape symbols cannot be empty", span); + } + } + TL::Item(S("I" | "q0", _), S(src, src_d)) => match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if initial_state.is_some() { + ctx.emit_error("initial state already set", span); + } + if states.contains_key(&State(ident)) { + initial_state = Some(State(ident)) + } else { + ctx.emit_error("initial state symbol not defined as a state", src_d); + } + } + _ => ctx.emit_error("expected ident", src_d), + }, + TL::Item(S("S" | "z0", _), S(src, src_d)) => match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if initial_tape.is_some() { + ctx.emit_error("initial tape symbol already set", span); + } + if symbols.contains_key(&Symbol(ident)) { + initial_tape = Some(Symbol(ident)); + } else { + ctx.emit_error( + "initial tape symbol not defined as a tape symbol", + src_d, + ); + } + } + _ => ctx.emit_error("expected ident", src_d), + }, + TL::Item(S(name, dest_s), _) => { + ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s); + } + + TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => { + let list = list.set_weak(); + let Some((from_state, from_tape)) = + tuple.as_ref().expect_tm_transition_function(ctx) + else { + continue; + }; + if !states.contains_key(&State(from_state.0)) { + ctx.emit_error("transition state not defined as state", from_state.1); + continue; + }; + if !symbols.contains_key(&Symbol(from_tape.0)) { + ctx.emit_error( + "transition tape symbol not defined as tape symbol", + from_tape.1, + ); + continue; + }; + + for item in list { + let Some((to_state, to_tape, direction)) = item + .expect_tuple(ctx) + .and_then(|item| item.expect_tm_transition(ctx)) + else { + continue; + }; + + if !states.contains_key(&State(to_state.0)) { + ctx.emit_error("transition state not defined as state", to_state.1); + continue; + }; + + let entry: &mut _ = transitions + .entry(TransitionFrom { + state: State(from_state.0), + symbol: Symbol(from_tape.0), + }) + .or_default(); + if !entry.is_empty() && !options.non_deterministic { + ctx.emit_error("transition already defined for this starting point (non determinism not permitted)", item.1); + } + if !entry.insert(TransitionTo { + state: State(to_state.0), + symbol: Symbol(to_tape.0), + direction: direction.0, + + function: tuple.1, + transition: item.1, + }) { + ctx.emit_warning("duplicate transition", item.1); + } + } + } + TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { + ctx.emit_error( + format!( + "unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'" + ), + dest_s, + ); + } + + TL::ProductionRule(_, _) => { + ctx.emit_error("unexpected production rule", span); + } + TL::Table() => ctx.emit_error("unexpected table", span), + } + } + + if symbols.is_empty() { + ctx.emit_error_locless("tape symbols never defined"); + } + + if states.is_empty() { + ctx.emit_error_locless("states never defined"); + } + + let initial_tape = match initial_tape { + Some(some) => some, + None => { + if symbols.contains_key(&Symbol("z0")) { + ctx.emit_warning_locless("initial tape symbol not defined, defaulting to 'z0'"); + } else { + ctx.emit_error_locless("initial tape symbol not defined"); + } + Symbol("z0") + } + }; + + let initial_state = match initial_state { + Some(some) => some, + None => { + if states.contains_key(&State("q0")) { + ctx.emit_warning_locless("initial state not defined, defaulting to 'q0'"); + } else { + ctx.emit_error_locless("initial state not defined"); + } + State("q0") + } + }; + + if ctx.contains_errors() { + return None; + } + + Some(Tm { + initial_state, + initial_tape, + states, + symbols, + final_states, + transitions, + }) + } +} + +impl<'a> Spanned<&ast::Tuple<'a>> { + fn expect_tm_transition_function( + &self, + ctx: &mut Context<'a>, + ) -> Option<(Spanned<&'a str>, Spanned<&'a str>)> { + match &self.0.0[..] { + [ + Spanned(ast::Item::Symbol(ast::Symbol::Ident(state)), state_span), + Spanned(ast::Item::Symbol(ast::Symbol::Ident(tape)), tape_span), + ] => { + return Some((Spanned(state, *state_span), Spanned(*tape, *tape_span))); + } + _ => ctx.emit_error("expected TM transition function (ident, ident)", self.1), + } + None + } + + fn expect_tm_transition( + &self, + ctx: &mut Context<'a>, + ) -> Option<(Spanned<&'a str>, Spanned<&'a str>, Spanned)> { + match &self.0.0[..] { + [ + Spanned(ast::Item::Symbol(ast::Symbol::Ident(state)), state_span), + Spanned(ast::Item::Symbol(ast::Symbol::Ident(tape)), tape_span), + Spanned(ast::Item::Symbol(direction), direction_span), + ] => { + let direction = match direction { + ast::Symbol::Ident("left" | "L" | "<") => Direction::Left, + ast::Symbol::Ident("right" | "R" | ">") => Direction::Right, + ast::Symbol::Epsilon | ast::Symbol::Ident("~") => Direction::None, + ast::Symbol::Ident(ident) => { + ctx.emit_error( + format!("invalid direction specified '{ident}'"), + *direction_span, + ); + Direction::None + } + }; + return Some(( + Spanned(state, *state_span), + Spanned(*tape, *tape_span), + Spanned(direction, *direction_span), + )); + } + _ => ctx.emit_error( + "expected TM transition function (ident, ident, ident)", + self.1, + ), + } + None + } +} diff --git a/src/lib.rs b/src/lib.rs index 345bf34..af73e12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,2 @@ -pub mod automata; +pub mod automatan; pub mod loader; diff --git a/src/loader/ast.rs b/src/loader/ast.rs index 1176d6e..5f6afcb 100644 --- a/src/loader/ast.rs +++ b/src/loader/ast.rs @@ -51,7 +51,10 @@ pub struct ProductionGroup<'a>(pub Vec>>); #[derive(Clone, Debug)] pub enum TopLevel<'a> { Item(Spanned<&'a str>, Spanned>), - TransitionFunc(Spanned<(Spanned<&'a str>, Spanned>)>, Spanned>), + TransitionFunc( + Spanned<(Spanned<&'a str>, Spanned>)>, + Spanned>, + ), ProductionRule( Spanned>, Spanned>>>, @@ -62,12 +65,19 @@ pub enum TopLevel<'a> { use crate::loader::Context; impl<'a> Spanned> { + pub fn expect_symbol(&self, ctx: &mut Context<'a>) -> Option> { + match &self.0 { + Item::Symbol(sym) => return Some(*sym), + Item::Tuple(_) => ctx.emit_error("expected ident found tuple", self.1), + Item::List(_) => ctx.emit_error("expected ident found list", self.1), + } + None + } + pub fn expect_ident(&self, ctx: &mut Context<'a>) -> Option<&'a str> { match &self.0 { Item::Symbol(Symbol::Ident(ident)) => return Some(ident), - Item::Symbol(Symbol::Epsilon) => { - ctx.emit_error("expected ident found epsilon", self.1) - } + Item::Symbol(Symbol::Epsilon) => ctx.emit_error("expected ident found epsilon", self.1), Item::Tuple(_) => ctx.emit_error("expected ident found tuple", self.1), Item::List(_) => ctx.emit_error("expected ident found list", self.1), } @@ -111,71 +121,10 @@ impl<'a> Spanned> { pub fn expect_tuple(&self, ctx: &mut Context<'a>) -> Option>> { match &self.0 { Item::Symbol(Symbol::Ident(_)) => ctx.emit_error("expected tuple found ident", self.1), - Item::Symbol(Symbol::Epsilon) => { - ctx.emit_error("expected tuple found epsilon", self.1) - } + Item::Symbol(Symbol::Epsilon) => ctx.emit_error("expected tuple found epsilon", self.1), Item::Tuple(tuple) => return Some(Spanned(tuple, self.1)), Item::List(_) => ctx.emit_error("expected tuple found list", self.1), } None } } - -impl<'a, 'b> Spanned<&'b Tuple<'a>> { - pub fn expect_dfa_transition(&self, _: &mut Context<'a>) -> ! { - todo!() - } - pub fn expect_nfa_transition(&self, _: &mut Context<'a>) -> ! { - todo!() - } - - pub fn expect_dpda_transition(&self, _: &mut Context<'a>) -> ! { - todo!() - } - - pub fn expect_npda_transition_function( - &self, - ctx: &mut Context<'a>, - ) -> Option<(Spanned<&'a str>, Spanned>, Spanned<&'a str>)> { - match &self.0.0[..] { - [ - Spanned(Item::Symbol(Symbol::Ident(state)), state_span), - Spanned(Item::Symbol(letter), letter_span), - Spanned(Item::Symbol(Symbol::Ident(symbol)), symbol_span), - ] => { - return Some(( - Spanned(state, *state_span), - Spanned(*letter, *letter_span), - Spanned(symbol, *symbol_span), - )); - } - _ => ctx.emit_error( - "expected NPDA transition function (ident, ident|~, ident)", - self.1, - ), - } - None - } - pub fn expect_npda_transition( - &self, - ctx: &mut Context<'a>, - ) -> Option<(Spanned<&'a str>, &'b [Spanned>])> { - match &self.0.0[..] { - [ - Spanned(Item::Symbol(Symbol::Ident(state)), state_span), - list, - ] => { - return Some((Spanned(state, *state_span), list.list_weak())); - } - _ => ctx.emit_error("expected NPDA transition (ident, item|[item])", self.1), - } - None - } - - pub fn expect_tm_transition(&self, _: &Context<'a>) -> ! { - todo!() - } - pub fn expect_ntm_transition(&self, _: &Context<'a>) -> ! { - todo!() - } -} diff --git a/src/loader/lexer.rs b/src/loader/lexer.rs index fe74d6a..fbb9d67 100644 --- a/src/loader/lexer.rs +++ b/src/loader/lexer.rs @@ -69,10 +69,7 @@ pub enum Error { impl<'a> Lexer<'a> { pub fn new(input: &'a str) -> Self { - Self { - input, - position: 0, - } + Self { input, position: 0 } } fn consume(&mut self) -> Option { @@ -92,7 +89,6 @@ impl<'a> Lexer<'a> { self.position -= previous.len_utf8(); } } - } fn begin_ident(c: char) -> bool { @@ -110,12 +106,12 @@ impl<'a> std::iter::Iterator for Lexer<'a> { while let Some(c) = self.peek() && c.is_whitespace() { - if c == '\n'{ + if c == '\n' { let start = self.position; self.consume(); let res = Some(Spanned(Ok(Token::LineEnd), Span(start, self.position))); return res; - }else{ + } else { self.consume(); } } @@ -151,7 +147,7 @@ impl<'a> std::iter::Iterator for Lexer<'a> { '/' => match self.consume() { Some('/') => loop { - match self.consume(){ + match self.consume() { Some('\n') => { self.backtrack(); break Ok(Token::Comment(&self.input[start + 2..=self.position])); @@ -166,9 +162,7 @@ impl<'a> std::iter::Iterator for Lexer<'a> { match self.consume() { Some('*') if self.peek() == Some('/') => { self.consume(); - break Ok(Token::Comment( - &self.input[start + 2..self.position - 2], - )); + break Ok(Token::Comment(&self.input[start + 2..self.position - 2])); } Some(_) => {} None => break Err(Error::UnclosedMultiLine), diff --git a/src/loader/log.rs b/src/loader/log.rs index 57ddd12..b30d966 100644 --- a/src/loader/log.rs +++ b/src/loader/log.rs @@ -1,4 +1,4 @@ -use std::{fmt::Display}; +use std::fmt::Display; use crate::loader::Span; @@ -64,11 +64,11 @@ impl Logs { }); } - pub fn displayable_with<'a>(&'a self, src: &'a str) -> impl Iterator> { - self.logs.iter().map(|entry| LogEntryDisplay { - src, - entry, - }) + pub fn displayable_with<'a>( + &'a self, + src: &'a str, + ) -> impl Iterator> { + self.logs.iter().map(|entry| LogEntryDisplay { src, entry }) } pub fn entries(&self) -> &[LogEntry] { diff --git a/src/loader/mod.rs b/src/loader/mod.rs index 0abfbc8..fb412f6 100644 --- a/src/loader/mod.rs +++ b/src/loader/mod.rs @@ -1,4 +1,4 @@ -use crate::{automata::npda, loader::ast::TopLevel}; +use crate::{automatan::*, loader::ast::TopLevel}; pub mod ast; pub mod lexer; @@ -13,7 +13,11 @@ pub const GAMMA_UPPER: &str = "Γ"; pub const GAMMA_LOWER: &str = "γ"; #[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] -pub struct Span(pub usize, pub usize); +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct Span( + #[cfg_attr(feature = "serde", serde(rename = "start"))] pub usize, + #[cfg_attr(feature = "serde", serde(rename = "end"))] pub usize, +); impl Span { pub fn join(&self, end: Span) -> Span { Span(self.0, end.1) @@ -32,26 +36,28 @@ impl Spanned { } } - -pub struct Context<'a>{ +pub struct Context<'a> { logs: log::Logs, - src: &'a str + src: &'a str, } -impl<'a> Context<'a>{ - pub fn new(src: &'a str) -> Self{ - Self { logs: log::Logs::new(), src } +impl<'a> Context<'a> { + pub fn new(src: &'a str) -> Self { + Self { + logs: log::Logs::new(), + src, + } } - pub fn src(&self) -> &'a str{ + pub fn src(&self) -> &'a str { self.src } - pub fn logs_display(&self) -> impl Iterator>{ + pub fn logs_display(&self) -> impl Iterator> { self.logs.displayable_with(self.src) } - pub fn eof(&self) -> Span{ + pub fn eof(&self) -> Span { Span(self.src.len(), self.src.len()) } @@ -78,42 +84,45 @@ impl<'a> Context<'a>{ pub fn emit_info(&mut self, msg: impl Into, span: Span) { self.logs.emit_info(msg, span); } - + pub fn contains_errors(&self) -> bool { self.logs.contains_errors() } - pub fn into_logs(self) -> log::Logs{ + pub fn into_logs(self) -> log::Logs { self.logs } } - -pub enum Machine{ - Npda(npda::Npda) +pub enum Machine<'a> { + Fa(fa::Fa<'a>), + Pda(pda::Pda<'a>), + Tm(tm::Tm<'a>), } -pub fn parse_universal(ctx: &mut Context<'_>) -> Option { +pub fn parse_universal<'a>(ctx: &mut Context<'a>) -> Option> { let mut items = parser::Parser::new(ctx).collect::>().into_iter(); - if ctx.logs.contains_errors(){ + if ctx.logs.contains_errors() { return None; } use Spanned as S; #[derive(Debug)] - enum Type{ + enum Type { Dfa, Nfa, Dpda, Npda, Tm, - Ntm + Ntm, } - fn parse_type<'a>(item: Option>>, ctx: &mut Context<'a>) -> Option{ - let (str, span) = match item{ - Some(S(TopLevel::Item(S("type", _), item@S(_,span)), _)) => (item.expect_ident(ctx)?, span), + fn parse_type<'a>(item: Option>>, ctx: &mut Context<'a>) -> Option { + let (str, span) = match item { + Some(S(TopLevel::Item(S("type", _), item @ S(_, span)), _)) => { + (item.expect_ident(ctx)?, span) + } Some(S(_, span)) => { ctx.emit_error("expected type= as first item", span); return None; @@ -124,25 +133,39 @@ pub fn parse_universal(ctx: &mut Context<'_>) -> Option { } }; - Some(match str{ - "dfa"|"DFA" => Type::Dfa, - "nfa"|"NFA" => Type::Nfa, - "dpda"|"DPDA" => Type::Dpda, - "npdaA"|"NPDA" => Type::Npda, - "tm"|"TM" => Type::Tm, - "ntm"|"NTM" => Type::Ntm, + Some(match str { + "dfa" | "DFA" => Type::Dfa, + "nfa" | "NFA" => Type::Nfa, + "dpda" | "DPDA" => Type::Dpda, + "npdaA" | "NPDA" => Type::Npda, + "tm" | "TM" => Type::Tm, + "ntm" | "NTM" => Type::Ntm, _ => { - ctx.emit_error("unknown type, expected 'DFA' | 'NFA' | 'DPDA' | 'NPDA' | 'TM' | 'NTM'", span); + ctx.emit_error( + "unknown type, expected 'DFA' | 'NFA' | 'DPDA' | 'NPDA' | 'TM' | 'NTM'", + span, + ); return None; - }, + } }) } - Some(match parse_type(items.next(), ctx)?{ - Type::Npda => Machine::Npda(npda::Npda::load_from_ast(items, ctx)?), - ty => { - ctx.emit_error_locless(format!("currently unsupported type {ty:?}")); - return None; - } + const D: Options = Options { + non_deterministic: false, + epsilon_moves: false, + }; + + const N: Options = Options { + non_deterministic: true, + epsilon_moves: true, + }; + + Some(match parse_type(items.next(), ctx)? { + Type::Dfa => Machine::Fa(fa::Fa::parse(items, ctx, D)?), + Type::Nfa => Machine::Fa(fa::Fa::parse(items, ctx, N)?), + Type::Dpda => Machine::Pda(pda::Pda::parse(items, ctx, D)?), + Type::Npda => Machine::Pda(pda::Pda::parse(items, ctx, N)?), + Type::Tm => Machine::Tm(tm::Tm::parse(items, ctx, D)?), + Type::Ntm => Machine::Tm(tm::Tm::parse(items, ctx, N)?), }) -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 2d43b95..0000000 --- a/src/main.rs +++ /dev/null @@ -1,35 +0,0 @@ -use automata::{automata::npda, loader::Context}; - -fn main() { - let input = include_str!("../example.npda"); - let mut ctx = Context::new(input); - - let machine = automata::loader::parse_universal(&mut ctx); - for log in ctx.logs_display(){ - println!("{log}") - } - - let machine = match machine{ - Some(automata::loader::Machine::Npda(npda)) => { - npda - }, - None => return, - }; - - let input = "aababaaba"; - println!("running on: '{input}'"); - let mut simulator = npda::Simulator::begin(input, machine); - loop { - match simulator.step() { - npda::SimulatorResult::Pending => {} - npda::SimulatorResult::Reject => { - println!("REJECTED"); - break; - } - npda::SimulatorResult::Accept(npda) => { - println!("ACCEPT: {npda:?}"); - break; - } - } - } -} diff --git a/src/automata/dfa.rs b/src/sim/dfa.rs similarity index 100% rename from src/automata/dfa.rs rename to src/sim/dfa.rs diff --git a/src/automata/dpda.rs b/src/sim/dpda.rs similarity index 100% rename from src/automata/dpda.rs rename to src/sim/dpda.rs diff --git a/src/automata/mod.rs b/src/sim/mod.rs similarity index 100% rename from src/automata/mod.rs rename to src/sim/mod.rs diff --git a/src/automata/nfa.rs b/src/sim/nfa.rs similarity index 100% rename from src/automata/nfa.rs rename to src/sim/nfa.rs diff --git a/src/automata/npda.rs b/src/sim/npda.rs similarity index 100% rename from src/automata/npda.rs rename to src/sim/npda.rs diff --git a/src/automata/ntm.rs b/src/sim/ntm.rs similarity index 100% rename from src/automata/ntm.rs rename to src/sim/ntm.rs diff --git a/src/automata/tm.rs b/src/sim/tm.rs similarity index 100% rename from src/automata/tm.rs rename to src/sim/tm.rs diff --git a/web/Cargo.toml b/web/Cargo.toml index 599e65e..fd26f16 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -automata = {path=".."} +automata = {path="..", features = ["serde"]} console_error_panic_hook = "0.1.7" wasm-bindgen = "*" web-sys = { version = "0.3.83", features = ["Window", "Document", "HtmlElement", "Text"] } diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts index 8db2a28..e07c8a2 100644 --- a/web/root/src/editor.ts +++ b/web/root/src/editor.ts @@ -78,7 +78,11 @@ function buildAnalysis(text: string, doc: Text) { const { log, log_formatted, graph } = compile(text); if (graph){ - setAutomaton(JSON.parse(graph)) + try{ + setAutomaton(JSON.parse(graph)) + }catch(e){ + console.log(e); + } } // Build ONE Decoration set: syntax + diagnostics diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index ac3df28..0ffc366 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -132,6 +132,7 @@ export function clearAutomaton() { } export function setAutomaton(auto: GraphDef) { + console.log(auto); automaton = auto; automaton.final_states = new Set(automaton.final_states) automaton.states = new Set(automaton.states) diff --git a/web/src/lib.rs b/web/src/lib.rs index 1075cdb..3df3cb6 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -149,7 +149,6 @@ pub struct CompileLog { pub end: Option, } - #[derive(Serialize, Debug)] pub struct Graph<'a> { initial: &'a str, @@ -171,50 +170,11 @@ pub fn compile(input: &str) -> CompileResult { let result = automata::loader::parse_universal(&mut ctx); let graph = if let Some(result) = result { - match result { - loader::Machine::Npda(npda) => { - let mut transitions = HashMap::new(); - for ((from, symbol), to_transitions) in npda.transitions().entries(){ - let from = npda.get_state_name(from).unwrap_or(""); - let symbol = npda.get_symbol_name(symbol).unwrap_or(""); - for (char, to) in to_transitions.entries(){ - for to in to{ - let to_state = npda.get_state_name(to.state()).unwrap_or(""); - let string: &mut String = transitions.entry(format!("{from}#{to_state}")).or_default(); - if !string.is_empty(){ - string.push('\n'); - } - let char = char.unwrap_or('ε'); - let stack = to.stack().iter().map(|s|npda.get_symbol_name(*s).unwrap_or("")).fold(String::new(), |mut s, b|{ - if !s.is_empty(){ - s.push_str(", "); - } - s.push_str(b); - s - }); - write!(string, "{char}, {symbol} -> [{stack}]").unwrap(); - - } - } - } - let graph = Graph { - states: npda.states().map(|(_, n)| n).collect(), - initial: npda - .get_state_name(npda.initial_state()) - .unwrap_or(""), - final_states: npda - .final_states() - .map(|i| { - i.map(|s| npda.get_state_name(s).unwrap_or("")) - .collect::>() - }) - .unwrap_or_default(), - transitions - }; - - Some(serde_json::to_string(&graph).unwrap()) - } - } + Some(match result { + loader::Machine::Fa(fa) => serde_json::to_string(&fa).unwrap(), + loader::Machine::Pda(pda) => serde_json::to_string(&pda).unwrap(), + loader::Machine::Tm(tm) => serde_json::to_string(&tm).unwrap(), + }) } else { None }; From c57a95b7b5b2eb93ec29e7de77eb3abe10373627 Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:18:25 -0500 Subject: [PATCH 3/4] reorganized project --- Cargo.lock | 4 ++++ Cargo.toml | 17 ++--------------- automata/Cargo.toml | 12 ++++++++++++ {src => automata/src}/automatan/fa.rs | 0 {src => automata/src}/automatan/mod.rs | 0 {src => automata/src}/automatan/pda.rs | 0 {src => automata/src}/automatan/tm.rs | 0 {src => automata/src}/lib.rs | 0 {src => automata/src}/loader/ast.rs | 0 {src => automata/src}/loader/lexer.rs | 0 {src => automata/src}/loader/log.rs | 0 {src => automata/src}/loader/mod.rs | 0 {src => automata/src}/loader/parser.rs | 0 {src => automata/src}/sim/dfa.rs | 0 {src => automata/src}/sim/dpda.rs | 0 {src => automata/src}/sim/mod.rs | 0 {src => automata/src}/sim/nfa.rs | 0 {src => automata/src}/sim/npda.rs | 0 {src => automata/src}/sim/ntm.rs | 0 {src => automata/src}/sim/tm.rs | 0 cli/Cargo.toml | 8 ++++++++ cli/src/main.rs | 5 +++++ example.npda | 21 --------------------- web/build.sh | 2 -- web/run.sh | 2 -- web/tools/build.ts | 2 +- web/tools/dev.ts | 2 +- {web => web_lib}/Cargo.toml | 2 +- {web => web_lib}/src/lib.rs | 0 29 files changed, 34 insertions(+), 43 deletions(-) create mode 100644 automata/Cargo.toml rename {src => automata/src}/automatan/fa.rs (100%) rename {src => automata/src}/automatan/mod.rs (100%) rename {src => automata/src}/automatan/pda.rs (100%) rename {src => automata/src}/automatan/tm.rs (100%) rename {src => automata/src}/lib.rs (100%) rename {src => automata/src}/loader/ast.rs (100%) rename {src => automata/src}/loader/lexer.rs (100%) rename {src => automata/src}/loader/log.rs (100%) rename {src => automata/src}/loader/mod.rs (100%) rename {src => automata/src}/loader/parser.rs (100%) rename {src => automata/src}/sim/dfa.rs (100%) rename {src => automata/src}/sim/dpda.rs (100%) rename {src => automata/src}/sim/mod.rs (100%) rename {src => automata/src}/sim/nfa.rs (100%) rename {src => automata/src}/sim/npda.rs (100%) rename {src => automata/src}/sim/ntm.rs (100%) rename {src => automata/src}/sim/tm.rs (100%) create mode 100644 cli/Cargo.toml create mode 100644 cli/src/main.rs delete mode 100644 example.npda delete mode 100755 web/build.sh delete mode 100755 web/run.sh rename {web => web_lib}/Cargo.toml (86%) rename {web => web_lib}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index b8bb181..6e21067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,6 +25,10 @@ dependencies = [ "serde_with", ] +[[package]] +name = "automata-cli" +version = "0.1.0" + [[package]] name = "automata-web" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9c14234..377efa4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,3 @@ -[package] -name = "automata" -version = "0.1.0" -edition = "2024" - -[dependencies] -serde = { version = "1.0", features = ["derive"], optional = true} -serde_with = { version = "3.0", features = ["default"], optional = true} - -[features] -default = [] -serde = ["dep:serde", "dep:serde_with"] - [workspace] -members = ["web"] - +members = ["web_lib", "automata", "cli"] +resolver = "3" diff --git a/automata/Cargo.toml b/automata/Cargo.toml new file mode 100644 index 0000000..ad078e8 --- /dev/null +++ b/automata/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "automata" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0", features = ["derive"], optional = true} +serde_with = { version = "3.0", features = ["default"], optional = true} + +[features] +default = [] +serde = ["dep:serde", "dep:serde_with"] \ No newline at end of file diff --git a/src/automatan/fa.rs b/automata/src/automatan/fa.rs similarity index 100% rename from src/automatan/fa.rs rename to automata/src/automatan/fa.rs diff --git a/src/automatan/mod.rs b/automata/src/automatan/mod.rs similarity index 100% rename from src/automatan/mod.rs rename to automata/src/automatan/mod.rs diff --git a/src/automatan/pda.rs b/automata/src/automatan/pda.rs similarity index 100% rename from src/automatan/pda.rs rename to automata/src/automatan/pda.rs diff --git a/src/automatan/tm.rs b/automata/src/automatan/tm.rs similarity index 100% rename from src/automatan/tm.rs rename to automata/src/automatan/tm.rs diff --git a/src/lib.rs b/automata/src/lib.rs similarity index 100% rename from src/lib.rs rename to automata/src/lib.rs diff --git a/src/loader/ast.rs b/automata/src/loader/ast.rs similarity index 100% rename from src/loader/ast.rs rename to automata/src/loader/ast.rs diff --git a/src/loader/lexer.rs b/automata/src/loader/lexer.rs similarity index 100% rename from src/loader/lexer.rs rename to automata/src/loader/lexer.rs diff --git a/src/loader/log.rs b/automata/src/loader/log.rs similarity index 100% rename from src/loader/log.rs rename to automata/src/loader/log.rs diff --git a/src/loader/mod.rs b/automata/src/loader/mod.rs similarity index 100% rename from src/loader/mod.rs rename to automata/src/loader/mod.rs diff --git a/src/loader/parser.rs b/automata/src/loader/parser.rs similarity index 100% rename from src/loader/parser.rs rename to automata/src/loader/parser.rs diff --git a/src/sim/dfa.rs b/automata/src/sim/dfa.rs similarity index 100% rename from src/sim/dfa.rs rename to automata/src/sim/dfa.rs diff --git a/src/sim/dpda.rs b/automata/src/sim/dpda.rs similarity index 100% rename from src/sim/dpda.rs rename to automata/src/sim/dpda.rs diff --git a/src/sim/mod.rs b/automata/src/sim/mod.rs similarity index 100% rename from src/sim/mod.rs rename to automata/src/sim/mod.rs diff --git a/src/sim/nfa.rs b/automata/src/sim/nfa.rs similarity index 100% rename from src/sim/nfa.rs rename to automata/src/sim/nfa.rs diff --git a/src/sim/npda.rs b/automata/src/sim/npda.rs similarity index 100% rename from src/sim/npda.rs rename to automata/src/sim/npda.rs diff --git a/src/sim/ntm.rs b/automata/src/sim/ntm.rs similarity index 100% rename from src/sim/ntm.rs rename to automata/src/sim/ntm.rs diff --git a/src/sim/tm.rs b/automata/src/sim/tm.rs similarity index 100% rename from src/sim/tm.rs rename to automata/src/sim/tm.rs diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..3efa157 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "automata-cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +# automata = {path = "../automata"} + diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..496680a --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,5 @@ + + +pub fn main(){ + +} \ No newline at end of file diff --git a/example.npda b/example.npda deleted file mode 100644 index 60a9f6b..0000000 --- a/example.npda +++ /dev/null @@ -1,21 +0,0 @@ -//type=NPDA -Q = {q0, q1} // states -E = {a, b} // alphabet -T = {z0, A, B} // stack -q0 = q0 -z0 = z0 - -// construct all possible permutations of A's and B's -d(q0, epsilon, z0) = { (q0, [A z0]), (q0, [B z0]) } -d(q0, epsilon, A) = { (q0, [A A]), (q0, [B A]) } - -d(q0, epsilon, B) = { (q0, [A B]), (q0, [B B]) } - -// transition to q1 -d(q0, epsilon, z0) = { (q1, z0) } -d(q0, epsilon, A) = { (q1, A) } -d(q0, epsilon, B) = { (q1, B) } - -// consume stack until empty -d(q1, a, A) = { (q1, epsilon) } -d(q1, b, B) = { (q1, epsilon) } \ No newline at end of file diff --git a/web/build.sh b/web/build.sh deleted file mode 100755 index 37727a3..0000000 --- a/web/build.sh +++ /dev/null @@ -1,2 +0,0 @@ - -wasm-pack build --release --target web --no-typescript --out-dir root/automata --no-pack \ No newline at end of file diff --git a/web/run.sh b/web/run.sh deleted file mode 100755 index 08eb2f1..0000000 --- a/web/run.sh +++ /dev/null @@ -1,2 +0,0 @@ -cd root -simple-http-server \ No newline at end of file diff --git a/web/tools/build.ts b/web/tools/build.ts index fa4834d..f0de5b7 100644 --- a/web/tools/build.ts +++ b/web/tools/build.ts @@ -29,7 +29,7 @@ const result = sass.compile(String(new URL("style/style.scss", ROOT).pathname), await Deno.writeTextFile(new URL("style.css", DIST), result.css); console.log("Compiling wasm lib..."); -await run(["wasm-pack", "build", "--target", "web", "--release", "--out-dir", "wasm"], ""); +await run(["wasm-pack", "build", "--target", "web", "--release", "--out-dir", "../web/wasm"], "../web_lib"); await Deno.copyFile(new URL("automata_web_bg.wasm", WASM), new URL("automata_web_bg.wasm", DIST)); console.log("Compiling bundle..."); diff --git a/web/tools/dev.ts b/web/tools/dev.ts index 7e1efae..8cfdd76 100644 --- a/web/tools/dev.ts +++ b/web/tools/dev.ts @@ -46,7 +46,7 @@ await startServer(); console.log("👀 watching for changes…"); -const watcher = Deno.watchFs(["root", "src"]); +const watcher = Deno.watchFs(["root", "../automata", "../web_lib"]); for await (const event of watcher) { if ( event.kind === "modify" || diff --git a/web/Cargo.toml b/web_lib/Cargo.toml similarity index 86% rename from web/Cargo.toml rename to web_lib/Cargo.toml index fd26f16..4815b45 100644 --- a/web/Cargo.toml +++ b/web_lib/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["cdylib", "rlib"] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -automata = {path="..", features = ["serde"]} +automata = {path="../automata", features = ["serde"]} console_error_panic_hook = "0.1.7" wasm-bindgen = "*" web-sys = { version = "0.3.83", features = ["Window", "Document", "HtmlElement", "Text"] } diff --git a/web/src/lib.rs b/web_lib/src/lib.rs similarity index 100% rename from web/src/lib.rs rename to web_lib/src/lib.rs From 62cda62b31a723bc79cb33bce1d822fa6dfb7ff5 Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:55:39 -0500 Subject: [PATCH 4/4] loading machines visually now downs --- automata/src/automatan/fa.rs | 2 +- automata/src/automatan/mod.rs | 1 + automata/src/loader/mod.rs | 3 + web/root/src/automata.ts | 171 ++++++++++++++++++++++++++++++++++ web/root/src/editor.ts | 3 +- web/root/src/visualizer.ts | 104 ++++++++++----------- web_lib/src/lib.rs | 10 +- 7 files changed, 230 insertions(+), 64 deletions(-) create mode 100644 web/root/src/automata.ts diff --git a/automata/src/automatan/fa.rs b/automata/src/automatan/fa.rs index 44f0950..d52354f 100644 --- a/automata/src/automatan/fa.rs +++ b/automata/src/automatan/fa.rs @@ -25,8 +25,8 @@ pub struct TransitionTo<'a> { #[derive(Clone, Debug)] #[allow(unused)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr(feature = "serde", serde_with::serde_as)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Fa<'a> { pub initial_state: State<'a>, pub states: HashMap, StateInfo>, diff --git a/automata/src/automatan/mod.rs b/automata/src/automatan/mod.rs index 802816e..ba329eb 100644 --- a/automata/src/automatan/mod.rs +++ b/automata/src/automatan/mod.rs @@ -6,6 +6,7 @@ pub mod fa; pub mod pda; pub mod tm; + #[derive(Clone, Copy, Debug)] pub struct Options { pub non_deterministic: bool, diff --git a/automata/src/loader/mod.rs b/automata/src/loader/mod.rs index fb412f6..96687cd 100644 --- a/automata/src/loader/mod.rs +++ b/automata/src/loader/mod.rs @@ -94,6 +94,9 @@ impl<'a> Context<'a> { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub enum Machine<'a> { Fa(fa::Fa<'a>), Pda(pda::Pda<'a>), diff --git a/web/root/src/automata.ts b/web/root/src/automata.ts new file mode 100644 index 0000000..c8c8517 --- /dev/null +++ b/web/root/src/automata.ts @@ -0,0 +1,171 @@ +export type Machine = Fa | Pda | Tm; + +export function machine_from_json(json: string): Machine { + const machine: Machine = JSON.parse(json); + machine.states = new Map(Object.entries(machine.states)); + + if (machine.alphabet) { + machine.alphabet = new Map(Object.entries(machine.alphabet)); + } + if (machine.final_states) { + machine.final_states = new Map(Object.entries(machine.final_states)); + } + + // deno-lint-ignore no-explicit-any + const transitions = machine.transitions as any as [any, any]; + machine.transitions = new Map(); + for (const [key, value] of transitions) { + machine.transitions.set(key, value); + } + machine.edges = new Map(); + + switch (machine.type) { + case "fa": + { + for (const [from, tos] of machine.transitions) { + for (const to of tos) { + const edge = from.state + "#" + to.state; + if (!machine.edges.has(edge)) machine.edges.set(edge, []); + machine.edges.get(edge)?.push({ + repr: from.letter?from.letter:"ε", + function: to.function, + transition: to.transition, + }); + } + } + } + break; + case "pda": + { + machine.symbols = new Map(Object.entries(machine.symbols)); + for (const [from, tos] of machine.transitions) { + for (const to of tos) { + const edge = from.state + "#" + to.state; + if (!machine.edges.has(edge)) machine.edges.set(edge, []); + machine.edges.get(edge)?.push({ + repr: (from.letter?from.letter:"ε")+","+from.symbol+"->["+to.stack+"]", + function: to.function, + transition: to.transition, + }); + } + } + } + break; + case "tm": + { + machine.symbols = new Map(Object.entries(machine.symbols)); + for (const [from, tos] of machine.transitions) { + for (const to of tos) { + const edge = from.state + "#" + to.state; + if (!machine.edges.has(edge)) machine.edges.set(edge, []); + machine.edges.get(edge)?.push({ + repr: from.symbol+"->"+to.symbol+","+to.direction, + function: to.function, + transition: to.transition, + }); + } + } + } + break; + } + return machine; +} + +export type State = string; +export type Symbol = string; +export type Letter = string; + +export type Span = [number, number]; + +export type StateInfo = { definition: Span }; +export type LetterInfo = { definition: Span }; +export type SymbolInfo = { definition: Span }; + +export type FaTransFrom = { + state: State; + letter: Letter|null; +}; + +export type FaTransTo = { + state: State; + + transition: Span; + function: Span; +}; + +export type Edge = { + repr: string; + function: Span; + transition: Span; +}; + +export type Fa = { + type: "fa"; + + initial_state: State; + states: Map; + alphabet: Map; + final_states: Map; + + transitions: Map; + + edges: Map; +}; + +export type PdaTransFrom = { + state: State; + letter: Letter|null; + symbol: Symbol; +}; + +export type PdaTransTo = { + state: State; + stack: Symbol[]; + + transition: Span; + function: Span; +}; + +export type Pda = { + type: "pda"; + + initial_state: State; + initial_stack: Symbol; + states: Map; + symbols: Map; + alphabet: Map; + final_states: Map | null; + + transitions: Map; + + edges: Map; +}; + +export type TmTransFrom = { + state: State; + symbol: Symbol; +}; + +export type TmTransTo = { + state: State; + symbol: Symbol; + direction: "L" | "R" | "N"; + + transition: Span; + function: Span; +}; + +export type Tm = { + type: "tm"; + + initial_state: State; + initial_tape: Symbol; + states: Map; + symbols: Map; + alphabet: Map; + final_states: Map; + + transitions: Map; + + edges: Map; +}; diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts index e07c8a2..a45ccef 100644 --- a/web/root/src/editor.ts +++ b/web/root/src/editor.ts @@ -21,6 +21,7 @@ import wasm from "./wasm.ts" import { terminalPlugin } from "./terminal.ts"; import { setAutomaton } from "./visualizer.ts"; +import { machine_from_json } from "./automata.ts"; function tokenize(text: string) { @@ -79,7 +80,7 @@ function buildAnalysis(text: string, doc: Text) { if (graph){ try{ - setAutomaton(JSON.parse(graph)) + setAutomaton(machine_from_json(graph)) }catch(e){ console.log(e); } diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index 0ffc366..1954d6b 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -3,11 +3,11 @@ // deno-lint-ignore no-import-prefix import * as vis from "npm:vis-network/standalone"; import { StateEffect } from "npm:@codemirror/state"; +import { Machine } from "./automata.ts"; export const nodes = new vis.DataSet(); export const edges = new vis.DataSet(); - type Color = string; type GraphTheme = { bg_0: Color; @@ -25,8 +25,8 @@ type GraphTheme = { edge_hover: Color; edge_active: Color; - edge_font_size: number, - node_font_size: number, + edge_font_size: number; + node_font_size: number; }; let _graphTheme: GraphTheme | null = null; @@ -55,11 +55,11 @@ function getGraphTheme(): GraphTheme { node_anchor: cssVar("--graph-node-anchor"), node_border: cssVar("--graph-node-border"), current_node_border: cssVar("--graph-current-node-border"), - + edge: cssVar("--graph-edge"), edge_hover: cssVar("--graph-edge-hover"), edge_active: cssVar("--graph-edge-active"), - + edge_font_size: Number(cssVar("--graph-edge-font-size")), node_font_size: Number(cssVar("--graph-node-font-size")), }; @@ -77,21 +77,21 @@ export function updateGraphTheme() { color: gt.fg_0, bold: { color: gt.fg_1, - mod: '' + mod: "", }, }, }, edges: { labelHighlightBold: true, - font: { - align: "top", + font: { + align: "top", size: gt.edge_font_size, color: gt.fg_0, strokeColor: gt.bg_0, bold: { color: gt.fg_1, size: gt.edge_font_size, - mod: '' + mod: "", }, }, color: { @@ -101,43 +101,39 @@ export function updateGraphTheme() { }, shadow: { enabled: false, - } + }, }, }); } - -type StateId = string; -type GraphDef = { - initial: StateId; - final_states: Set; - states: Set; - transitions: Record; -}; - -let automaton: GraphDef = { - initial: "", - final_states: new Set(), - states: new Set(), - transitions: {}, +let automaton: Machine = { + type: "fa", + alphabet: new Map(), + final_states: new Map(), + initial_state: "", + states: new Map(), + transitions: new Map(), + edges: new Map(), }; export function clearAutomaton() { - setAutomaton({ - initial: "", - final_states: new Set(), - states: new Set(), - transitions: {}, - }); + automaton = { + type: "fa", + alphabet: new Map(), + final_states: new Map(), + initial_state: "", + states: new Map(), + transitions: new Map(), + edges: new Map(), + }; } -export function setAutomaton(auto: GraphDef) { +export function setAutomaton(auto: Machine) { console.log(auto); automaton = auto; - automaton.final_states = new Set(automaton.final_states) - automaton.states = new Set(automaton.states) + // Populate nodes - for (const state of automaton.states) { + for (const state of automaton.states.keys()) { if (nodes.get(state)) { nodes.update({ id: state, @@ -151,40 +147,41 @@ export function setAutomaton(auto: GraphDef) { } } - // Populate edges - for (const [k, v] of Object.entries(automaton.transitions)) { - const to_from = k.split("#"); + // // Populate edges + for (const [edge_id, transitions] of auto.edges) { + const to_from = edge_id.split("#"); const font = { - vadjust: -getGraphTheme().edge_font_size*Math.floor(((v.match(/\n/g) || '').length + 1)/2) - }; - if (edges.get(k)) { + vadjust: -getGraphTheme().edge_font_size * + Math.floor(transitions.length / 2), + }; + if (edges.get(edge_id)) { edges.update({ - id: k, + id: edge_id, font, from: to_from[0], to: to_from[1], - label: v, + label: transitions.map(i => i.repr).join("\n"), }); } else { edges.add({ - id: k, + id: edge_id, font, from: to_from[0], to: to_from[1], - label: v, + label: transitions.map(i => i.repr).join("\n"), }); } } - for (const edge_id of edges.getIds()){ - if (auto.transitions[edge_id as string] === undefined){ - edges.remove(edge_id) + for (const edge_id of edges.getIds()) { + if (!auto.edges.has(edge_id as string)) { + edges.remove(edge_id); } } - for (const node_id of nodes.getIds()){ - if (!auto.states.has(node_id as string)){ - nodes.remove(node_id) + for (const node_id of nodes.getIds()) { + if (!auto.states.has(node_id as string)) { + nodes.remove(node_id); } } } @@ -278,12 +275,13 @@ function renderNode({ }: any) { return { drawNode() { - const t = getGraphTheme(); const r = Math.max(14, style?.size ?? 18); - const isInitial = automaton.initial === id; - const isFinal = automaton.final_states.has(id); + const isInitial = automaton.initial_state === id; + const isFinal = automaton.final_states + ? automaton.final_states.has(id) + : false; const isActive = false; const fill = selected ? t.bg_2 : hover ? t.bg_1 : t.bg_0; diff --git a/web_lib/src/lib.rs b/web_lib/src/lib.rs index 3df3cb6..71c996d 100644 --- a/web_lib/src/lib.rs +++ b/web_lib/src/lib.rs @@ -169,15 +169,7 @@ pub fn compile(input: &str) -> CompileResult { let mut ctx = Context::new(input); let result = automata::loader::parse_universal(&mut ctx); - let graph = if let Some(result) = result { - Some(match result { - loader::Machine::Fa(fa) => serde_json::to_string(&fa).unwrap(), - loader::Machine::Pda(pda) => serde_json::to_string(&pda).unwrap(), - loader::Machine::Tm(tm) => serde_json::to_string(&tm).unwrap(), - }) - } else { - None - }; + let graph = result.map(|result| serde_json::to_string(&result).unwrap()); use std::fmt::Write; let log_formatted = ctx.logs_display().fold(String::new(), |mut s, e| {