From c06a0a014736d405fae89bbc950db380ebef9245 Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:35:17 -0500 Subject: [PATCH 1/6] serde nonsense --- automata/src/automatan/pda.rs | 73 +++++++++++++++++-------------- automata/src/automatan/tm.rs | 74 +++++++++++++++++-------------- automata/src/lib.rs | 82 ++++++++++++++++++++++++++++++++++- automata/src/loader/mod.rs | 28 ++++++------ 4 files changed, 176 insertions(+), 81 deletions(-) diff --git a/automata/src/automatan/pda.rs b/automata/src/automatan/pda.rs index 5532a14..80f82d7 100644 --- a/automata/src/automatan/pda.rs +++ b/automata/src/automatan/pda.rs @@ -2,51 +2,60 @@ use std::collections::HashSet; use super::*; -use crate::{delta_lower, gamma_upper, loader::{ +use crate::{delta_lower, dual_struct_serde, gamma_upper, loader::{ Context, INITIAL_STACK, INITIAL_STATE, Spanned, ast::{self, Symbol as Sym}, log::LogSink }, sigma_upper}; -#[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>, +dual_struct_serde! { + #[derive(Debug, PartialEq, Eq, Clone, Hash)] + pub struct TransitionFrom<'a> { + #[serde(borrow)] + pub state: State<'a>, + #[serde(borrow)] + pub letter: Option>, + #[serde(borrow)] + 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>, +dual_struct_serde! { + #[derive(Debug, PartialEq, Eq, Clone, Hash)] + pub struct TransitionTo<'a> { + #[serde(borrow)] + pub state: State<'a>, + #[serde(borrow)] + pub stack: Vec>, - pub transition: Span, - pub function: Span, + 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>, +dual_struct_serde! { {#[serde_with::serde_as]} + #[derive(Clone, Debug)] + pub struct Pda<'a> { + #[serde(borrow)] + pub initial_state: State<'a>, + #[serde(borrow)] + pub initial_stack: Symbol<'a>, + #[serde(borrow)] + pub states: HashMap, StateInfo>, + #[serde(borrow)] + pub symbols: HashMap, SymbolInfo>, + #[serde(borrow)] + pub alphabet: HashMap, LetterInfo>, - pub final_states: Option, StateInfo>>, + #[serde(borrow)] + 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>>, + #[serde(borrow)] + #[serde_as(as = "serde_with::Seq<(_, _)>")] + pub transitions: HashMap, HashSet>>, + } } impl<'a> Pda<'a> { - pub fn parse( + pub fn compile( items: impl Iterator>>, ctx: &mut Context<'a>, options: Options, diff --git a/automata/src/automatan/tm.rs b/automata/src/automatan/tm.rs index 179ab45..7c52244 100644 --- a/automata/src/automatan/tm.rs +++ b/automata/src/automatan/tm.rs @@ -2,57 +2,65 @@ use std::collections::HashSet; use super::*; -use crate::{delta_lower, gamma_upper, loader::{ +use crate::{delta_lower, dual_struct_serde, gamma_upper, loader::{ BLANK_SYMBOL, Context, Spanned, ast::{self, Symbol as Sym}, log::LogSink }}; - -#[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>, +dual_struct_serde! { + #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] + pub struct TransitionFrom<'a> { + #[serde(borrow)] + pub state: State<'a>, + #[serde(borrow)] + pub symbol: Symbol<'a>, + } } -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 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, +dual_struct_serde! { + #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] + pub struct TransitionTo<'a> { + #[serde(borrow)] + pub state: State<'a>, + #[serde(borrow)] + pub symbol: Symbol<'a>, + pub direction: Direction, - pub transition: Span, - pub function: Span, + 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>, +dual_struct_serde! {{#[serde_with::serde_as]} + #[derive(Clone, Debug)] + pub struct Tm<'a> { + #[serde(borrow)] + pub initial_state: State<'a>, + #[serde(borrow)] + pub initial_tape: Symbol<'a>, + #[serde(borrow)] + pub states: HashMap, StateInfo>, + #[serde(borrow)] + pub symbols: HashMap, SymbolInfo>, - pub final_states: HashMap, StateInfo>, + #[serde(borrow)] + 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>>, + + #[serde(borrow)] + #[serde_as(as = "serde_with::Seq<(_, _)>")] + pub transitions: HashMap, HashSet>>, + } } impl<'a> Tm<'a> { - pub fn parse( + pub fn compile( items: impl Iterator>>, ctx: &mut Context<'a>, options: Options, diff --git a/automata/src/lib.rs b/automata/src/lib.rs index ecad2bb..f1b2f24 100644 --- a/automata/src/lib.rs +++ b/automata/src/lib.rs @@ -1,7 +1,6 @@ pub mod automatan; pub mod loader; - #[macro_export] macro_rules! dual_struct_serde { ($({$(#[$serde_specific:meta])*})? @@ -33,4 +32,83 @@ macro_rules! dual_struct_serde { ),* } }; -} \ No newline at end of file +} + +#[macro_export] +macro_rules! dual_enum_serde { + ( + $( {$(#[$serde_specific:meta])*} )? + $(#[$enum_meta:meta])* + $vis:vis enum $Name:ident $(<$($gen:tt),*>)? + { + $( + $(#[$variant_meta:meta])* + $Variant:ident + $( + // Tuple variant: Variant(T1, T2, ...) + ( $( + $(#[$tfield_meta:meta])* + $tfield_ty:ty + ),* $(,)? ) + )? + $( + // Struct variant: Variant { a: T, b: U, ... } + { $( + $(#[$sfield_meta:meta])* + $sfield_vis:vis $sfield_name:ident : $sfield_ty:ty + ),* $(,)? } + )? + ),* $(,)? + } + ) => { + #[cfg(feature = "serde")] + $(#[$enum_meta])* + #[derive(serde::Serialize, serde::Deserialize)] + $( $(#[$serde_specific])* )? + $vis enum $Name $(<$($gen),*>)? { + $( + $(#[$variant_meta])* + $Variant + $( + ( + $( + $(#[$tfield_meta])* + $tfield_ty + ),* + ) + )? + $( + { + $( + $(#[$sfield_meta])* + $sfield_vis $sfield_name: $sfield_ty + ),* + } + )? + ),* + } + + #[cfg(not(feature = "serde"))] + $(#[$enum_meta])* + $vis enum $Name $(<$($gen),*>)? { + $( + // strip variant + field attrs in non-serde version + $Variant + $( + ( + $( + $tfield_ty + ),* + ) + )? + $( + { + $( + $sfield_vis $sfield_name: $sfield_ty + ),* + } + )? + ),* + } + }; +} diff --git a/automata/src/loader/mod.rs b/automata/src/loader/mod.rs index 5379a3d..ef4546b 100644 --- a/automata/src/loader/mod.rs +++ b/automata/src/loader/mod.rs @@ -1,9 +1,8 @@ use crate::{ - automatan::*, - loader::{ + automatan::*, dual_enum_serde, dual_struct_serde, loader::{ ast::TopLevel, log::{LogEntry, LogSink}, - }, + } }; pub mod ast; @@ -120,13 +119,14 @@ 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>), - Tm(tm::Tm<'a>), +dual_enum_serde!{ + {#[serde(tag = "type")] #[serde(rename_all = "snake_case")]} + #[derive(Clone, Debug)] + pub enum Machine<'a> { + Fa(#[serde(borrow)] fa::Fa<'a>), + Pda(#[serde(borrow)] pda::Pda<'a>), + Tm(#[serde(borrow)] tm::Tm<'a>), + } } pub fn parse_universal<'a>(ctx: &mut Context<'a>) -> Option> { @@ -194,9 +194,9 @@ pub fn parse_universal<'a>(ctx: &mut Context<'a>) -> Option> { Some(match parse_type(items.next(), ctx)? { Type::Dfa => Machine::Fa(fa::Fa::compile(items, ctx, D)?), Type::Nfa => Machine::Fa(fa::Fa::compile(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)?), + Type::Dpda => Machine::Pda(pda::Pda::compile(items, ctx, D)?), + Type::Npda => Machine::Pda(pda::Pda::compile(items, ctx, N)?), + Type::Tm => Machine::Tm(tm::Tm::compile(items, ctx, D)?), + Type::Ntm => Machine::Tm(tm::Tm::compile(items, ctx, N)?), }) } From d6e4fff7821be94e7a6dc1bd9a92263443984ad0 Mon Sep 17 00:00:00 2001 From: ParkerTenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:35:39 -0500 Subject: [PATCH 2/6] improved error messages for TM and PDA's --- automata/src/automatan/fa.rs | 27 +- automata/src/automatan/mod.rs | 19 +- automata/src/automatan/pda.rs | 705 ++++++++++++++++++++-------------- automata/src/automatan/tm.rs | 547 +++++++++++++++----------- automata/src/loader/ast.rs | 28 +- automata/src/loader/lexer.rs | 5 +- automata/src/loader/log.rs | 1 - automata/src/loader/mod.rs | 10 +- automata/src/loader/parser.rs | 2 +- cli/src/main.rs | 6 +- web/root/src/automata.ts | 2 +- web_lib/src/lib.rs | 2 +- 12 files changed, 817 insertions(+), 537 deletions(-) diff --git a/automata/src/automatan/fa.rs b/automata/src/automatan/fa.rs index 66f4989..ab8cecc 100644 --- a/automata/src/automatan/fa.rs +++ b/automata/src/automatan/fa.rs @@ -3,11 +3,13 @@ use std::collections::HashSet; use super::*; use crate::{ - delta_lower, dual_struct_serde, epsilon, loader::{ + delta_lower, dual_struct_serde, epsilon, + loader::{ Context, INITIAL_STATE, Spanned, ast::{self, Symbol as Sym, TopLevel}, log::LogSink, - }, sigma_upper + }, + sigma_upper, }; dual_struct_serde! { @@ -104,6 +106,12 @@ impl<'a, 'b> FaCompiler<'a, 'b> { self.compile_top_level(element, span); } + if self.states_def.is_none() { + self.ctx + .emit_error_locless("states never defined") + .emit_help_logless("add: Q = {...}"); + } + if self.alphabet_def.is_none() { self.ctx .emit_error_locless("alphabet never defined") @@ -111,12 +119,6 @@ impl<'a, 'b> FaCompiler<'a, 'b> { .emit_info_logless(concat!("E can be ", sigma_upper!(str))); } - if self.states_def.is_none() { - self.ctx - .emit_error_locless("states never defined") - .emit_help_logless("add: Q = {...}"); - } - if self.final_states_def.is_none() { self.ctx .emit_error_locless("final states never defined") @@ -139,9 +141,12 @@ impl<'a, 'b> FaCompiler<'a, 'b> { } }; - if self.transitions.is_empty(){ - self.ctx.emit_warning_locless("no transitions defined") - .emit_help_logless("consider defining one: d(state, letter|epsilon) = state | {state, state, ...}") + if self.transitions.is_empty() { + self.ctx + .emit_warning_locless("no transitions defined") + .emit_help_logless( + "consider defining one: d(state, letter|epsilon) = state | {state, ...}", + ) .emit_info_logless(concat!("d can be ", delta_lower!(str))) .emit_info_logless(concat!("epsilon can be ", epsilon!(str))); } diff --git a/automata/src/automatan/mod.rs b/automata/src/automatan/mod.rs index bf457c6..58750a4 100644 --- a/automata/src/automatan/mod.rs +++ b/automata/src/automatan/mod.rs @@ -6,7 +6,6 @@ pub mod fa; pub mod pda; pub mod tm; - #[derive(Clone, Copy, Debug)] pub struct Options { pub non_deterministic: bool, @@ -14,15 +13,27 @@ pub struct Options { } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(transparent) +)] pub struct State<'a>(pub &'a str); #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(transparent) +)] pub struct Symbol<'a>(pub &'a str); #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] +#[cfg_attr( + feature = "serde", + derive(serde::Serialize, serde::Deserialize), + serde(transparent) +)] pub struct Letter<'a>(pub &'a str); #[derive(Clone, Debug)] diff --git a/automata/src/automatan/pda.rs b/automata/src/automatan/pda.rs index 80f82d7..5959ed9 100644 --- a/automata/src/automatan/pda.rs +++ b/automata/src/automatan/pda.rs @@ -2,9 +2,15 @@ use std::collections::HashSet; use super::*; -use crate::{delta_lower, dual_struct_serde, gamma_upper, loader::{ - Context, INITIAL_STACK, INITIAL_STATE, Spanned, ast::{self, Symbol as Sym}, log::LogSink -}, sigma_upper}; +use crate::{ + delta_lower, dual_struct_serde, epsilon, gamma_upper, + loader::{ + Context, INITIAL_STACK, INITIAL_STATE, Spanned, + ast::{self, Symbol as Sym}, + log::LogSink, + }, + sigma_upper, +}; dual_struct_serde! { #[derive(Debug, PartialEq, Eq, Clone, Hash)] @@ -54,315 +60,446 @@ dual_struct_serde! { {#[serde_with::serde_as]} } } +pub struct PdaCompiler<'a, 'b> { + ctx: &'b mut Context<'a>, + options: Options, + + initial_state: Option<(State<'a>, Span)>, + initial_stack: Option<(Symbol<'a>, Span)>, + + states: HashMap, StateInfo>, + states_def: Option, + + symbols: HashMap, SymbolInfo>, + symbols_def: Option, + + alphabet: HashMap, LetterInfo>, + alphabet_def: Option, + + final_states: HashMap, StateInfo>, + final_states_def: Option, + + transitions: HashMap, HashSet>>, +} + impl<'a> Pda<'a> { pub fn compile( items: impl Iterator>>, ctx: &mut Context<'a>, options: Options, ) -> Option> { - let mut initial_state = None; - let mut initial_stack = None; + PdaCompiler::new(ctx, options).compile(items) + } +} - let mut states = HashMap::new(); - let mut symbols = HashMap::new(); - let mut alphabet = HashMap::new(); - let mut final_states = None; +impl<'a, 'b> PdaCompiler<'a, 'b> { + pub fn new(ctx: &'b mut Context<'a>, options: Options) -> Self { + Self { + ctx, + options, - let mut transitions: HashMap, HashSet>> = - HashMap::new(); + initial_state: Default::default(), + initial_stack: Default::default(), + states: Default::default(), + states_def: Default::default(), + symbols: Default::default(), + symbols_def: Default::default(), + alphabet: Default::default(), + alphabet_def: Default::default(), + final_states: Default::default(), + final_states_def: Default::default(), + transitions: Default::default(), + } + } + pub fn compile( + mut self, + items: impl Iterator>>, + ) -> Option> { 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(sigma_upper!(pat), _), 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_some() - { - 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(gamma_upper!(pat), _), 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(INITIAL_STATE, _), 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(INITIAL_STACK, _), 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 states, alphabet, symbols, final states, initial state, initial stack"), dest_s); - } - - TL::TransitionFunc(S((S(delta_lower!(pat), _), 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 transition function ( {} )", delta_lower!(str) - ), - dest_s, - ); - } - - TL::ProductionRule(_, _) => { - ctx.emit_error("unexpected production rule", span); - } - TL::Table() => _ = ctx.emit_error("unexpected table", span), - } + self.compile_top_level(element, span); } - if symbols.is_empty() { - ctx.emit_error_locless("stack symbols never defined"); + if self.states_def.is_none() { + self.ctx + .emit_error_locless("states never defined") + .emit_help_logless("add: Q = {...}"); } - if alphabet.is_empty() { - ctx.emit_error_locless("alphabet never defined"); + if self.alphabet_def.is_none() { + self.ctx + .emit_error_locless("alphabet never defined") + .emit_help_logless("add: E = {...}") + .emit_info_logless(concat!("E can be ", sigma_upper!(str))); } - if states.is_empty() { - ctx.emit_error_locless("states never defined"); + if self.symbols_def.is_none() { + self.ctx + .emit_error_locless("stack symbols never defined") + .emit_help_logless("add: G = {...}") + .emit_info_logless(concat!("G can be ", gamma_upper!(str))); } - let initial_stack = match initial_stack { - Some(some) => some, + // if self.final_states_def.is_none() { + // self.ctx + // .emit_error_locless("final states never defined") + // .emit_help_logless("add: F = {...}"); + // } + + let initial_state = match self.initial_state { + Some(some) => some.0, None => { - if symbols.contains_key(&Symbol("Z0")) { - ctx.emit_warning_locless( - "initial stack symbol not defined, defaulting to 'Z0'", - ); + if self.states.contains_key(&State("q0")) { + self.ctx + .emit_warning_locless("initial state not defined, defaulting to 'q0'") + .emit_help_logless(format!("add: {INITIAL_STATE} = q0")); } 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"); + self.ctx + .emit_error_locless("initial state not defined") + .emit_help_logless(format!("add: {INITIAL_STATE} = ...")); } State("q0") } }; - if ctx.contains_errors() { + let initial_stack = match self.initial_stack { + Some(some) => some.0, + None => { + if self.symbols.contains_key(&Symbol("Z0")) { + self.ctx + .emit_warning_locless( + "initial stack symbol not defined, defaulting to 'Z0'", + ) + .emit_help_logless(format!("add: {INITIAL_STACK} = Z0")); + } else { + self.ctx + .emit_error_locless("initial stack symbol not defined") + .emit_help_logless(format!("add: {INITIAL_STACK} = ...")); + } + Symbol("Z0") + } + }; + + if self.transitions.is_empty() { + self.ctx + .emit_warning_locless("no transitions defined") + .emit_help_logless( + "consider defining one: d(state, letter|epsilon, symbol) = (state, [symbol]) | {(state, [symbol]), ...}", + ) + .emit_info_logless(concat!("d can be ", delta_lower!(str))) + .emit_info_logless(concat!("epsilon can be ", epsilon!(str))); + } + + if self.ctx.contains_errors() { return None; } Some(Pda { initial_state, initial_stack, - states, - symbols, - alphabet, - final_states, - transitions, + states: self.states, + symbols: self.symbols, + alphabet: self.alphabet, + final_states: Some(self.final_states), + transitions: self.transitions, }) } + + fn compile_top_level(&mut self, element: ast::TopLevel<'a>, span: Span) { + use Spanned as S; + use ast::TopLevel as TL; + match element { + TL::Item(S("Q", _), list) => self.compile_states(list, span), + TL::Item(S(gamma_upper!(pat), _), list) => self.compile_symbols(list, span), + TL::Item(S(sigma_upper!(pat), _), list) => self.compile_alphabet(list, span), + TL::Item(S("F", _), list) => self.compile_final_states(list, span), + TL::Item(S(INITIAL_STATE, _), item) => self.compile_initial_state(item, span), + TL::Item(S(INITIAL_STACK, _), item) => self.compile_initial_stack(item, span), + TL::Item(S(name, dest_s), _) => { + self.ctx.emit_error(format!("unknown item {name:?}, expected states, stack symbols, alphabet, final states, initial state, initial stack"), dest_s); + } + + TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { + self.compile_transition_function(args, list) + } + TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { + self.ctx.emit_error( + format!( + "unknown function {name:?}, expected transition function ( {} )", + delta_lower!(str) + ), + dest_s, + ); + } + + TL::ProductionRule(_, _) => { + self.ctx.emit_error("unexpected production rule", span); + } + TL::Table() => _ = self.ctx.emit_error("unexpected table", span), + } + } + + fn compile_states(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.states_def { + self.ctx + .emit_error("states already set", top_level) + .emit_info("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + if let Some(previous) = self + .states + .insert(State(ident), StateInfo { definition: item.1 }) + { + self.ctx + .emit_error("state redefined", item.1) + .emit_info("previously defined here", previous.definition); + } + } + + if list.is_empty() { + self.ctx.emit_error("states cannot be empty", top_level); + } + self.states_def = Some(top_level); + } + + fn compile_symbols(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.symbols_def { + self.ctx + .emit_error("stack symbols already set", top_level) + .emit_info("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + if let Some(previous) = self + .symbols + .insert(Symbol(ident), SymbolInfo { definition: item.1 }) + { + self.ctx + .emit_error("stack symbol redefined", item.1) + .emit_info("previously defined here", previous.definition); + } + } + + if list.is_empty() { + self.ctx.emit_error("states cannot be empty", top_level); + } + self.symbols_def = Some(top_level); + } + + fn compile_alphabet(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.alphabet_def { + self.ctx + .emit_error("alphabet already set", top_level) + .emit_info("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + + if ident.chars().count() != 1 { + self.ctx + .emit_error("letter cannot be longer than one char", item.1); + } + + if let Some(previous) = self + .alphabet + .insert(Letter(ident), LetterInfo { definition: item.1 }) + { + self.ctx + .emit_error("letter redefined", item.1) + .emit_help("previously defined here", previous.definition); + } + } + if list.is_empty() { + self.ctx.emit_error("alphabet cannot be empty", top_level); + } + self.alphabet_def = Some(top_level); + } + + fn compile_final_states(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.final_states_def { + self.ctx + .emit_error("final states already set", top_level) + .emit_help("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + if self.states.contains_key(&State(ident)) { + if self + .final_states + .insert(State(ident), StateInfo { definition: item.1 }) + .is_some() + { + self.ctx.emit_error("final state redefined", item.1); + } + } else { + self.ctx + .emit_error("final state not defined in set of states", item.1); + } + } + self.final_states_def = Some(top_level); + } + + fn compile_initial_state( + &mut self, + Spanned(src, src_d): Spanned>, + top_level: Span, + ) { + match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if let Some((_, previous)) = self.initial_state { + self.ctx + .emit_error("initial state already set", top_level) + .emit_help("previously defined here", previous); + } + if self.states.contains_key(&State(ident)) { + self.initial_state = Some((State(ident), top_level)) + } else { + self.ctx + .emit_error("initial state symbol not defined as a state", src_d); + } + } + _ => _ = self.ctx.emit_error("expected ident", src_d), + } + } + + fn compile_initial_stack( + &mut self, + Spanned(src, src_d): Spanned>, + top_level: Span, + ) { + match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if let Some((_, previous)) = self.initial_stack { + self.ctx + .emit_error("initial stack symbol already set", top_level) + .emit_help("previously defined here", previous); + } + if self.symbols.contains_key(&Symbol(ident)) { + self.initial_stack = Some((Symbol(ident), top_level)) + } else { + self.ctx + .emit_error("initial stack symbol not defined as a state", src_d); + } + } + _ => _ = self.ctx.emit_error("expected ident", src_d), + } + } + + fn compile_transition_function( + &mut self, + args: Spanned>, + list: Spanned>, + ) { + let list = list.set_weak(); + let Some((state, letter, stack_symbol)) = + args.as_ref().expect_pda_transition_function(self.ctx) + else { + return; + }; + if !self.states.contains_key(&State(state.0)) { + self.ctx + .emit_error("transition state not defined as state", state.1); + return; + }; + if !self.symbols.contains_key(&Symbol(stack_symbol.0)) { + self.ctx.emit_error( + "transition stack symbol not defined as stack symbol", + stack_symbol.1, + ); + return; + }; + + let letter: Option> = match letter.0 { + Sym::Epsilon(_) => { + if !self.options.epsilon_moves { + self.ctx.emit_error("epsilon moves not permitted", letter.1); + } + None + } + Sym::Ident(val) => { + if !self.alphabet.contains_key(&Letter(val)) { + self.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(self.ctx) + .and_then(|item| item.expect_pda_transition(self.ctx)) + else { + continue; + }; + + if !self.states.contains_key(&State(next_state.0)) { + self.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(self.ctx)?; + + if !self.symbols.contains_key(&Symbol(ident)) { + self.ctx + .emit_error("transition stack symbol not defined", symbol.1); + return None; + }; + Some(Symbol(ident)) + }) + .collect(); + + let entry: &mut _ = self + .transitions + .entry(TransitionFrom { + letter, + state: State(state.0), + symbol: Symbol(stack_symbol.0), + }) + .or_default(); + if !entry.is_empty() && !self.options.non_deterministic { + self.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: args.1, + transition: item.1, + }) { + self.ctx.emit_warning("duplicate transition", item.1); + } + } + } } impl<'a, 'b> Spanned<&'b ast::Tuple<'a>> { @@ -382,10 +519,12 @@ impl<'a, 'b> Spanned<&'b ast::Tuple<'a>> { Spanned(symbol, *symbol_span), )); } - _ => _ = ctx.emit_error( - "expected PDA transition function (state, letter|epsilon, symbol)", - self.1, - ), + _ => { + _ = ctx.emit_error( + "expected PDA transition function (state, letter|epsilon, symbol)", + self.1, + ) + } } None } diff --git a/automata/src/automatan/tm.rs b/automata/src/automatan/tm.rs index 7c52244..8e9906d 100644 --- a/automata/src/automatan/tm.rs +++ b/automata/src/automatan/tm.rs @@ -2,9 +2,14 @@ use std::collections::HashSet; use super::*; -use crate::{delta_lower, dual_struct_serde, gamma_upper, loader::{ - BLANK_SYMBOL, Context, Spanned, ast::{self, Symbol as Sym}, log::LogSink -}}; +use crate::{ + delta_lower, dual_struct_serde, + loader::{ + BLANK_SYMBOL, Context, INITIAL_STATE, Spanned, + ast::{self, Symbol as Sym}, + log::LogSink, + }, +}; dual_struct_serde! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub struct TransitionFrom<'a> { @@ -43,7 +48,7 @@ dual_struct_serde! {{#[serde_with::serde_as]} #[serde(borrow)] pub initial_state: State<'a>, #[serde(borrow)] - pub initial_tape: Symbol<'a>, + pub blank_symbol: Symbol<'a>, #[serde(borrow)] pub states: HashMap, StateInfo>, #[serde(borrow)] @@ -52,7 +57,7 @@ dual_struct_serde! {{#[serde_with::serde_as]} #[serde(borrow)] pub final_states: HashMap, StateInfo>, - + #[serde(borrow)] #[serde_as(as = "serde_with::Seq<(_, _)>")] pub transitions: HashMap, HashSet>>, @@ -65,237 +70,341 @@ impl<'a> Tm<'a> { ctx: &mut Context<'a>, options: Options, ) -> Option> { - let mut initial_state = None; - let mut initial_tape = None; + TmCompiler::new(ctx, options).compile(items) + } +} - let mut states = HashMap::new(); - let mut symbols = HashMap::new(); - let mut final_states = HashMap::new(); +pub struct TmCompiler<'a, 'b> { + ctx: &'b mut Context<'a>, + options: Options, - let mut transitions: HashMap, HashSet>> = - HashMap::new(); + initial_state: Option<(State<'a>, Span)>, + blank_symbol: Option<(Symbol<'a>, Span)>, + states: HashMap, StateInfo>, + states_def: Option, + + symbols: HashMap, SymbolInfo>, + symbols_def: Option, + + final_states: HashMap, StateInfo>, + final_states_def: Option, + + transitions: HashMap, HashSet>>, +} + +impl<'a, 'b> TmCompiler<'a, 'b> { + pub fn new(ctx: &'b mut Context<'a>, options: Options) -> Self { + Self { + ctx, + options, + + initial_state: Default::default(), + blank_symbol: Default::default(), + states: Default::default(), + states_def: Default::default(), + symbols: Default::default(), + symbols_def: Default::default(), + final_states: Default::default(), + final_states_def: Default::default(), + transitions: Default::default(), + } + } + + pub fn compile( + mut self, + items: impl Iterator>>, + ) -> Option> { 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(gamma_upper!(pat), _), 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("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(BLANK_SYMBOL, _), 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 states, symbols, final states, initial state, blank symbol"), dest_s); - } - - TL::TransitionFunc(S((S(delta_lower!(pat), _), 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 transition function ( {} )", delta_lower!(str) - ), - dest_s, - ); - } - - TL::ProductionRule(_, _) => { - ctx.emit_error("unexpected production rule", span); - } - TL::Table() => _ = ctx.emit_error("unexpected table", span), - } + self.compile_top_level(element, span); } - if symbols.is_empty() { - ctx.emit_error_locless("tape symbols never defined"); + if self.final_states_def.is_none() { + self.ctx + .emit_error_locless("final states never defined") + .emit_help_logless("add: F = {...}"); } - if states.is_empty() { - ctx.emit_error_locless("states never defined"); - } - - let initial_tape = match initial_tape { - Some(some) => some, + let initial_state = match self.initial_state { + Some(some) => some.0, None => { - if symbols.contains_key(&Symbol("z0")) { - ctx.emit_warning_locless("initial tape symbol not defined, defaulting to 'z0'"); + if self.states.contains_key(&State("q0")) { + self.ctx + .emit_warning_locless("initial state not defined, defaulting to 'q0'") + .emit_help_logless(format!("add: {INITIAL_STATE} = q0")); } 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"); + self.ctx + .emit_error_locless("initial state not defined") + .emit_help_logless(format!("add: {BLANK_SYMBOL} = ...")); } State("q0") } }; - if ctx.contains_errors() { + let blank_symbol = match self.blank_symbol { + Some(some) => some.0, + None => { + if self.symbols.contains_key(&Symbol("B")) { + self.ctx + .emit_warning_locless("blank symbol not defined, defaulting to 'B'") + .emit_help_logless(format!("add: {BLANK_SYMBOL} = B")); + } else { + self.ctx + .emit_error_locless("blank symbol not defined") + .emit_help_logless(format!("add: {BLANK_SYMBOL} = ...")); + } + Symbol("B") + } + }; + + if self.transitions.is_empty() { + self.ctx + .emit_warning_locless("no transitions defined") + .emit_help_logless( + "consider defining one: d(state, symbol) = (state, symbol, direction) | {(state, symbol, direction), ...}", + ) + .emit_info_logless(concat!("d can be ", delta_lower!(str))); + } + + if self.ctx.contains_errors() { return None; } Some(Tm { initial_state, - initial_tape, - states, - symbols, - final_states, - transitions, + blank_symbol, + states: self.states, + symbols: self.symbols, + final_states: self.final_states, + transitions: self.transitions, }) } + + fn compile_top_level(&mut self, element: ast::TopLevel<'a>, span: Span) { + use Spanned as S; + use ast::TopLevel as TL; + match element { + TL::Item(S("Q", _), list) => self.compile_states(list, span), + TL::Item(S(delta_lower!(pat), _), list) => self.compile_symbols(list, span), + TL::Item(S("F", _), list) => self.compile_final_states(list, span), + TL::Item(S(INITIAL_STATE, _), item) => self.compile_initial_state(item, span), + TL::Item(S(BLANK_SYMBOL, _), item) => self.compile_blank_symbol(item, span), + TL::Item(S(name, dest_s), _) => { + self.ctx.emit_error(format!("unknown item {name:?}, expected states, symbols, final states, initial state, blank symbol"), dest_s); + } + + TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { + self.compile_transition_function(args, list) + } + TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { + self.ctx.emit_error( + format!( + "unknown function {name:?}, expected transition function ( {} )", + delta_lower!(str) + ), + dest_s, + ); + } + + TL::ProductionRule(_, _) => { + self.ctx.emit_error("unexpected production rule", span); + } + TL::Table() => _ = self.ctx.emit_error("unexpected table", span), + } + } + + fn compile_states(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.states_def { + self.ctx + .emit_error("states already set", top_level) + .emit_info("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + if let Some(previous) = self + .states + .insert(State(ident), StateInfo { definition: item.1 }) + { + self.ctx + .emit_error("state redefined", item.1) + .emit_info("previously defined here", previous.definition); + } + } + + if list.is_empty() { + self.ctx.emit_error("states cannot be empty", top_level); + } + self.states_def = Some(top_level); + } + + fn compile_symbols(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.symbols_def { + self.ctx + .emit_error("stack symbols already set", top_level) + .emit_info("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + if let Some(previous) = self + .symbols + .insert(Symbol(ident), SymbolInfo { definition: item.1 }) + { + self.ctx + .emit_error("stack symbol redefined", item.1) + .emit_info("previously defined here", previous.definition); + } + } + + if list.is_empty() { + self.ctx.emit_error("states cannot be empty", top_level); + } + self.symbols_def = Some(top_level); + } + + fn compile_final_states(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.final_states_def { + self.ctx + .emit_error("final states already set", top_level) + .emit_help("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + if self.states.contains_key(&State(ident)) { + if self + .final_states + .insert(State(ident), StateInfo { definition: item.1 }) + .is_some() + { + self.ctx.emit_error("final state redefined", item.1); + } + } else { + self.ctx + .emit_error("final state not defined in set of states", item.1); + } + } + self.final_states_def = Some(top_level); + } + + fn compile_initial_state( + &mut self, + Spanned(src, src_d): Spanned>, + top_level: Span, + ) { + match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if let Some((_, previous)) = self.initial_state { + self.ctx + .emit_error("initial state already set", top_level) + .emit_help("previously defined here", previous); + } + if self.states.contains_key(&State(ident)) { + self.initial_state = Some((State(ident), top_level)) + } else { + self.ctx + .emit_error("initial state symbol not defined as a state", src_d); + } + } + _ => _ = self.ctx.emit_error("expected ident", src_d), + } + } + + fn compile_blank_symbol( + &mut self, + Spanned(src, src_d): Spanned>, + top_level: Span, + ) { + match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if let Some((_, previous)) = self.blank_symbol { + self.ctx + .emit_error("blank symbol already set", top_level) + .emit_help("previously defined here", previous); + } + if self.states.contains_key(&State(ident)) { + self.blank_symbol = Some((Symbol(ident), top_level)) + } else { + self.ctx + .emit_error("blank symbol not defined as a state", src_d); + } + } + _ => _ = self.ctx.emit_error("expected ident", src_d), + } + } + + fn compile_transition_function( + &mut self, + args: Spanned>, + list: Spanned>, + ) { + let list = list.set_weak(); + let Some((from_state, from_tape)) = args.as_ref().expect_tm_transition_function(self.ctx) + else { + return; + }; + if !self.states.contains_key(&State(from_state.0)) { + self.ctx + .emit_error("transition state not defined as state", from_state.1); + return; + }; + if !self.symbols.contains_key(&Symbol(from_tape.0)) { + self.ctx.emit_error( + "transition tape symbol not defined as tape symbol", + from_tape.1, + ); + return; + }; + + for item in list { + let Some((to_state, to_tape, direction)) = item + .expect_tuple(self.ctx) + .and_then(|item| item.expect_tm_transition(self.ctx)) + else { + continue; + }; + + if !self.states.contains_key(&State(to_state.0)) { + self.ctx + .emit_error("transition state not defined as state", to_state.1); + continue; + }; + + let entry: &mut _ = self + .transitions + .entry(TransitionFrom { + state: State(from_state.0), + symbol: Symbol(from_tape.0), + }) + .or_default(); + if !entry.is_empty() && !self.options.non_deterministic { + self.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: args.1, + transition: item.1, + }) { + self.ctx.emit_warning("duplicate transition", item.1); + } + } + } } impl<'a> Spanned<&ast::Tuple<'a>> { @@ -343,10 +452,12 @@ impl<'a> Spanned<&ast::Tuple<'a>> { Spanned(direction, *direction_span), )); } - _ => _ = ctx.emit_error( - "expected TM transition function (state, symbol, direction)", - self.1, - ), + _ => { + _ = ctx.emit_error( + "expected TM transition function (state, symbol, direction)", + self.1, + ) + } } None } diff --git a/automata/src/loader/ast.rs b/automata/src/loader/ast.rs index 4db3aca..a70ed32 100644 --- a/automata/src/loader/ast.rs +++ b/automata/src/loader/ast.rs @@ -77,7 +77,9 @@ impl<'a> Spanned> { 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), } @@ -86,8 +88,12 @@ impl<'a> Spanned> { pub fn expect_set(&self, ctx: &mut Context<'a>) -> Option<&[Spanned>]> { match &self.0 { - Item::Symbol(Symbol::Ident(_)) => _ = ctx.emit_error("expected set found ident", self.1), - Item::Symbol(Symbol::Epsilon(_)) => _ = ctx.emit_error("expected set found epsilon", self.1), + Item::Symbol(Symbol::Ident(_)) => { + _ = ctx.emit_error("expected set found ident", self.1) + } + Item::Symbol(Symbol::Epsilon(_)) => { + _ = ctx.emit_error("expected set found epsilon", self.1) + } Item::Tuple(_) => _ = ctx.emit_error("expected set found tuple", self.1), Item::List(list) => return Some(&list.0), } @@ -96,8 +102,12 @@ impl<'a> Spanned> { pub fn expect_list(&self, ctx: &mut Context<'a>) -> Option<&[Spanned>]> { match &self.0 { - Item::Symbol(Symbol::Ident(_)) => _ = ctx.emit_error("expected list found ident", self.1), - Item::Symbol(Symbol::Epsilon(_)) => _ = ctx.emit_error("expected list found epsilon", self.1), + Item::Symbol(Symbol::Ident(_)) => { + _ = ctx.emit_error("expected list found ident", self.1) + } + Item::Symbol(Symbol::Epsilon(_)) => { + _ = ctx.emit_error("expected list found epsilon", self.1) + } Item::Tuple(_) => _ = ctx.emit_error("expected list found tuple", self.1), Item::List(list) => return Some(&list.0), } @@ -120,8 +130,12 @@ 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::Ident(_)) => { + _ = ctx.emit_error("expected tuple found ident", 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), } diff --git a/automata/src/loader/lexer.rs b/automata/src/loader/lexer.rs index 183ecde..c4bc999 100644 --- a/automata/src/loader/lexer.rs +++ b/automata/src/loader/lexer.rs @@ -96,7 +96,10 @@ fn begin_ident(c: char) -> bool { } fn continue_ident(c: char) -> bool { - c.is_alphanumeric() || c == '_' || c=='\'' || (!c.is_ascii() && !c.is_control() && !c.is_whitespace()) + c.is_alphanumeric() + || c == '_' + || c == '\'' + || (!c.is_ascii() && !c.is_control() && !c.is_whitespace()) } impl<'a> std::iter::Iterator for Lexer<'a> { diff --git a/automata/src/loader/log.rs b/automata/src/loader/log.rs index 084047c..4766072 100644 --- a/automata/src/loader/log.rs +++ b/automata/src/loader/log.rs @@ -2,7 +2,6 @@ use std::fmt::Display; use crate::loader::Span; - #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Logs { logs: Vec, diff --git a/automata/src/loader/mod.rs b/automata/src/loader/mod.rs index ef4546b..88fbb12 100644 --- a/automata/src/loader/mod.rs +++ b/automata/src/loader/mod.rs @@ -1,8 +1,10 @@ use crate::{ - automatan::*, dual_enum_serde, dual_struct_serde, loader::{ + automatan::*, + dual_enum_serde, + loader::{ ast::TopLevel, log::{LogEntry, LogSink}, - } + }, }; pub mod ast; @@ -119,8 +121,8 @@ impl<'a> Context<'a> { } } -dual_enum_serde!{ - {#[serde(tag = "type")] #[serde(rename_all = "snake_case")]} +dual_enum_serde! { + {#[serde(tag = "type")] #[serde(rename_all = "snake_case")]} #[derive(Clone, Debug)] pub enum Machine<'a> { Fa(#[serde(borrow)] fa::Fa<'a>), diff --git a/automata/src/loader/parser.rs b/automata/src/loader/parser.rs index d03a5c9..2426339 100644 --- a/automata/src/loader/parser.rs +++ b/automata/src/loader/parser.rs @@ -93,7 +93,7 @@ impl<'a, 'b> Parser<'a, 'b> { fn parse_as_symbol(&mut self, tok: S>) -> S> { match tok { S(T::Tilde, r) => S(Symbol::Epsilon("~"), r), - S(T::Ident(repr@ epsilon!(pat)), r) => S(Symbol::Epsilon(repr), r), + S(T::Ident(repr @ epsilon!(pat)), r) => S(Symbol::Epsilon(repr), r), S(T::Ident(ident), r) => S(Symbol::Ident(ident), r), S(got, span) => { self.ctx.emit_error( diff --git a/cli/src/main.rs b/cli/src/main.rs index 496680a..da0f5d9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,5 +1 @@ - - -pub fn main(){ - -} \ No newline at end of file +pub fn main() {} diff --git a/web/root/src/automata.ts b/web/root/src/automata.ts index f1b8a12..6577551 100644 --- a/web/root/src/automata.ts +++ b/web/root/src/automata.ts @@ -189,7 +189,7 @@ export type Tm = { type: "tm"; initial_state: State; - initial_tape: Symbol; + blank_symbol: Symbol; states: Map; symbols: Map; alphabet: Map; diff --git a/web_lib/src/lib.rs b/web_lib/src/lib.rs index 75c4099..f38855d 100644 --- a/web_lib/src/lib.rs +++ b/web_lib/src/lib.rs @@ -94,7 +94,7 @@ pub fn lex(input: &str) -> Vec { } // ugly hack to keep single ascii letters non keyworded for user - Token::Ident(ident) if ident.is_ascii() && ident.len()==1 => Kind::Ident, + Token::Ident(ident) if ident.is_ascii() && ident.len() == 1 => Kind::Ident, Token::Ident( epsilon!(pat) | delta_lower!(pat) | sigma_upper!(pat) | gamma_upper!(pat), ) => Kind::Keyword, From ba996ee942a22e097a55a9d602ee07d35d722f76 Mon Sep 17 00:00:00 2001 From: ParkerTenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:20:42 -0500 Subject: [PATCH 3/6] added explicit accept by for PDAs --- automata/src/automatan/fa.rs | 2 +- automata/src/automatan/pda.rs | 81 ++++++++++++++++++++++++++++++++--- automata/src/automatan/tm.rs | 2 +- web/root/src/examples.ts | 23 +++++----- 4 files changed, 89 insertions(+), 19 deletions(-) diff --git a/automata/src/automatan/fa.rs b/automata/src/automatan/fa.rs index ab8cecc..ff7399b 100644 --- a/automata/src/automatan/fa.rs +++ b/automata/src/automatan/fa.rs @@ -173,7 +173,7 @@ impl<'a, 'b> FaCompiler<'a, 'b> { TL::Item(S("F", _), list) => self.compile_final_states(list, span), TL::Item(S(INITIAL_STATE, _), item) => self.compile_initial_state(item, span), TL::Item(S(name, dest_s), _) => { - self.ctx.emit_error(format!("unknown item {name:?}, expected states, alphabet, final states, initial state"), dest_s); + self.ctx.emit_error(format!("unknown item {name:?}, expected states | alphabet | final states | initial state"), dest_s); } TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { diff --git a/automata/src/automatan/pda.rs b/automata/src/automatan/pda.rs index 5959ed9..e3c2c02 100644 --- a/automata/src/automatan/pda.rs +++ b/automata/src/automatan/pda.rs @@ -60,12 +60,19 @@ dual_struct_serde! { {#[serde_with::serde_as]} } } +#[derive(Clone, Copy)] +enum AcceptBy { + EmptyStack, + FinalState, +} + pub struct PdaCompiler<'a, 'b> { ctx: &'b mut Context<'a>, options: Options, initial_state: Option<(State<'a>, Span)>, initial_stack: Option<(Symbol<'a>, Span)>, + accept_by: Option<(AcceptBy, Span)>, states: HashMap, StateInfo>, states_def: Option, @@ -92,6 +99,18 @@ impl<'a> Pda<'a> { } } +macro_rules! accept_empty { + ($ident: ident) => { + $crate::maker!($ident: "N","n","null","empty","E","Z0","z0") + }; +} + +macro_rules! accept_final { + ($ident: ident) => { + $crate::maker!($ident: "F","final") + }; +} + impl<'a, 'b> PdaCompiler<'a, 'b> { pub fn new(ctx: &'b mut Context<'a>, options: Options) -> Self { Self { @@ -100,6 +119,7 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { initial_state: Default::default(), initial_stack: Default::default(), + accept_by: Default::default(), states: Default::default(), states_def: Default::default(), symbols: Default::default(), @@ -140,11 +160,32 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { .emit_info_logless(concat!("G can be ", gamma_upper!(str))); } - // if self.final_states_def.is_none() { - // self.ctx - // .emit_error_locless("final states never defined") - // .emit_help_logless("add: F = {...}"); - // } + if self.accept_by.is_none() { + self.ctx + .emit_error_locless("accept by never defined") + .emit_help_logless("add: accept = N|F") + .emit_info_logless(concat!( + "accept by empty stack N can be ", + accept_empty!(str) + )) + .emit_info_logless(concat!( + "accept by final state F can be ", + accept_final!(str) + )); + } + + if self.final_states_def.is_none() + && matches!(self.accept_by, Some((AcceptBy::FinalState, _))) + { + self.ctx + .emit_error_locless("final states never defined") + .emit_help_logless("add: F = {...}"); + }else if let (Some((AcceptBy::EmptyStack, empty)), Some(states)) = (self.accept_by, self.final_states_def){ + self.ctx + .emit_error_locless("final states defined alongside accept by empty stack") + .emit_help("either remote to accept by empty stack", states) + .emit_help("or remote to accept by final state", empty); + } let initial_state = match self.initial_state { Some(some) => some.0, @@ -194,13 +235,16 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { return None; } + let final_states = + matches!(self.accept_by, Some((AcceptBy::FinalState, _))).then_some(self.final_states); + Some(Pda { initial_state, initial_stack, states: self.states, symbols: self.symbols, alphabet: self.alphabet, - final_states: Some(self.final_states), + final_states, transitions: self.transitions, }) } @@ -209,6 +253,7 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { use Spanned as S; use ast::TopLevel as TL; match element { + TL::Item(S("accept", _), item) => self.compile_accept_by(item, span), TL::Item(S("Q", _), list) => self.compile_states(list, span), TL::Item(S(gamma_upper!(pat), _), list) => self.compile_symbols(list, span), TL::Item(S(sigma_upper!(pat), _), list) => self.compile_alphabet(list, span), @@ -216,7 +261,7 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { TL::Item(S(INITIAL_STATE, _), item) => self.compile_initial_state(item, span), TL::Item(S(INITIAL_STACK, _), item) => self.compile_initial_stack(item, span), TL::Item(S(name, dest_s), _) => { - self.ctx.emit_error(format!("unknown item {name:?}, expected states, stack symbols, alphabet, final states, initial state, initial stack"), dest_s); + self.ctx.emit_error(format!("unknown item {name:?}, expected states | stack symbols | alphabet | accept by | final states | initial state | initial stack"), dest_s); } TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { @@ -239,6 +284,28 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { } } + fn compile_accept_by(&mut self, item: Spanned>, top_level: Span) { + if let Some((_, previous)) = self.accept_by { + self.ctx + .emit_error("accept by already set", top_level) + .emit_info("previously defined here", previous); + } + let Some(by) = item.expect_ident(self.ctx) else { + return; + }; + + let by = match by { + accept_empty!(pat) => AcceptBy::EmptyStack, + accept_final!(pat) => AcceptBy::FinalState, + _ => { + self.ctx.emit_error("invalid accept by", item.1); + return; + } + }; + + self.accept_by = Some((by, top_level)); + } + fn compile_states(&mut self, list: Spanned>, top_level: Span) { if let Some(previous) = self.states_def { self.ctx diff --git a/automata/src/automatan/tm.rs b/automata/src/automatan/tm.rs index 8e9906d..adc57f4 100644 --- a/automata/src/automatan/tm.rs +++ b/automata/src/automatan/tm.rs @@ -190,7 +190,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> { TL::Item(S(INITIAL_STATE, _), item) => self.compile_initial_state(item, span), TL::Item(S(BLANK_SYMBOL, _), item) => self.compile_blank_symbol(item, span), TL::Item(S(name, dest_s), _) => { - self.ctx.emit_error(format!("unknown item {name:?}, expected states, symbols, final states, initial state, blank symbol"), dest_s); + self.ctx.emit_error(format!("unknown item {name:?}, expected states | symbols | final states | initial state | blank symbol"), dest_s); } TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { diff --git a/web/root/src/examples.ts b/web/root/src/examples.ts index 6745845..c3cd7e6 100644 --- a/web/root/src/examples.ts +++ b/web/root/src/examples.ts @@ -27,11 +27,11 @@ export const examples: readonly Example[] = [ "DFA", `// strings over a,b which start and end with different letters -type = DFA // type of machine DFA, NFA, DPDA, NPDA, DTM, NTM -Q = {q0, qa, qa', qb, qb'} // set of states -E = {a, b} // alphabet -F = {qa', qb'} // set of final states -q0 = q0 // initial state +type = DFA // type of machine DFA, NFA, DPDA, NPDA, DTM, NTM +Q = {q0, qa, qa', qb, qb'} // set of states +E = {a, b} // alphabet +F = {qa', qb'} // set of final states +q0 = q0 // initial state // transition function (state, letter) -> state d(q0, a) = qa @@ -81,11 +81,12 @@ d(q4, 3) = q2`, new Example( "DPDA", "unequal", - `type=DPDA -Q = {q0, qas, qeq, qmb, qlb} // states -E = {a, b} // alphabet -T = {z0, A} // stack -F = {qmb, qlb} // final states + `type = DPDA +Q = {q0, qas, qeq, qmb, qlb} // states +E = {a, b} // alphabet +T = {z0, A} // stack +F = {qmb, qlb} // final states +accept = F // accept by final state q0 = q0 z0 = z0 @@ -112,6 +113,7 @@ d(qmb, b, z0) = (qmb, z0)`, Q = {q0, q1} // states E = {a, b} // alphabet T = {z0, A, B} // stack +accept = E // accept by empty stack q0 = q0 z0 = z0 @@ -142,6 +144,7 @@ d(q1, b, B) = { (q1, epsilon) }`, Q = {q0, q1} // states E = {a, b} // alphabet T = {z0, A, B} // stack +accept = E // accept by empty stack q0 = q0 z0 = z0 From 58fb1b956c817fade86257f6b002a5d377c8d776 Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:00:48 -0500 Subject: [PATCH 4/6] starting work on highlights --- web/root/src/editor.ts | 63 +++++++++++++++++++++++++++++++++++++- web/root/src/focus.ts | 0 web/root/style/editor.scss | 11 +++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 web/root/src/focus.ts diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts index d3594be..c3340e5 100644 --- a/web/root/src/editor.ts +++ b/web/root/src/editor.ts @@ -2,6 +2,7 @@ import { Decoration, + DecorationSet, EditorView, highlightActiveLine, highlightActiveLineGutter, @@ -10,7 +11,7 @@ import { lineNumbers, } from "npm:@codemirror/view"; -import { EditorState, StateField, Text } from "npm:@codemirror/state"; +import { EditorState, RangeSetBuilder, StateEffect, StateField, Text } from "npm:@codemirror/state"; import { defaultKeymap, history, @@ -45,6 +46,57 @@ function compile( } } + +export type HighlightKind = "focus" | "success" | "warning" | "error"; + +export type HighlightSpan = { + from: number; + to: number; + kind: HighlightKind; +}; + +function decoForKind(kind: HighlightKind) { + // Use a class per kind so each gets a distinct color via CSS + return Decoration.mark({ class: `cm-highlight cm-highlight-${kind}` }); +} + +export function applyHighlights(view: EditorView, spans: HighlightSpan[]) { + view.dispatch({ effects: setHighlights.of(spans) }); +} + + + +export const highlightsField = StateField.define({ + create() { + return Decoration.none; + }, + + update(highlights, tr) { + // Keep highlights aligned with document edits + // highlights = highlights.map(tr.changes); + + for (const e of tr.effects) { + if (e.is(setHighlights)) { + const spans = e.value; + + const builder = new RangeSetBuilder(); + for (const s of spans) { + const from = Math.max(0, Math.min(s.from, tr.state.doc.length)); + const to = Math.max(0, Math.min(s.to, tr.state.doc.length)); + if (to > from) builder.add(from, to, decoForKind(s.kind)); + } + highlights = builder.finish(); + } + } + + return highlights; + }, + + provide: (f) => EditorView.decorations.from(f), +}); + +export const setHighlights = StateEffect.define(); + const eventBusConnection = StateField.define({ create(state) { const text = state.doc.toString(); @@ -198,6 +250,7 @@ const state = EditorState.create({ keymap.of([...defaultKeymap, ...historyKeymap]), eventBusConnection, + highlightsField, diagHover, EditorView.lineWrapping, @@ -223,3 +276,11 @@ bus.on("controls/editor/set_text", ({ text }) => { bus.on("example/selected", ({ example }) => { bus.emit("controls/editor/set_text", { text: example.machine }); }); + + +applyHighlights(editor, [ + { from: 0, to: 10, kind: "focus" }, + { from: 10, to: 20, kind: "success" }, + { from: 20, to: 30, kind: "warning" }, + { from: 30, to: 40, kind: "error" }, +]) \ No newline at end of file diff --git a/web/root/src/focus.ts b/web/root/src/focus.ts new file mode 100644 index 0000000..e69de29 diff --git a/web/root/style/editor.scss b/web/root/style/editor.scss index 6cd740a..834cafa 100644 --- a/web/root/style/editor.scss +++ b/web/root/style/editor.scss @@ -132,3 +132,14 @@ text-underline-offset: 2px; } + + +.cm-highlight { + border-radius: 4px; + padding: 0 1px; +} + +.cm-highlight-warning { background: color-mix(in srgb, var(--warning) 40%, var(--bg-0)); } +.cm-highlight-focus { background: color-mix(in srgb, var(--focus) 40%, var(--bg-0)); } +.cm-highlight-success { background: color-mix(in srgb, var(--success) 40%, var(--bg-0)); } +.cm-highlight-error { background: color-mix(in srgb, var(--error) 40%, var(--bg-0)); } \ No newline at end of file From 7f519cd7f3258e546ac54140578c1fe10d279493 Mon Sep 17 00:00:00 2001 From: ParkerTenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:00:13 -0500 Subject: [PATCH 5/6] some form of highlighting --- automata/src/automatan/fa.rs | 8 +-- automata/src/automatan/pda.rs | 9 +-- automata/src/automatan/tm.rs | 7 +- web/root/src/bus.ts | 17 +++-- web/root/src/controls.ts | 2 +- web/root/src/editor.ts | 44 ++++-------- web/root/src/examples.ts | 2 +- web/root/src/focus.ts | 0 web/root/src/highlight.ts | 85 ++++++++++++++++++++++ web/root/src/simulation.ts | 15 ++-- web/root/src/visualizer.ts | 132 +++++++++++++++++++--------------- web/root/style/editor.scss | 2 +- 12 files changed, 209 insertions(+), 114 deletions(-) delete mode 100644 web/root/src/focus.ts create mode 100644 web/root/src/highlight.ts diff --git a/automata/src/automatan/fa.rs b/automata/src/automatan/fa.rs index ff7399b..2dbefb0 100644 --- a/automata/src/automatan/fa.rs +++ b/automata/src/automatan/fa.rs @@ -176,8 +176,8 @@ impl<'a, 'b> FaCompiler<'a, 'b> { self.ctx.emit_error(format!("unknown item {name:?}, expected states | alphabet | final states | initial state"), dest_s); } - TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { - self.compile_transition_function(args, list) + TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => { + self.compile_transition_function(args, func, list) } TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { self.ctx.emit_error( @@ -313,6 +313,7 @@ impl<'a, 'b> FaCompiler<'a, 'b> { fn compile_transition_function( &mut self, args: Spanned>, + function: Span, list: Spanned>, ) { let list = list.set_weak(); @@ -368,8 +369,7 @@ impl<'a, 'b> FaCompiler<'a, 'b> { } if let Some(previous) = entry.replace(TransitionTo { state: State(next_state.0), - - function: args.1, + function, transition: item.1, }) { self.ctx diff --git a/automata/src/automatan/pda.rs b/automata/src/automatan/pda.rs index e3c2c02..5c2ec2a 100644 --- a/automata/src/automatan/pda.rs +++ b/automata/src/automatan/pda.rs @@ -264,8 +264,8 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { self.ctx.emit_error(format!("unknown item {name:?}, expected states | stack symbols | alphabet | accept by | final states | initial state | initial stack"), dest_s); } - TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { - self.compile_transition_function(args, list) + TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => { + self.compile_transition_function(args, func, list) } TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { self.ctx.emit_error( @@ -476,6 +476,7 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { fn compile_transition_function( &mut self, args: Spanned>, + function: Span, list: Spanned>, ) { let list = list.set_weak(); @@ -559,8 +560,8 @@ impl<'a, 'b> PdaCompiler<'a, 'b> { if !entry.insert(TransitionTo { state: State(next_state.0), stack, - - function: args.1, + + function, transition: item.1, }) { self.ctx.emit_warning("duplicate transition", item.1); diff --git a/automata/src/automatan/tm.rs b/automata/src/automatan/tm.rs index adc57f4..626b18c 100644 --- a/automata/src/automatan/tm.rs +++ b/automata/src/automatan/tm.rs @@ -193,8 +193,8 @@ impl<'a, 'b> TmCompiler<'a, 'b> { self.ctx.emit_error(format!("unknown item {name:?}, expected states | symbols | final states | initial state | blank symbol"), dest_s); } - TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { - self.compile_transition_function(args, list) + TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => { + self.compile_transition_function(args, func, list) } TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { self.ctx.emit_error( @@ -349,6 +349,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> { fn compile_transition_function( &mut self, args: Spanned>, + function: Span, list: Spanned>, ) { let list = list.set_weak(); @@ -398,7 +399,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> { symbol: Symbol(to_tape.0), direction: direction.0, - function: args.1, + function, transition: item.1, }) { self.ctx.emit_warning("duplicate transition", item.1); diff --git a/web/root/src/bus.ts b/web/root/src/bus.ts index 1c56cd0..eb284fb 100644 --- a/web/root/src/bus.ts +++ b/web/root/src/bus.ts @@ -1,10 +1,11 @@ // deno-lint-ignore-file -import type { Machine } from "./automata.ts"; +import type { Machine, Span } from "./automata.ts"; import type { Example } from "./examples.ts"; import type { Sim, SimStepResult } from "./simulation.ts"; import type wasm from "./wasm.ts"; import type { Text } from "npm:@codemirror/state"; +import type { Highlight } from "./highlight.ts"; type Unsubscribe = () => void; @@ -69,14 +70,14 @@ type AppEvents = { "editor/change": {text: string, doc: Text}; "compiled": {log: wasm.CompileLog[], ansi_log: string, machine: string|undefined}; - "automata/sim/update": { simulation: Sim|null }; + "automata/sim/update": Sim|null; "automata/sim/before_step": { simulation: Sim }; "automata/sim/after_step": { simulation: Sim, result: SimStepResult }; - "automata/update": { automaton: Machine }; + "automata/update": Machine; - "example/selected": {example: Example}; + "example/selected": Example; - "controls/editor/set_text": {text: string}; + "controls/editor/set_text": string; "controls/vis/physics": {enabled: boolean}; "controls/vis/reset_network": void; @@ -85,6 +86,12 @@ type AppEvents = { "controls/sim/reload": void; "controls/sim/clear": void; + "highlight/one/add": Highlight; + "highlight/one/remove": Highlight; + "highlight/all/remove": void; + + "highlight/update": void; + "theme/update": void; }; diff --git a/web/root/src/controls.ts b/web/root/src/controls.ts index 16980f3..8d553f0 100644 --- a/web/root/src/controls.ts +++ b/web/root/src/controls.ts @@ -89,7 +89,7 @@ class Controls { if (Controls.running) Controls.setRunning(false); }); - bus.on("automata/sim/update", ({ simulation }) => { + bus.on("automata/sim/update", simulation => { Controls.simulation_active = !!simulation; if (!simulation) Controls.stop(); }); diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts index c3340e5..dbadd4c 100644 --- a/web/root/src/editor.ts +++ b/web/root/src/editor.ts @@ -25,6 +25,7 @@ import wasm from "./wasm.ts"; import { Share } from "./share.ts"; import { examples } from "./examples.ts"; import { bus } from "./bus.ts"; +import { current, Highlight, HighlightKind } from "./highlight.ts"; function tokenize(text: string): wasm.Tok[] { try { @@ -47,25 +48,16 @@ function compile( } -export type HighlightKind = "focus" | "success" | "warning" | "error"; - -export type HighlightSpan = { - from: number; - to: number; - kind: HighlightKind; -}; - function decoForKind(kind: HighlightKind) { // Use a class per kind so each gets a distinct color via CSS return Decoration.mark({ class: `cm-highlight cm-highlight-${kind}` }); } -export function applyHighlights(view: EditorView, spans: HighlightSpan[]) { - view.dispatch({ effects: setHighlights.of(spans) }); -} - - - +bus.on("highlight/update", _ => { + const arr = current.values().toArray().sort((a, b) => a.span[0]-b.span[0]); + editor.dispatch({ effects: setHighlights.of(arr) }); +}); +export const setHighlights = StateEffect.define(); export const highlightsField = StateField.define({ create() { return Decoration.none; @@ -73,7 +65,7 @@ export const highlightsField = StateField.define({ update(highlights, tr) { // Keep highlights aligned with document edits - // highlights = highlights.map(tr.changes); + highlights = highlights.map(tr.changes); for (const e of tr.effects) { if (e.is(setHighlights)) { @@ -81,8 +73,9 @@ export const highlightsField = StateField.define({ const builder = new RangeSetBuilder(); for (const s of spans) { - const from = Math.max(0, Math.min(s.from, tr.state.doc.length)); - const to = Math.max(0, Math.min(s.to, tr.state.doc.length)); + + const from = Math.max(0, Math.min(s.span[0], tr.state.doc.length)); + const to = Math.max(0, Math.min(s.span[1], tr.state.doc.length)); if (to > from) builder.add(from, to, decoForKind(s.kind)); } highlights = builder.finish(); @@ -95,7 +88,6 @@ export const highlightsField = StateField.define({ provide: (f) => EditorView.decorations.from(f), }); -export const setHighlights = StateEffect.define(); const eventBusConnection = StateField.define({ create(state) { @@ -264,23 +256,15 @@ const editor = new EditorView({ bus.on( "begin", - (_) => bus.emit("controls/editor/set_text", { text: defaultText() }), + (_) => bus.emit("controls/editor/set_text", defaultText()), ); -bus.on("controls/editor/set_text", ({ text }) => { +bus.on("controls/editor/set_text", text => { editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: text }, }); }); -bus.on("example/selected", ({ example }) => { - bus.emit("controls/editor/set_text", { text: example.machine }); +bus.on("example/selected", example => { + bus.emit("controls/editor/set_text", example.machine); }); - - -applyHighlights(editor, [ - { from: 0, to: 10, kind: "focus" }, - { from: 10, to: 20, kind: "success" }, - { from: 20, to: 30, kind: "warning" }, - { from: 30, to: 40, kind: "error" }, -]) \ No newline at end of file diff --git a/web/root/src/examples.ts b/web/root/src/examples.ts index c3cd7e6..593e253 100644 --- a/web/root/src/examples.ts +++ b/web/root/src/examples.ts @@ -245,5 +245,5 @@ function buildExamplesDropdown( const selectEl = document.getElementById("exampleSelect") as HTMLSelectElement; buildExamplesDropdown(selectEl, examples, (example) => { - bus.emit("example/selected", {example}); + bus.emit("example/selected", example); }); diff --git a/web/root/src/focus.ts b/web/root/src/focus.ts deleted file mode 100644 index e69de29..0000000 diff --git a/web/root/src/highlight.ts b/web/root/src/highlight.ts new file mode 100644 index 0000000..5c40df2 --- /dev/null +++ b/web/root/src/highlight.ts @@ -0,0 +1,85 @@ +import type { Span } from "./automata.ts"; +import { bus } from "./bus.ts"; +import { automaton } from "./simulation.ts"; + + +export type HighlightKind = "focus" | "error" | "warning" | "success"; + + + +export type Highlight = { + span: Span, + kind: HighlightKind, +} + +type HighlightEntry = { + span: Span, + kind: HighlightKind, + count: number; +} + +export const current: Map = new Map(); + + +function asKey(highlight: Highlight): string { + return `${highlight.span[0]}:${highlight.span[1]}:${highlight.kind}` +} + +export function highlight_from_node_id(node_id: string) { + const state = automaton.states.get(node_id); + if (state) { + bus.emit("highlight/one/add", { kind: "success", span: state.definition }) + } +} + +export function dehighlight_from_node_id(node_id: string) { + const state = automaton.states.get(node_id); + if (state) { + bus.emit("highlight/one/remove", { kind: "success", span: state.definition }) + } +} + +export function highlight_from_edge_id(node_id: string) { + for (const edge_value of automaton.edges.get(node_id)!) { + bus.emit("highlight/one/add", { kind: "focus", span: edge_value.function }) + bus.emit("highlight/one/add", { kind: "warning", span: edge_value.transition }) + } +} + +export function dehighlight_from_edge_id(node_id: string) { + for (const edge_value of automaton.edges.get(node_id)!) { + bus.emit("highlight/one/remove", { kind: "focus", span: edge_value.function }) + bus.emit("highlight/one/remove", { kind: "warning", span: edge_value.transition }) + } +} + +bus.on("automata/update", _ => { + bus.emit("highlight/all/remove", undefined); +}) + +bus.on("highlight/one/add", (highlight) => { + const key = asKey(highlight); + if (current.has(key)) { + current.get(key)!.count += 1; + } else { + current.set(key, { count: 1, ...highlight }); + bus.emit("highlight/update", undefined); + } +}); +bus.on("highlight/one/remove", (highlight) => { + const key = asKey(highlight); + if (current.has(key)) { + const value = current.get(key)! + value.count -= 1; + if (value.count === 0) { + current.delete(key); + bus.emit("highlight/update", undefined); + } + } +}); +bus.on("highlight/all/remove", (_) => { + if (current.size !== 0) { + current.clear(); + bus.emit("highlight/update", undefined); + } +}); \ No newline at end of file diff --git a/web/root/src/simulation.ts b/web/root/src/simulation.ts index 7e78a02..bafd19b 100644 --- a/web/root/src/simulation.ts +++ b/web/root/src/simulation.ts @@ -11,8 +11,9 @@ import {parse_machine_from_json} from "./automata.ts"; export type SimStepResult = "pending" | "accept" | "reject"; export type Sim = FaSim | PdaSim | TmSim; -let simulation: Sim | null = null; -let automaton: Machine = { + +export let simulation: Sim | null = null; +export let automaton: Machine = { type: "fa", alphabet: new Map(), final_states: new Map(), @@ -28,7 +29,7 @@ bus.on("compiled", ({ machine }) => { try { bus.emit("controls/sim/clear", undefined); automaton = parse_machine_from_json(machine); - bus.emit("automata/update", { automaton }); + bus.emit("automata/update", automaton); } catch (e) { console.log(e); } @@ -36,7 +37,7 @@ bus.on("compiled", ({ machine }) => { }); bus.on("controls/sim/clear", (_) => { simulation = null; - bus.emit("automata/sim/update", { simulation: null }); + bus.emit("automata/sim/update", null); }); bus.on("controls/sim/step", (_) => { if (simulation) { @@ -48,7 +49,7 @@ bus.on("controls/sim/step", (_) => { } }); const machineInput = document.getElementById("machineInput") as HTMLInputElement; -machineInput.addEventListener("input", () => bus.emit("automata/sim/update", {simulation: null})); +machineInput.addEventListener("input", () => bus.emit("controls/sim/clear", undefined)); machineInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { bus.emit("controls/sim/reload", undefined) @@ -67,10 +68,10 @@ bus.on("controls/sim/reload", (_) => { simulation = new TmSim(automaton as Tm, input); break; } - bus.emit("automata/sim/update", { simulation }); + bus.emit("automata/sim/update", simulation); }); const simulationStatus = document.getElementById("simulationStatus") as HTMLInputElement; -bus.on("automata/sim/update", ({simulation}) => { +bus.on("automata/sim/update", simulation => { if (!simulation){ simulationStatus.innerText = "N/A" simulationStatus.style.color = "var(--fg-2)"; diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index 0c4bae0..48f035e 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -3,46 +3,42 @@ import * as vis from "npm:vis-network/standalone"; import { bus } from "./bus.ts"; -import type { Sim } from "./simulation.ts"; -import type { Machine } from "./automata.ts"; +import { automaton, simulation } from "./simulation.ts"; +import { dehighlight_from_edge_id, dehighlight_from_node_id, highlight_from_edge_id, highlight_from_node_id } from "./highlight.ts"; -bus.on("controls/vis/physics", ({enabled}) => { +bus.on("controls/vis/physics", ({ enabled }) => { network.setOptions({ physics: { enabled } }); - network.setOptions({edges: {smooth: enabled}}); + network.setOptions({ edges: { smooth: enabled } }); }); bus.on("controls/vis/reset_network", _ => { - try { - nodes.forEach((n) => { - n.physics = true; - n.x = undefined; - n.y = undefined; - }); - network.setData({ nodes, edges }); - } catch { - // Last resort - network.setData({ nodes, edges }); - } + try { + nodes.forEach((n) => { + n.physics = true; + n.x = undefined; + n.y = undefined; + }); + network.setData({ nodes, edges }); + } catch { + // Last resort + network.setData({ nodes, edges }); + } }); bus.on("automata/sim/after_step", _ => { network.redraw(); }); -let simulation: Sim | null = null; -bus.on("automata/sim/update", ({simulation: sim}) => { - simulation = sim; +bus.on("automata/sim/update", _ => { network.redraw(); }); -let automaton: Machine +bus.on("automata/update", automaton => { -bus.on("automata/update", ({automaton: auto}) => { - automaton = auto; // Populate nodes for (const state of automaton.states.keys()) { - - const size = measureTextWidth(state, getGraphTheme().node_font)/2+10 + + const size = measureTextWidth(state, getGraphTheme().node_font) / 2 + 10 if (nodes.get(state)) { nodes.update({ id: state, @@ -62,20 +58,20 @@ bus.on("automata/update", ({automaton: auto}) => { for (const [edge_id, transitions] of automaton.edges) { const to_from = edge_id.split("#"); const vadjust = -getGraphTheme().edge_font_size * - Math.floor(transitions.length / 2); + Math.floor(transitions.length / 2); const font = { vadjust, - bold: { - vadjust - } - }; + bold: { + vadjust + } + }; if (edges.get(edge_id)) { edges.update({ id: edge_id, font, from: to_from[0], to: to_from[1], - label: transitions.map(i => i.repr).join(automaton.type=="fa"?",":"\n"), + label: transitions.map(i => i.repr).join(automaton.type == "fa" ? "," : "\n"), }); } else { edges.add({ @@ -83,7 +79,7 @@ bus.on("automata/update", ({automaton: auto}) => { font, from: to_from[0], to: to_from[1], - label: transitions.map(i => i.repr).join(automaton.type=="fa"?",":"\n"), + label: transitions.map(i => i.repr).join(automaton.type == "fa" ? "," : "\n"), }); } } @@ -238,22 +234,6 @@ function measureTextWidth(text: string, font: string): number { return ctx.measureText(text).width; } -function chosen_edge( - _: vis.ChosenNodeValues, - id: vis.IdType, - selected: boolean, - hovered: boolean, -) { -} - -function chosen_node( - _: vis.ChosenNodeValues, - id: vis.IdType, - selected: boolean, - hovered: boolean, -) { -} - const network: vis.Network = createGraph(); function createGraph(): vis.Network { @@ -286,16 +266,9 @@ function createGraph(): vis.Network { shape: "custom", size: 18, // @ts-expect-error bad library - chosen: { - node: chosen_node, - }, ctxRenderer: renderNode, }, edges: { - chosen: { - // @ts-expect-error bad library - edge: chosen_edge, - }, arrowStrikethrough: false, arrows: "to", }, @@ -303,7 +276,7 @@ function createGraph(): vis.Network { ); vis.DataSet; - network.on("doubleClick", (params: {nodes: string[]}) => { + network.on("doubleClick", (params: { nodes: string[] }) => { for (const node_id of params.nodes) { const node: vis.Node = nodes.get(node_id)!; node.physics = !node.physics; @@ -311,6 +284,49 @@ function createGraph(): vis.Network { } }); + network.on("hoverEdge", ({ edge }: { edge: string }) => { + highlight_from_edge_id(edge) + }); + + network.on('blurEdge', ({edge}: {edge: string}) => { + dehighlight_from_edge_id(edge) + }); + + network.on("hoverNode", ({ node }: { node: string }) => { + highlight_from_node_id(node); + }); + + network.on('blurNode', ({ node }: { node: string }) => { + dehighlight_from_node_id(node) + }); + + + network.on("selectEdge", item => { + const id = network.getEdgeAt(item.pointer.DOM); + if(id)highlight_from_edge_id(id as string); + }); + + network.on('deselectEdge', item => { + console.log(item); + for (const edge of item.previousSelection.edges){ + console.log(edge); + dehighlight_from_edge_id(edge.id) + } + }); + + network.on("selectNode", item => { + const id = network.getNodeAt(item.pointer.DOM); + if(id)highlight_from_node_id(id as string); + }); + + network.on('deselectNode', item => { + console.log(item); + for (const node of item.previousSelection.nodes){ + console.log(node); + dehighlight_from_node_id(node.id) + } + }); + return network; } @@ -323,7 +339,7 @@ function renderNode({ state: { selected, hover }, style, label, -}: {ctx: CanvasRenderingContext2D, id: string, x: number, y: number, state: {selected: boolean, hover: boolean}, style: vis.NodeOptions, label: string}) { +}: { ctx: CanvasRenderingContext2D, id: string, x: number, y: number, state: { selected: boolean, hover: boolean }, style: vis.NodeOptions, label: string }) { return { drawNode() { const t = getGraphTheme(); @@ -333,7 +349,7 @@ function renderNode({ const isFinal = automaton.final_states ? automaton.final_states.has(id) : false; - const isActive = simulation?simulation.current_states.has(id):false; + const isActive = simulation ? simulation.current_states.has(id) : false; const fill = selected ? t.bg_2 : hover ? t.bg_1 : t.bg_0; const stroke = isActive ? t.current_node_border : t.node_border; @@ -345,7 +361,7 @@ function renderNode({ ctx.save(); - ctx.font = hover||selected?t.node_font_bold:t.node_font; + ctx.font = hover || selected ? t.node_font_bold : t.node_font; ctx.textAlign = "center"; ctx.textBaseline = "middle"; @@ -398,7 +414,7 @@ function renderNode({ ctx.textBaseline = "top"; for (let i = 0; i < paths.length; i++) { - ctx.fillStyle = paths[i].accepted?t.current_node_border:t.fg_0; + ctx.fillStyle = paths[i].accepted ? t.current_node_border : t.fg_0; ctx.fillText(paths[i].toString(), x, by + padY + i * lineH); } } diff --git a/web/root/style/editor.scss b/web/root/style/editor.scss index 834cafa..2a0a13c 100644 --- a/web/root/style/editor.scss +++ b/web/root/style/editor.scss @@ -136,7 +136,7 @@ .cm-highlight { border-radius: 4px; - padding: 0 1px; + // padding: 0 1px; } .cm-highlight-warning { background: color-mix(in srgb, var(--warning) 40%, var(--bg-0)); } From 2a777f31988cebb0f2b4d66868150f417a6fe191 Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:34:06 -0500 Subject: [PATCH 6/6] tm simulation, simulation code cleanup --- automata/src/automatan/tm.rs | 14 +- web/root/src/automata.ts | 48 +++---- web/root/src/examples.ts | 24 ++++ web/root/src/simulation.ts | 244 ++------------------------------- web/root/src/simulation/fa.ts | 117 ++++++++++++++++ web/root/src/simulation/pda.ts | 146 ++++++++++++++++++++ web/root/src/simulation/tm.ts | 129 +++++++++++++++++ web/root/src/visualizer.ts | 4 +- 8 files changed, 460 insertions(+), 266 deletions(-) create mode 100644 web/root/src/simulation/fa.ts create mode 100644 web/root/src/simulation/pda.ts create mode 100644 web/root/src/simulation/tm.ts diff --git a/automata/src/automatan/tm.rs b/automata/src/automatan/tm.rs index 626b18c..a8838dc 100644 --- a/automata/src/automatan/tm.rs +++ b/automata/src/automatan/tm.rs @@ -3,12 +3,11 @@ use std::collections::HashSet; use super::*; use crate::{ - delta_lower, dual_struct_serde, - loader::{ + delta_lower, dual_struct_serde, gamma_upper, loader::{ BLANK_SYMBOL, Context, INITIAL_STATE, Spanned, ast::{self, Symbol as Sym}, log::LogSink, - }, + } }; dual_struct_serde! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] @@ -23,8 +22,11 @@ dual_struct_serde! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Direction { + #[serde(rename = "<")] Left, + #[serde(rename = ">")] Right, + #[serde(rename = "_")] None, } @@ -185,7 +187,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> { use ast::TopLevel as TL; match element { TL::Item(S("Q", _), list) => self.compile_states(list, span), - TL::Item(S(delta_lower!(pat), _), list) => self.compile_symbols(list, span), + TL::Item(S(gamma_upper!(pat), _), list) => self.compile_symbols(list, span), TL::Item(S("F", _), list) => self.compile_final_states(list, span), TL::Item(S(INITIAL_STATE, _), item) => self.compile_initial_state(item, span), TL::Item(S(BLANK_SYMBOL, _), item) => self.compile_blank_symbol(item, span), @@ -335,11 +337,11 @@ impl<'a, 'b> TmCompiler<'a, 'b> { .emit_error("blank symbol already set", top_level) .emit_help("previously defined here", previous); } - if self.states.contains_key(&State(ident)) { + if self.symbols.contains_key(&Symbol(ident)) { self.blank_symbol = Some((Symbol(ident), top_level)) } else { self.ctx - .emit_error("blank symbol not defined as a state", src_d); + .emit_error("blank symbol not defined as a symbol", src_d); } } _ => _ = self.ctx.emit_error("expected ident", src_d), diff --git a/web/root/src/automata.ts b/web/root/src/automata.ts index 6577551..8077648 100644 --- a/web/root/src/automata.ts +++ b/web/root/src/automata.ts @@ -100,11 +100,11 @@ export type State = string; export type Symbol = string; export type Letter = string; -export type Span = [number, number]; +export type Span = readonly [number, number]; -export type StateInfo = { definition: Span }; -export type LetterInfo = { definition: Span }; -export type SymbolInfo = { definition: Span }; +export type StateInfo = { readonly definition: Span }; +export type LetterInfo = { readonly definition: Span }; +export type SymbolInfo = { readonly definition: Span }; export type FaTransFrom = { state: State; @@ -112,16 +112,16 @@ export type FaTransFrom = { }; export type FaTransTo = { - state: State; + readonly state: State; - transition: Span; - function: Span; + readonly transition: Span; + readonly function: Span; }; export type Edge = { - repr: string; - function: Span; - transition: Span; + readonly repr: string; + readonly function: Span; + readonly transition: Span; }; export type Fa = { @@ -139,17 +139,17 @@ export type Fa = { }; export type PdaTransFrom = { - state: State; - letter: Letter | null; - symbol: Symbol; + readonly state: State; + readonly letter: Letter | null; + readonly symbol: Symbol; }; export type PdaTransTo = { - state: State; - stack: Symbol[]; + readonly state: State; + readonly stack: readonly Symbol[]; - transition: Span; - function: Span; + readonly transition: Span; + readonly function: Span; }; export type Pda = { @@ -172,17 +172,17 @@ export type Pda = { }; export type TmTransFrom = { - state: State; - symbol: Symbol; + readonly state: State; + readonly symbol: Symbol; }; export type TmTransTo = { - state: State; - symbol: Symbol; - direction: "L" | "R" | "N"; + readonly state: State; + readonly symbol: Symbol; + readonly direction: "<" | ">" | "_"; - transition: Span; - function: Span; + readonly transition: Span; + readonly function: Span; }; export type Tm = { diff --git a/web/root/src/examples.ts b/web/root/src/examples.ts index 593e253..bf2304b 100644 --- a/web/root/src/examples.ts +++ b/web/root/src/examples.ts @@ -163,6 +163,30 @@ d(q0, epsilon, B) = { (q1, B) } d(q1, a, A) = { (q1, epsilon) } d(q1, b, B) = { (q1, epsilon) }`, ), + + new Example("TM", "a^nb^n", + `// accepts all strings on {a,b}+ of the form anbn + +type = TM +Q = { q0, q1, q2, q3, q4 } // set of internal states +F = { q4 } // set of final states +T = { a, b, X, Y, B } // tape alphabet +B = B // the blank symbol (tape initializer symbol) +q0 = q0 // initial state + +d(q0,a)=(q1,x,R) +d(q1,a)=(q1,a,R) +d(q1,Y)=(q1,y,R) +d(q1,b)=(q2,y,L) + +d(q2,Y)=(q2,y,L) +d(q2,a)=(q2,a,L) +d(q2,X)=(q0,x,R) + +d(q0,Y)=(q3,y,R) +d(q3,Y)=(q3,y,R) +d(q3,B)=(q4,B,R) +`) ]; const CATEGORY_ORDER: Category[] = [ diff --git a/web/root/src/simulation.ts b/web/root/src/simulation.ts index bafd19b..84b9fa1 100644 --- a/web/root/src/simulation.ts +++ b/web/root/src/simulation.ts @@ -1,14 +1,21 @@ import { bus } from "./bus.ts"; import type { - Fa, Machine, + Fa, Pda, - State, - Symbol, Tm, } from "./automata.ts"; import {parse_machine_from_json} from "./automata.ts"; +import { FaSim } from "./simulation/fa.ts"; +export { FaSim } from "./simulation/fa.ts"; + +import { PdaSim } from "./simulation/pda.ts"; +export { PdaSim } from "./simulation/pda.ts"; + +import { TmSim } from "./simulation/tm.ts"; +export { TmSim } from "./simulation/tm.ts"; + export type SimStepResult = "pending" | "accept" | "reject"; export type Sim = FaSim | PdaSim | TmSim; @@ -93,234 +100,3 @@ bus.on("automata/sim/after_step", ({result}) => { } }); -export class FaState { - readonly state: State; - - readonly position: number; - readonly input: string; - readonly accepted: boolean = false; - private repr!: string; - - constructor(state: State, position: number, input: string) { - this.state = state; - this.position = position; - this.input = input; - } - - toString(): string { - if (!this.repr) { - this.repr = this.state + " >" + this.input.substring(this.position); - } - return this.repr; - } -} - -export class FaSim { - machine: Fa; - paths: FaState[]; - input: string; - - current_states: Map = new Map(); - accepted: FaState[] = []; - - constructor(machine: Fa, input: string) { - this.machine = machine; - this.paths = [new FaState(machine.initial_state, 0, input)]; - this.current_states.set(machine.initial_state, [this.paths[0]]); - this.input = input; - } - - step(): SimStepResult { - if (this.paths.length == 0) return "reject"; - if (this.accepted.length != 0) return "accept"; - - const paths: FaState[] = []; - this.current_states.clear(); - - const push = (state: FaState) => { - paths.push(state); - if (!this.current_states.has(state.state)) { - this.current_states.set(state.state, []); - } - this.current_states.get(state.state)?.push(state); - - if ( - state.position == this.input.length && - this.machine.final_states.has(state.state) - ) { - // @ts-expect-error sillllyyyy - state.accepted = true; - this.accepted.push(state); - } - }; - - for (const path of this.paths) { - const letter_map = this.machine.transitions_components.get(path.state)!; - if (!letter_map) continue; - - for (const to of letter_map.get(null) ?? []) { - push(new FaState(to.state, path.position, this.input)); - } - - if (path.position >= this.input.length) continue; - - const char = this.input.charAt(path.position); - - for (const to of letter_map.get(char) ?? []) { - push(new FaState(to.state, path.position + 1, this.input)); - } - } - this.paths = paths; - - if (this.paths.length == 0) return "reject"; - if (this.accepted.length != 0) return "accept"; - return "pending"; - } -} - -export class PdaState { - readonly state: State; - readonly stack: Symbol[]; - - readonly position: number; - readonly input: string; - readonly accepted: boolean = false; - private repr!: string; - - constructor(state: State, stack: Symbol[], position: number, input: string) { - this.state = state; - this.stack = stack; - this.position = position; - this.input = input; - } - - toString(): string { - if (!this.repr) { - this.repr = this.state + " [" + this.stack + "]" + " >" + - this.input.substring(this.position); - } - return this.repr; - } -} - -export class PdaSim { - machine: Pda; - paths: PdaState[]; - input: string; - - current_states: Map = new Map(); - accepted: PdaState[] = []; - - constructor(machine: Pda, input: string) { - this.machine = machine; - this.paths = [ - new PdaState(machine.initial_state, [machine.initial_stack], 0, input), - ]; - this.current_states.set(machine.initial_state, [this.paths[0]]); - this.input = input; - } - - step(): SimStepResult { - if (this.paths.length == 0) return "reject"; - if (this.accepted.length != 0) return "accept"; - - const paths: PdaState[] = []; - this.current_states.clear(); - - const push = (state: PdaState) => { - paths.push(state); - if (!this.current_states.has(state.state)) { - this.current_states.set(state.state, []); - } - this.current_states.get(state.state)?.push(state); - - if ( - state.position == this.input.length && this.machine.final_states && - this.machine.final_states.has(state.state) || - state.position == this.input.length && !this.machine.final_states && - state.stack.length == 1 && - state.stack[0] == this.machine.initial_stack - ) { - // @ts-expect-error sillllyyyy - state.accepted = true; - this.accepted.push(state); - } - }; - - for (const path of this.paths) { - const stack = path.stack.pop()!; - const letter_map = this.machine.transitions_components.get(path.state) - ?.get(stack); - if (!letter_map) continue; - - for (const to of letter_map.get(null) ?? []) { - push( - new PdaState( - to.state, - path.stack.concat(to.stack), - path.position, - this.input, - ), - ); - } - - if (path.position >= this.input.length) continue; - - const char = this.input.charAt(path.position); - - for (const to of letter_map.get(char) ?? []) { - push( - new PdaState( - to.state, - path.stack.concat(to.stack), - path.position + 1, - this.input, - ), - ); - } - } - this.paths = paths; - - if (this.paths.length == 0) return "reject"; - if (this.accepted.length != 0) return "accept"; - return "pending"; - } -} - -export class TmState { - readonly state: State; - readonly tape: Symbol[]; - - readonly position: number; - readonly input: string; - readonly accepted: boolean = false; - private repr!: string; - - constructor(state: State, tape: Symbol[], position: number, input: string) { - this.state = state; - this.tape = tape; - this.position = position; - this.input = input; - } - - toString(): string { - if (!this.repr) this.repr = this.state + " " + this.position; - return this.repr; - } -} - -export class TmSim { - machine: Tm; - input: string; - current_states: Map = new Map(); - accepted: TmState[] = []; - - constructor(machine: Tm, input: string) { - this.machine = machine; - this.input = input; - } - - step(): SimStepResult { - return "pending"; - } -} diff --git a/web/root/src/simulation/fa.ts b/web/root/src/simulation/fa.ts new file mode 100644 index 0000000..476fe41 --- /dev/null +++ b/web/root/src/simulation/fa.ts @@ -0,0 +1,117 @@ +import type { + Fa, + FaTransTo, + State, +} from "../automata.ts"; +import { SimStepResult } from "../simulation.ts"; + +export type FaState = { + readonly state: State; + readonly position: number; + + readonly accepted: boolean; + readonly repr: string; + + readonly path: readonly FaTransTo[]; +}; + +type Initializer = { -readonly [P in keyof T]?: T[P] | undefined }; + +export class FaSim { + readonly machine: Fa; + readonly input: string; + + paths: FaState[] = []; + current_states: Map = new Map(); + accepted: FaState[] = []; + rejected: FaState[] = []; + + constructor(machine: Fa, input: string) { + this.machine = machine; + this.input = input; + this.initial(); + } + + private accept(state: Initializer): boolean { + const pos = state.position ?? 0; + const st = state.state!; + return pos === this.input.length && this.machine.final_states.has(st); + } + + private init_state(state: Initializer) { + state.position ??= 0; + + state.accepted = this.accept(state); + state.repr = state.state + " >" + this.input.substring(state.position); + + const frozen = state as FaState; + + if (frozen.accepted) this.accepted.push(frozen); + this.paths.push(frozen); + + if (!this.current_states.has(frozen.state)) { + this.current_states.set(frozen.state, []); + } + this.current_states.get(frozen.state)!.push(frozen); + } + + private initial() { + const state: Initializer = { + state: this.machine.initial_state, + position: 0, + path: [], + }; + + this.init_state(state); + } + + private transition(from: FaState, to: FaTransTo, consume: boolean) { + const state: Initializer = { + state: to.state, + position: from.position + (consume ? 1 : 0), + path: from.path.concat([to]), + }; + + this.init_state(state); + } + + step(): SimStepResult { + if (this.accepted.length !== 0) return "accept"; + if (this.paths.length === 0) return "reject"; + + const paths = this.paths; + this.paths = []; + this.current_states.clear(); + + for (const from of paths) { + const letterMap = this.machine.transitions_components.get(from.state); + + if (!letterMap) { + this.rejected.push(from); + continue; + } + + // epsilon transitions + const eps = letterMap.get(null) ?? []; + for (const to of eps) this.transition(from, to, false); + + // consuming transitions + if (from.position >= this.input.length) { + if (eps.length === 0) this.rejected.push(from); + continue; + } + + const ch = this.input.charAt(from.position); + const trs = letterMap.get(ch) ?? []; + for (const to of trs) this.transition(from, to, true); + + if (eps.length === 0 && trs.length === 0) { + this.rejected.push(from); + } + } + + if (this.accepted.length !== 0) return "accept"; + if (this.paths.length === 0) return "reject"; + return "pending"; + } +} diff --git a/web/root/src/simulation/pda.ts b/web/root/src/simulation/pda.ts new file mode 100644 index 0000000..80ec4be --- /dev/null +++ b/web/root/src/simulation/pda.ts @@ -0,0 +1,146 @@ +import type { + Pda, + PdaTransTo, + State, + Symbol +} from "../automata.ts"; +import { SimStepResult } from "../simulation.ts"; + +export type PdaState = { + readonly state: State; + readonly stack: Symbol[]; + readonly position: number; + + readonly accepted: boolean; + readonly repr: string; + + readonly path: readonly PdaTransTo[]; +}; + +type Initializer = { -readonly [P in keyof T]?: T[P] | undefined }; + +export class PdaSim { + readonly machine: Pda; + readonly input: string; + + paths: PdaState[] = []; + current_states: Map = new Map(); + accepted: PdaState[] = []; + rejected: PdaState[] = []; + + constructor(machine: Pda, input: string) { + this.machine = machine; + this.input = input; + this.initial(); + } + + private accept(state: Initializer): boolean { + const pos = state.position ?? 0; + const st = state.state!; + const stack = state.stack ?? []; + + //accept by final state + if (pos === this.input.length && this.machine.final_states && this.machine.final_states.has(st)) { + return true; + } + //accept by empty stack + if (pos === this.input.length && !this.machine.final_states && stack.length === 1 && stack[0] === this.machine.initial_stack) { + return true; + } + + return false; + } + + private init_state(state: Initializer) { + state.stack ??= [this.machine.initial_stack]; + state.position ??= 0; + + state.accepted = this.accept(state); + state.repr = state.state + " [" + state.stack.join(",") + "] >" + this.input.substring(state.position); + + const frozen = state as PdaState; + + if (frozen.accepted) this.accepted.push(frozen); + this.paths.push(frozen); + + if (!this.current_states.has(frozen.state)) { + this.current_states.set(frozen.state, []); + } + this.current_states.get(frozen.state)!.push(frozen); + } + + private initial() { + const state: Initializer = { + state: this.machine.initial_state, + stack: [this.machine.initial_stack], + position: 0, + path: [], + }; + + this.init_state(state); + } + + private transition(from: PdaState, to: PdaTransTo, consume: boolean) { + const stackCopy = from.stack.slice(0, from.stack.length - 1); // pop off top + const nextStack = stackCopy.concat(to.stack); + if (nextStack.length == 0) { + this.rejected.push(from) + return; + } + + const state: Initializer = { + state: to.state, + stack: nextStack, + position: from.position + (consume ? 1 : 0), + path: from.path.concat([to]), + }; + + this.init_state(state); + } + + step(): SimStepResult { + if (this.accepted.length !== 0) return "accept"; + if (this.paths.length === 0) return "reject"; + + const paths = this.paths; + this.paths = []; + this.current_states.clear(); + + for (const from of paths) { + const top = from.stack[from.stack.length - 1]; + + const letterMap = this.machine.transitions_components.get(from.state)?.get(top); + if (!letterMap) { + this.rejected.push(from); + continue; + } + + // epsilon transitions + const epsilon_transitions = letterMap.get(null) ?? []; + for (const to of epsilon_transitions) { + this.transition(from, to, false); + } + + if (from.position >= this.input.length) { + if (epsilon_transitions.length == 0){ + this.rejected.push(from); + } + continue; + } + // consuming transitions + const ch = this.input.charAt(from.position); + + const transitions = letterMap.get(ch) ?? []; + for (const to of transitions) { + this.transition(from, to, true); + } + if (epsilon_transitions.length == 0 && transitions.length == 0){ + this.rejected.push(from); + } + } + + if (this.accepted.length !== 0) return "accept"; + if (this.paths.length === 0) return "reject"; + return "pending"; + } +} \ No newline at end of file diff --git a/web/root/src/simulation/tm.ts b/web/root/src/simulation/tm.ts new file mode 100644 index 0000000..74052d9 --- /dev/null +++ b/web/root/src/simulation/tm.ts @@ -0,0 +1,129 @@ +import type { + State, + Symbol, + Tm, + TmTransTo +} from "../automata.ts"; +import { SimStepResult } from "../simulation.ts"; + + +export type TmState = { + readonly state: State; + readonly tape: Symbol[]; + readonly head: number; + + readonly accepted: boolean; + readonly repr: string; + + readonly path: readonly TmTransTo[]; +} + + +type Initializer = { -readonly [P in keyof T]?: T[P] | undefined }; + +export class TmSim { + readonly machine: Tm; + paths: TmState[] = []; + readonly input: string; + + current_states: Map = new Map(); + accepted: TmState[] = []; + rejected: TmState[] = []; + + constructor(machine: Tm, input: string) { + this.machine = machine; + this.input = input; + this.initial(); + } + + private init_state(state: Initializer) { + state.repr = state.state + " [ " + this.machine.blank_symbol + " " + state.tape!.map((s, i, _) => i == state.head ? `[${s}]` : s).join(" ") + " " + this.machine.blank_symbol + " ]"; + + + const frozen = state as TmState; + if (frozen.accepted) this.accepted.push(frozen); + this.paths.push(frozen); + if (!this.current_states.has(frozen.state)) { + this.current_states.set(frozen.state, []); + } + this.current_states.get(frozen.state)!.push(frozen); + } + + private initial() { + const state: Initializer = { + state: this.machine.initial_state, + accepted: this.machine.final_states.has(this.machine.initial_state), + tape: this.input.split(''), + head: 0, + + path: [], + }; + + if (state.tape!.length == 0) state.tape!.push(this.machine.blank_symbol) + + this.init_state(state); + } + + private transition(from: TmState, to: TmTransTo) { + const state: Initializer = { + state: to.state, + accepted: this.machine.final_states.has(to.state), + + path: from.path.concat([to]), + }; + + switch (to.direction) { + case "_": + state.tape = from.tape.slice(); + state.tape![from.head] = to.symbol; + state.head = from.head; + break; + case "<": + if (from.head == 0) { + state.tape = from.tape.splice(0, 0, to.symbol); + state.head = 0; + } else { + state.tape = from.tape.slice(); + state.tape![from.head] = to.symbol; + state.head = from.head - 1; + } + break; + case ">": + state.head = from.head + 1; + state.tape = from.tape.slice(); + state.tape![from.head] = to.symbol; + if (state.head == from.tape.length) { + state.tape!.push(this.machine.blank_symbol); + } + break; + } + + this.init_state(state) + } + + step(): SimStepResult { + if (this.accepted.length != 0) return "accept"; + if (this.paths.length == 0) return "reject"; + + const paths: TmState[] = this.paths; + this.paths = []; + this.current_states.clear(); + + for (const from of paths) { + const symbol = from.tape[from.head]; + const transitions = this.machine.transitions_components.get(from.state)?.get(symbol) ?? []; + if (transitions.length == 0) { + this.rejected.push(from); + continue; + } + + for (const to of transitions) { + this.transition(from, to); + } + } + + if (this.accepted.length != 0) return "accept"; + if (this.paths.length == 0) return "reject"; + return "pending"; + } +} \ No newline at end of file diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index 48f035e..8e1ba30 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -398,7 +398,7 @@ function renderNode({ const lineH = 14; let w = 0; - for (const ln of paths) w = Math.max(w, ctx.measureText(ln.toString()).width); + for (const ln of paths) w = Math.max(w, ctx.measureText(ln.repr).width); const boxW = w + padX * 2; const boxH = paths.length * lineH + padY * 2; @@ -415,7 +415,7 @@ function renderNode({ ctx.textBaseline = "top"; for (let i = 0; i < paths.length; i++) { ctx.fillStyle = paths[i].accepted ? t.current_node_border : t.fg_0; - ctx.fillText(paths[i].toString(), x, by + padY + i * lineH); + ctx.fillText(paths[i].repr, x, by + padY + i * lineH); } }