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 };