From 7971c61c746264847a99ef9812918302239a9fd8 Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:46:47 -0500 Subject: [PATCH] sync --- example.npda | 6 +- src/automata/mod.rs | 43 +- src/automata/npda.rs | 117 +- src/loader/ast.rs | 28 +- src/loader/lexer.rs | 19 +- src/loader/log.rs | 33 +- src/loader/parser.rs | 293 +- src/main.rs | 8 +- web/root/editor.css | 7 +- web/root/editor.js | 358 +- web/root/index.html | 9 +- web/root/index.js | 1 + web/root/js/vis-network.js | 40777 +++++++++++++++++++++++++++++++ web/root/js/vis-network.min.js | 27 + web/src/lib.rs | 11 +- 15 files changed, 41485 insertions(+), 252 deletions(-) create mode 100644 web/root/js/vis-network.js create mode 100644 web/root/js/vis-network.min.js diff --git a/example.npda b/example.npda index 9579916..2649bd5 100644 --- a/example.npda +++ b/example.npda @@ -1,7 +1,7 @@ - +type=NPDA Q = {q0, q1} // states E = {a, b} // alphabet -T = {z0, A, B} // stack +T = {z0, A, B} // stack q0 = q0 z0 = z0 @@ -18,4 +18,4 @@ d(q0, epsilon, B) = { (q1, B) } // consume stack until empty d(q1, a, A) = { (q1, epsilon) } -d(q1, b, B) = { (q1, epsilon) } +d(q1, b, B) = { (q1, epsilon) } \ No newline at end of file diff --git a/src/automata/mod.rs b/src/automata/mod.rs index 89d2718..3f8826c 100644 --- a/src/automata/mod.rs +++ b/src/automata/mod.rs @@ -7,13 +7,13 @@ pub mod npda; pub mod ntm; pub mod tm; -pub trait Get{ +pub trait Get { type Output; fn get(&self, idx: Idx) -> Option<&Self::Output>; fn get_mut(&mut self, idx: Idx) -> Option<&mut Self::Output>; } -pub trait GetDefault{ +pub trait GetDefault { type Output: Default; fn get_or_insert_default(&mut self, idx: Idx) -> &Self::Output; fn get_mut_or_insert_default(&mut self, idx: Idx) -> &mut Self::Output; @@ -67,7 +67,6 @@ pub struct State(u16); #[derive(Clone, Debug, Copy, Hash, PartialEq, Eq)] pub struct Symbol(u16); - #[derive(Clone, Debug)] pub struct StateMap(Vec); @@ -84,18 +83,42 @@ pub struct StateSymbolMap { max_state: u16, } - -index!(StateSymbolMap, self, self.map, state.0 as usize + self.max_state as usize * symbol.0 as usize, (state, symbol) = (State, Symbol)); -index!(StateSymbolMap, self, self.map, state.0 as usize + self.max_state as usize * symbol.0 as usize, (symbol, state) = (Symbol, State)); - +index!( + StateSymbolMap, + self, + self.map, + state.0 as usize + self.max_state as usize * symbol.0 as usize, + (state, symbol) = (State, Symbol) +); +index!( + StateSymbolMap, + self, + self.map, + state.0 as usize + self.max_state as usize * symbol.0 as usize, + (symbol, state) = (Symbol, State) +); #[derive(Clone, Debug, Default)] pub struct CharMap(HashMap); -index!(CharMap, self, self.0, &char, char = char, self.0.entry(char).or_default()); +index!( + CharMap, + self, + self.0, + &char, + char = char, + self.0.entry(char).or_default() +); #[derive(Clone, Debug, Default)] pub struct CharEpsilonMap(HashMap, T>); -index!(CharEpsilonMap, self, self.0, &Some(char), char = char, self.0.entry(Some(char)).or_default()); -index!(CharEpsilonMap, self, self.0, &char, char = Option, self.0.entry(char).or_default()); \ No newline at end of file +index!( + CharEpsilonMap, + self, + self.0, + &Some(char), + char = char, + self.0.entry(Some(char)).or_default() +); +index!(CharEpsilonMap, self, self.0, &char, char = Option, self.0.entry(char).or_default()); diff --git a/src/automata/npda.rs b/src/automata/npda.rs index b537153..dad5b07 100644 --- a/src/automata/npda.rs +++ b/src/automata/npda.rs @@ -31,15 +31,14 @@ pub struct Simulator { running: Vec, } -pub enum SimulatorResult{ +pub enum SimulatorResult { Pending, Reject, - Accept(NPDA) + Accept(NPDA), } impl Simulator { pub fn begin(input: impl Into, table: TransitionTable) -> Self { - Self { input: input.into(), running: vec![NPDA { @@ -110,9 +109,9 @@ impl Simulator { } } self.running = new; - if self.running.is_empty(){ + if self.running.is_empty() { SimulatorResult::Reject - }else{ + } else { SimulatorResult::Pending } } @@ -153,10 +152,9 @@ impl TransitionTable { for Spanned(element, span) in ast { use Spanned as S; - use ast::Dest; use ast::TopLevel as TL; match element { - TL::Assignment(S(Dest::Ident("Q"), _), list) => { + TL::Item(S("Q", _), list) => { if !states.is_empty() { logs.emit_error("states already set", *span); } @@ -183,7 +181,7 @@ impl TransitionTable { logs.emit_error("states cannot be empty", *span); } } - TL::Assignment(S(Dest::Ident("E" | SIGMA_UPPER | "sigma"), _), list) => { + TL::Item(S("E" | SIGMA_UPPER | "sigma", _), list) => { if !alphabet.is_empty() { logs.emit_error("alphabet already set", *span); } @@ -207,7 +205,7 @@ impl TransitionTable { logs.emit_error("alphabet cannot be empty", *span); } } - TL::Assignment(S(Dest::Ident("F"), _), list) => { + TL::Item(S("F", _), list) => { if final_states.is_some() { logs.emit_error("final states already set", *span); } @@ -219,17 +217,17 @@ impl TransitionTable { let Some(ident) = item.expect_ident(&mut logs) else { continue; }; - if let Some(state) = states.get(ident){ + if let Some(state) = states.get(ident) { if !map.insert(*state) { logs.emit_error("final state redefined", item.1); } - } else{ + } else { logs.emit_error("final state not defined in set of states", item.1); } } final_states = Some(map); } - TL::Assignment(S(Dest::Ident("T" | GAMMA_UPPER | "gamma"), _), list) => { + TL::Item(S("T" | GAMMA_UPPER | "gamma", _), list) => { if !stack_symbols.is_empty() { logs.emit_error("stack symbols already set", *span); } @@ -256,7 +254,7 @@ impl TransitionTable { logs.emit_error("stack symbols cannot be empty", *span); } } - TL::Assignment(S(Dest::Ident("I" | "q0"), _), S(src, src_d)) => match src { + TL::Item(S("I" | "q0", _), S(src, src_d)) => match src { ast::Item::Symbol(Sym::Ident(ident)) => { if initial_state.is_some() { logs.emit_error("initial state already set", *span); @@ -269,7 +267,7 @@ impl TransitionTable { } _ => logs.emit_error("expected ident", *src_d), }, - TL::Assignment(S(Dest::Ident("S" | "z0"), _), S(src, src_d)) => match src { + TL::Item(S("S" | "z0", _), S(src, src_d)) => match src { ast::Item::Symbol(Sym::Ident(ident)) => { if initial_stack.is_some() { logs.emit_error("initial stack already set", *span); @@ -285,12 +283,12 @@ impl TransitionTable { } _ => logs.emit_error("expected ident", *src_d), }, - TL::Assignment(S(Dest::Ident(name), dest_s), _) => { + TL::Item(S(name, dest_s), _) => { logs.emit_error(format!("unknown item {name:?}, expected 'Q'|'E'|'{SIGMA_UPPER}'|'sigma'|'F'|'T'|'{GAMMA_UPPER}'|'gamma'|'I'|'q0'|'S'|'z0'"), *dest_s); } - TL::Assignment( - S(Dest::Function(S("d" | DELTA_LOWER | "delta", _), tuple), _), + TL::TransitionFunc( + S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list, ) => { let list = list.set_weak(); @@ -299,7 +297,7 @@ impl TransitionTable { else { continue; }; - let Some(state) = states.get(state.0).copied() else{ + let Some(state) = states.get(state.0).copied() else { logs.emit_error("transition state not defined as state", state.1); continue; }; @@ -313,18 +311,25 @@ impl TransitionTable { let char = match letter.0 { Sym::Epsilon => None, - Sym::Ident(val) => if let Some(char) = val.chars().next() && val.chars().count() == 1 { - if !alphabet.contains(&char){ - logs.emit_error("transition letter not defined in alphabet", letter.1); + Sym::Ident(val) => { + if let Some(char) = val.chars().next() + && val.chars().count() == 1 + { + if !alphabet.contains(&char) { + logs.emit_error( + "transition letter not defined in alphabet", + letter.1, + ); + } + Some(char) + } else { + logs.emit_error( + "transition letter can only be single character", + letter.1, + ); + None } - Some(char) - }else{ - logs.emit_error( - "transition letter can only be single character", - letter.1, - ); - None - }, + } }; for item in list { @@ -339,29 +344,37 @@ impl TransitionTable { logs.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(&mut logs)?; - let Some(symbol) = stack_symbols.get(ident).copied() else{ - logs.emit_error("transition stack symbol not defined", symbol.1); - return None; - }; - Some(symbol) - }).collect(); - + 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(&mut logs)?; + + let Some(symbol) = stack_symbols.get(ident).copied() else { + logs.emit_error( + "transition stack symbol not defined", + symbol.1, + ); + return None; + }; + Some(symbol) + }) + .collect(); + if !transitions_map .entry((state, char, stack_symbol)) .or_insert(HashSet::new()) - .insert((next_state, stack)) { - logs.emit_warning("duplicate transition", item.1); - } + .insert((next_state, stack)) + { + logs.emit_warning("duplicate transition", item.1); + } } } - TL::Assignment(S(Dest::Function(S(name, _), _), dest_s), _) => { + TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { logs.emit_error( format!("unknown function {name:?}, expected 'd'|'delta'|'{DELTA_LOWER}'"), *dest_s, @@ -429,21 +442,21 @@ impl TransitionTable { a }, )); - let final_states = final_states.map(|f|{ - StateMap(f.iter().fold(vec![false; states.len()], |mut a, k|{ + let final_states = final_states.map(|f| { + StateMap(f.iter().fold(vec![false; states.len()], |mut a, k| { a[k.0 as usize] = true; a })) }); - let mut transitions: StateSymbolMap>> = StateSymbolMap{ + let mut transitions: StateSymbolMap>> = StateSymbolMap { map: vec![CharEpsilonMap::default(); stack_symbols.len() * states.len()], max_state: states.len() as u16, }; - - for ((q, c, s), to) in transitions_map{ + + for ((q, c, s), to) in transitions_map { let from = &mut transitions[(q, s)]; - for (n, ss) in to{ + for (n, ss) in to { from.get_mut_or_insert_default(c).push(To(n, ss)); } } diff --git a/src/loader/ast.rs b/src/loader/ast.rs index c230132..2f6ee80 100644 --- a/src/loader/ast.rs +++ b/src/loader/ast.rs @@ -2,6 +2,15 @@ use std::ops::Range; use super::Spanned; +#[derive(Clone, Debug)] +pub enum ListKind { + Brace, + Bracket, + + BraceComma, + BracketComma, +} + #[derive(Clone, Debug)] pub struct Tuple<'a>(pub Vec>>); @@ -11,12 +20,6 @@ pub enum Symbol<'a> { Ident(&'a str), } -#[derive(Clone, Debug)] -pub enum Dest<'a> { - Ident(&'a str), - Function(Spanned<&'a str>, Spanned>), -} - #[derive(Clone, Debug)] pub enum Item<'a> { Symbol(Symbol<'a>), @@ -40,12 +43,19 @@ pub enum Regex<'a> { } #[derive(Clone, Debug)] -pub struct List<'a>(pub Vec>>); +pub struct List<'a>(pub Vec>>, pub ListKind); + +#[derive(Clone, Debug)] +pub struct ProductionGroup<'a>(pub Vec>>); #[derive(Clone, Debug)] pub enum TopLevel<'a> { - Assignment(Spanned>, Spanned>), - ProductionRule(Spanned>, Spanned>), + Item(Spanned<&'a str>, Spanned>), + TransitionFunc(Spanned<(Spanned<&'a str>, Spanned>)>, Spanned>), + ProductionRule( + Spanned>, + Spanned>>>, + ), Table(), } diff --git a/src/loader/lexer.rs b/src/loader/lexer.rs index 3e7549e..9f14b78 100644 --- a/src/loader/lexer.rs +++ b/src/loader/lexer.rs @@ -26,13 +26,14 @@ pub enum Token<'a> { Comment(&'a str), Ident(&'a str), + LineEnd, } impl<'a> std::fmt::Display for Token<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Token::LPar => write!(f, "')'"), - Token::RPar => write!(f, "'('"), + Token::LPar => write!(f, "'('"), + Token::RPar => write!(f, "')'"), Token::LBrace => write!(f, "'{{'"), Token::RBrace => write!(f, "'}}'"), Token::LBracket => write!(f, "'['"), @@ -49,6 +50,7 @@ impl<'a> std::fmt::Display for Token<'a> { Token::Comment(_) => write!(f, ""), Token::Ident(ident) if f.alternate() => write!(f, "{ident:?}"), Token::Ident(_) => write!(f, "ident"), + Token::LineEnd => write!(f, "eol"), } } } @@ -118,7 +120,15 @@ impl<'a> std::iter::Iterator for Lexer<'a> { while let Some(c) = self.peek() && c.is_whitespace() { - self.consume(); + if c == '\n'{ + self.start = self.position; + self.consume(); + let res = Some(Spanned(Ok(Token::LineEnd), Span(self.start, self.position))); + self.start = self.position; + return res; + }else{ + self.consume(); + } } self.start = self.position; @@ -153,7 +163,8 @@ impl<'a> std::iter::Iterator for Lexer<'a> { '/' => match self.consume() { Some('/') => loop { if let Some('\n') | None = self.consume() { - break Ok(Token::Comment(&self.input[self.start + 2..self.position])); + self.backtrack(); + break Ok(Token::Comment(&self.input[self.start + 2..=self.position])); } }, Some('*') => loop { diff --git a/src/loader/log.rs b/src/loader/log.rs index d975241..ca64aa0 100644 --- a/src/loader/log.rs +++ b/src/loader/log.rs @@ -73,15 +73,15 @@ impl<'a> Logs<'a> { }) } - pub fn entries(&self) -> &[LogEntry]{ + pub fn entries(&self) -> &[LogEntry] { &self.logs } - pub fn into_entries(self) -> impl Iterator{ + pub fn into_entries(self) -> impl Iterator { self.logs.into_iter() } - pub fn src(&self) -> &str{ + pub fn src(&self) -> &str { &self.src } } @@ -134,15 +134,24 @@ impl<'a> Display for LogEntryDisplay<'a> { .map(|v| v + 1) .unwrap_or(0); - let end = self - .src - .get(span.1..) - .and_then(|s| s.find('\n')) - .map(|v| v + span.1) - .unwrap_or(self.src.len()); + let end = if self.src.get(..span.1).unwrap_or("").ends_with("\n") { + span.1 + } else { + self.src + .get(span.1..) + .and_then(|s| s.find('\n')) + .map(|v| v + span.1) + .unwrap_or(self.src.len()) + }; let mut index = start; - for (i, line) in self.src.get(start..end).unwrap_or("").lines().enumerate() { + for (i, line) in self + .src + .get(start..end) + .unwrap_or("") + .split_inclusive("\n") + .enumerate() + { write!(f, "{BOLD}{CYAN}{:>padding$}: {RESET}", i + line_start)?; for char in line.chars() { if char == '\t' { @@ -151,7 +160,9 @@ impl<'a> Display for LogEntryDisplay<'a> { write!(f, "{char}")? } } - writeln!(f)?; + if !line.ends_with("\n") { + writeln!(f)?; + } write!(f, "{BOLD}{CYAN}")?; for _ in 0..padding + 3 { write!(f, " ")?; diff --git a/src/loader/parser.rs b/src/loader/parser.rs index a002bcd..becef77 100644 --- a/src/loader/parser.rs +++ b/src/loader/parser.rs @@ -1,12 +1,12 @@ use crate::loader::log::{LogEntryDisplay, Logs}; -use crate::loader::{Span, Spanned}; +use crate::loader::{EPSILON_LOWER, Span, Spanned}; use super::ast::*; use super::lexer::{Lexer, Token}; pub struct Parser<'a> { lexer: Lexer<'a>, - peek: Option>>, + peek: Spanned>>, logs: Logs<'a>, } @@ -14,30 +14,43 @@ impl<'a> Parser<'a> { pub fn new(lexer: Lexer<'a>) -> Self { Parser { logs: Logs::new(lexer.input()), - peek: None, + peek: Spanned(None, Span(0,0)), lexer, } } - pub fn eof(&self) -> Span{ + fn eof(&self) -> Span { self.lexer.eof_span() } - fn next_token(&mut self) -> Option>> { - if self.peek.is_some(){ - return self.peek.take() + fn advance_line(&mut self) { + if self.expect_token(Token::LineEnd).0 { + self.peek = Spanned(None, Span(0,0)); + } + } + + fn next_token(&mut self) -> Spanned>> { + match self.peek.0 { + Some(Token::LineEnd) => return self.peek, + Some(_) => return Spanned(self.peek.0.take(), self.peek.1), + _ => {} } loop { - match self.lexer.next()? { - Spanned(Ok(Token::Comment(_)), _) => {} - Spanned(Ok(ok), r) => return Some(Spanned(ok, r)), - Spanned(Err(err), span) => self.logs.emit_error(format!("lexer: {err:?}"), span), + match self.lexer.next() { + Some(Spanned(Ok(Token::Comment(_)), _)) => {} + Some(Spanned(Ok(Token::LineEnd), span)) => { + self.peek = Spanned(Some(Token::LineEnd), span); + return self.peek; + } + Some(Spanned(Ok(ok), r)) => return Spanned(Some(ok), r), + Some(Spanned(Err(err), span)) => self.logs.emit_error(format!("lexer: {err:?}"), span), + None => return Spanned(None, self.lexer.eof_span()) } } } - fn peek_token(&mut self) -> Option>> { - if self.peek.is_none(){ + fn peek_token(&mut self) -> Spanned>> { + if self.peek.0.is_none() { self.peek = self.next_token(); } self.peek @@ -47,7 +60,7 @@ impl<'a> Parser<'a> { if let Some(Spanned(token, span)) = self.peek_token() { if token != expected { self.logs.emit_error( - format!("unexpected token {:#}, expected {:}", token, expected), + format!("unexpected {:#}, expected {:}", token, expected), span, ); (false, span) @@ -56,22 +69,24 @@ impl<'a> Parser<'a> { (true, span) } } else { - self.logs - .emit_error(format!("unexpected eof expected {:#}", expected), self.eof()); + self.logs.emit_error( + format!("unexpected eof expected {:#}", expected), + self.eof(), + ); (false, self.eof()) } } - pub fn parse_symbol(&mut self) -> Spanned> { + fn parse_symbol(&mut self) -> Spanned> { match self.next_token() { - Some(Spanned(Token::Tilde, r)) => Spanned(Symbol::Epsilon, r), - Some(Spanned(Token::Ident("epsilon"), r)) => Spanned(Symbol::Epsilon, r), - Some(Spanned(Token::Ident(super::EPSILON_LOWER), r)) => Spanned(Symbol::Epsilon, r), - Some(Spanned(Token::Ident(ident), r)) => Spanned(Symbol::Ident(ident), r), - Some(Spanned(got, span)) => { + Spanned(Some(Token::Tilde), r) => Spanned(Symbol::Epsilon, r), + Spanned(Some(Token::Ident("epsilon")), r) => Spanned(Symbol::Epsilon, r), + Spanned(Some(Token::Ident(super::EPSILON_LOWER)), r) => Spanned(Symbol::Epsilon, r), + Spanned(Some(Token::Ident(ident)), r) => Spanned(Symbol::Ident(ident), r), + Spanned(Some(got), span) => { self.logs.emit_error( format!( - "unexpected token {:#}, expected {:}|{:}", + "unexpected token {:#}, expected {:}|{:} (symbol)", got, Token::Tilde, Token::Ident("") @@ -80,21 +95,21 @@ impl<'a> Parser<'a> { ); Spanned(Symbol::Ident(""), span) } - None => { + Spanned(None, span) => { self.logs.emit_error( format!( - "unexpected eof expected {:}|{:}", + "unexpected eof expected {:}|{:} (symbol)", Token::Tilde, Token::Ident("") ), - self.eof(), + span, ); Spanned(Symbol::Ident(""), self.eof()) } } } - pub fn parse_tupple(&mut self) -> Spanned> { + fn parse_tupple(&mut self) -> Spanned> { let mut items = Vec::new(); let (matched, start) = self.expect_token(Token::LPar); if !matched { @@ -106,12 +121,20 @@ impl<'a> Parser<'a> { if matches!(self.peek_token(), Some(Spanned(Token::Comma, _))) { self.next_token(); } - if self.peek_token().is_none() { - self.logs.emit_error( - format!("unexpected eof expected {:}", Token::RPar), - self.eof(), - ); - break; + match self.peek_token() { + None => { + self.logs.emit_error( + format!("unexpected eof expected {:}", Token::RPar), + self.eof(), + ); + return Spanned(Tuple(items), start.join(self.eof())); + } + Some(Spanned(Token::LineEnd, span)) => { + self.logs + .emit_error(format!("unexpected eol expected {:}", Token::RPar), span); + return Spanned(Tuple(items), start.join(span)); + } + _ => {} } } @@ -120,7 +143,7 @@ impl<'a> Parser<'a> { Spanned(Tuple(items), start.join(end)) } - pub fn parse_item(&mut self) -> Spanned> { + fn parse_item(&mut self) -> Spanned> { match self.peek_token() { Some(Spanned(Token::Ident(_) | Token::Tilde, _)) => { self.parse_symbol().map(Item::Symbol) @@ -131,7 +154,7 @@ impl<'a> Parser<'a> { self.next_token(); self.logs.emit_error( format!( - "unexpected token {:#}, expected {:}|{:}|{:}|{:}|{:}", + "unexpected token {:#}, expected {:}|{:}|{:}|{:}|{:} (item)", got, Token::Tilde, Token::Ident(""), @@ -146,7 +169,7 @@ impl<'a> Parser<'a> { None => { self.logs.emit_error( format!( - "unexpected eof expected {:}|{:}|{:}|{:}|{:}", + "unexpected eof expected {:}|{:}|{:}|{:}|{:} (item)", Token::Tilde, Token::Ident(""), Token::LPar, @@ -160,7 +183,7 @@ impl<'a> Parser<'a> { } } - pub fn parse_list(&mut self) -> Spanned> { + fn parse_list(&mut self) -> Spanned> { let mut list = Vec::new(); let (start, match_end) = match self.next_token() { @@ -176,7 +199,7 @@ impl<'a> Parser<'a> { ), span, ); - return Spanned(List(Vec::new()), span); + return Spanned(List(Vec::new(), ListKind::BracketComma), span); } None => { self.logs.emit_error( @@ -187,65 +210,179 @@ impl<'a> Parser<'a> { ), self.eof(), ); - return Spanned(List(Vec::new()), self.eof()); + return Spanned(List(Vec::new(), ListKind::BracketComma), self.eof()); } }; + let mut comma = false; while self.peek_token().map(|t| t.0) != Some(match_end) { list.push(self.parse_item()); if matches!(self.peek_token(), Some(Spanned(Token::Comma, _))) { + comma = true; self.next_token(); } - if self.peek_token().is_none() { - self.logs - .emit_error(format!("unexpected eof expected {:}", match_end), self.eof()); - break; + match self.peek_token() { + None => { + self.logs.emit_error( + format!("unexpected eof expected {:}", match_end), + self.eof(), + ); + return Spanned(List(list, ListKind::BraceComma), start.join(self.eof())); + } + Some(Spanned(Token::LineEnd, span)) => { + self.logs + .emit_error(format!("unexpected eol expected {:}", match_end), span); + return Spanned(List(list, ListKind::BraceComma), start.join(span)); + } + _ => {} } } let (_, end) = self.expect_token(match_end); - Spanned(List(list), start.join(end)) + let kind = match (comma, match_end) { + (true, Token::RBrace) => ListKind::BraceComma, + (false, Token::RBrace) => ListKind::Brace, + (true, Token::RBracket) => ListKind::BracketComma, + (false, Token::RBracket) => ListKind::Bracket, + _ => unreachable!(), + }; + Spanned(List(list, kind), start.join(end)) } - pub fn parse_regex(&mut self) -> Spanned> { + fn parse_regex(&mut self) -> Spanned> { todo!() } + fn parse_production_rule( + &mut self, + sym: Symbol<'a>, + start: Span, + ) -> Option>> { + let mut lhs_group = ProductionGroup(vec![Spanned(sym, start)]); + let mut lhs_group_end = start; + while !matches!( + self.peek_token(), + None | Some(Spanned(Token::LSmallArrow | Token::LineEnd, _)) + ) { + let sym = self.parse_symbol(); + lhs_group_end = sym.1; + lhs_group.0.push(sym); + } + if !self.expect_token(Token::LSmallArrow).0{ + return Some(Spanned(TopLevel::ProductionRule(Spanned(lhs_group, start.join(lhs_group_end)), Spanned(vec![], lhs_group_end)), start.join(lhs_group_end))) + } + + let mut groups = Vec::new(); + + while !matches!(self.peek_token(), None | Some(Spanned(Token::LineEnd, _))){ + let mut group = ProductionGroup(vec![]); + while !matches!(self.peek_token(), None | Some(Spanned(Token::LineEnd|Token::Or, _))){ + group.0.push(self.parse_symbol()); + } + if group.0.is_empty(){ + let span = if let Some(Spanned(_, span)) = self.peek_token(){ + span + }else{ + self.eof() + }; + self.logs.emit_error("cannot have empty production rule", span); + } + if matches!(self.peek_token(), Some(Spanned(Token::Or, _))){ + self.next_token(); + // if matches!(self.peek_token(), None|Spanned(Token::Or|Token::LineEnd)) + } + let group_start = group.0.first().map(|g|g.1).unwrap_or(start); + let group_end = group.0.last().map(|g|g.1).unwrap_or(start); + groups.push(Spanned(group, group_start.join(group_end))) + } + + if groups.is_empty(){ + self.logs.emit_error("cannot have empty production rule", start.join(lhs_group_end)); + } + + let rules_start = groups.first().map(|f|f.1).unwrap_or(start); + let rules_end = groups.last().map(|f|f.1).unwrap_or(start); + + Some(Spanned(TopLevel::ProductionRule(Spanned(lhs_group, start.join(lhs_group_end)), Spanned(groups, rules_start.join(rules_end))), start.join(rules_end))) + } + + fn parse_transition_function( + &mut self, + ident: &'a str, + start: Span, + ) -> Option>> { + let tuple = self.parse_tupple(); + let span = start.join(tuple.1); + let dest = Spanned((Spanned(ident, start), tuple), span); + if !self.expect_token(Token::Eq).0 { + return None; + } + let item = self.parse_item(); + let span = start.join(item.1); + Some(Spanned(TopLevel::TransitionFunc(dest, item), span)) + } + + pub fn next_element(&mut self) -> Option>> { + let result = loop { + let next = self.next_token()?; + match (next, self.peek_token()) { + (Spanned(Token::LineEnd, _), _) => self.advance_line(), + (Spanned(Token::Ident(ident), start), Some(Spanned(Token::LPar, _))) => { + if let Some(tf) = self.parse_transition_function(ident, start) { + break Some(tf); + } + } + ( + Spanned( + Token::Ident(EPSILON_LOWER) | Token::Ident("epsilon") | Token::Tilde, + start, + ), + Some(Spanned(Token::LSmallArrow | Token::Ident(_) | Token::Tilde, _)), + ) => { + if let Some(pr) = self.parse_production_rule(Symbol::Epsilon, start) { + break Some(pr); + } + } + ( + Spanned(Token::Ident(ident), start), + Some(Spanned(Token::LSmallArrow | Token::Ident(_) | Token::Tilde, _)), + ) => { + if let Some(pr) = self.parse_production_rule(Symbol::Ident(ident), start) { + break Some(pr); + } + } + (Spanned(Token::Ident(ident), start), _) => { + let name = Spanned(ident, start); + if !self.expect_token(Token::Eq).0 { + continue; + } + let item = self.parse_item(); + let span = start.join(item.1); + break Some(Spanned(TopLevel::Item(name, item), span)); + } + _ => { + self.logs.emit_error( + format!( + "unexpected token {:#}, expected {:}", + next.0, + Token::Ident("") + ), + next.1, + ); + while !matches!(self.next_token(), None|Some(Spanned(Token::LineEnd, _))){ + + } + }, + } + }; + self.advance_line(); + result + } + pub fn parse_elements(mut self) -> (Vec>>, Logs<'a>) { let mut result = Vec::new(); - while let Some(next) = self.next_token() { - match (next, self.peek_token()) { - (Spanned(Token::Ident(ident), start), Some(Spanned(Token::LPar, _))) => { - let tuple = self.parse_tupple(); - let span = start.join(tuple.1); - let dest = Spanned(Dest::Function(Spanned(ident, start), tuple), span); - if !self.expect_token(Token::Eq).0{continue;} - let item = self.parse_item(); - let span = start.join(item.1); - result.push(Spanned(TopLevel::Assignment(dest, item), span)); - } - ( - Spanned(Token::Ident(_), start), - Some(Spanned(Token::LSmallArrow, end)), - ) => { - self.logs.emit_error("Production rules are not yet supported", start.join(end)); - } - (Spanned(Token::Ident(ident), start), _) => { - let dest = Spanned(Dest::Ident(ident), start); - if !self.expect_token(Token::Eq).0{continue;} - let item = self.parse_item(); - let span = start.join(item.1); - result.push(Spanned(TopLevel::Assignment(dest, item), span)); - } - _ => self.logs.emit_error( - format!( - "unexpected token {:#}, expected {:}", - next.0, - Token::Ident("") - ), - next.1, - ), - } + while let Some(next) = self.next_element() { + result.push(next) } (result, self.logs) diff --git a/src/main.rs b/src/main.rs index 51eccca..c280171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,16 +22,16 @@ fn main() { println!("running on: '{input}'"); let mut simulator = npda::Simulator::begin(input, table); loop { - match simulator.step(){ - npda::SimulatorResult::Pending => {}, + match simulator.step() { + npda::SimulatorResult::Pending => {} npda::SimulatorResult::Reject => { println!("REJECTED"); break; - }, + } npda::SimulatorResult::Accept(npda) => { println!("ACCEPT: {npda:?}"); break; - }, + } } } } diff --git a/web/root/editor.css b/web/root/editor.css index 0c80295..98ae2b6 100644 --- a/web/root/editor.css +++ b/web/root/editor.css @@ -106,10 +106,9 @@ body { background: #111; } -#canvas { - width: 100%; - height: 100%; - display: block; +.graph { + width: 100%; + height: 100%; } /* ---------- Bottom area (terminal + editor) ---------- */ diff --git a/web/root/editor.js b/web/root/editor.js index c4a162d..3119b1c 100644 --- a/web/root/editor.js +++ b/web/root/editor.js @@ -8,44 +8,45 @@ import { oneDark } from "https://esm.sh/@codemirror/theme-one-dark"; import wasm from "./wasm.js" +import * as vis from "./js/vis-network.js" + + function tokenize(text) { - try{ - return wasm.lex(text); - }catch(e){ - console.log(e) - return [] - } + try { + return wasm.lex(text); + } catch (e) { + console.log(e) + return [] + } } function compile(text) { - try{ - return wasm.compile(text); - }catch(e){ - console.log(e) - return [] + try { + return wasm.compile(text); + } catch (e) { + console.log(e) + return [] } } -/* ===================================================================== */ -// Map token types -> CSS classes const tokenClass = (t) => - ({ - comment: "tok-comment", - keyword: "tok-keyword", - error: "tok-error", - ident: "tok-ident", - punc: "tok-punc", - string: "tok-string", - lpar: "rb-", - lbrace: "rb-", - lbracket: "rb-", +({ + comment: "tok-comment", + keyword: "tok-keyword", + error: "tok-error", + ident: "tok-ident", + punc: "tok-punc", + string: "tok-string", + lpar: "rb-", + lbrace: "rb-", + lbracket: "rb-", + + rpar: "rb-", + rbrace: "rb-", + rbracket: "rb-", +}[t] || "tok-ident"); - rpar: "rb-", - rbrace: "rb-", - rbracket: "rb-", - }[t] || "tok-ident"); -// ===================== Diagnostics helpers ===================== function severityClass(sev) { const s = (sev || "error").toLowerCase(); if (s === "warning") return "cm-diag-warning"; @@ -57,21 +58,11 @@ function sevRank(sev) { if (sev === "warning") return 2; return 1; } -function sevLabel(sev) { - if (sev === "warning") return "warning"; - if (sev === "info") return "info"; - return "error"; -} -function sevAnsiColorClass(sev) { - if (sev === "warning") return "ansi-yellow"; - if (sev === "info") return "ansi-cyan"; - return "ansi-red"; -} function buildAnalysis(text, doc) { const tokens = tokenize(text); - const {log, log_formatted} = compile(text); + const { log, log_formatted } = compile(text); // Build ONE Decoration set: syntax + diagnostics const marks = []; @@ -81,8 +72,8 @@ function buildAnalysis(text, doc) { const start = Math.max(0, Math.min(docLen, tok.start)); const end = Math.max(start, Math.min(docLen, tok.end)); var tc = tokenClass(tok.kind); - if (tc === "rb-"){ - tc += tok.scope_level.toString(); + if (tc === "rb-") { + tc += tok.scope_level.toString(); } if (end > start) { marks.push(Decoration.mark({ class: tc }).range(start, end)); @@ -90,7 +81,7 @@ function buildAnalysis(text, doc) { } for (const d of log) { - if (d.start === undefined || d.end === undefined)continue; + if (d.start === undefined || d.end === undefined) continue; const start = Math.max(0, Math.min(docLen, d.start)); const endRaw = d.end == null ? d.start : d.end; const end = Math.max(start, Math.min(docLen, endRaw)); @@ -238,7 +229,7 @@ function formatTerminal(view) { let s = ""; s += `\x1b[90m[compile]\x1b[0m ${log.length} diagnostics\n`; - term.innerHTML = ansiToHtml(s+log_formatted); + term.innerHTML = ansiToHtml(s + log_formatted); } const terminalPlugin = ViewPlugin.fromClass( @@ -253,8 +244,8 @@ const terminalPlugin = ViewPlugin.fromClass( } ); -// ===================== Build editor ===================== -const initialText = `machine=NPDA + +const initialText = `type=NPDA Q = {q0, q1} // states E = {a, b} // alphabet T = {z0, A, B} // stack @@ -325,45 +316,24 @@ setDefaultLayoutWeights(); const app = document.getElementById("app"); const hSplit = document.getElementById("hSplit"); const vSplit = document.getElementById("vSplit"); - const canvas = document.getElementById("canvas"); + // const canvas = document.getElementById("canvas"); const canvasPane = document.getElementById("canvasPane"); let draggingH = false; let draggingV = false; - // --- Canvas height splitter --- hSplit.addEventListener("mousedown", (e) => { draggingH = true; document.body.style.cursor = "row-resize"; e.preventDefault(); }); - // --- Terminal/editor width splitter --- vSplit.addEventListener("mousedown", (e) => { draggingV = true; document.body.style.cursor = "col-resize"; e.preventDefault(); }); - function resizeCanvasToPane() { - // Keep canvas resolution in sync with CSS size (crisp rendering) - const rect = canvasPane.getBoundingClientRect(); - const dpr = window.devicePixelRatio || 1; - const w = Math.max(1, Math.floor(rect.width * dpr)); - const h = Math.max(1, Math.floor(rect.height * dpr)); - if (canvas.width !== w) canvas.width = w; - if (canvas.height !== h) canvas.height = h; - - // Optional: demo draw - const ctx = canvas.getContext("2d"); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "#1f6feb"; - ctx.fillRect(20 * dpr, 20 * dpr, 140 * dpr, 60 * dpr); - ctx.fillStyle = "#c9d1d9"; - ctx.font = `${14 * dpr}px ui-monospace, monospace`; - ctx.fillText("Canvas area", 30 * dpr, 55 * dpr); - } - window.addEventListener("mousemove", (e) => { const rect = app.getBoundingClientRect(); @@ -374,7 +344,6 @@ setDefaultLayoutWeights(); const maxCanvas = rect.height - 8 - minBottom; const canvasH = Math.max(minCanvas, Math.min(maxCanvas, y)); app.style.setProperty("--canvasH", `${canvasH}px`); - resizeCanvasToPane(); } if (draggingV) { @@ -383,7 +352,7 @@ setDefaultLayoutWeights(); const x = e.clientX - r.left; const minTerm = 220; const maxTerm = r.width - 8 - 220; - const termW = Math.max(minTerm, Math.min(maxTerm, r.width-x)); + const termW = Math.max(minTerm, Math.min(maxTerm, r.width - x)); app.style.setProperty("--termW", `${termW}px`); } }); @@ -393,8 +362,251 @@ setDefaultLayoutWeights(); draggingV = false; document.body.style.cursor = ""; }); +})(); - // Keep canvas crisp on window resize too - window.addEventListener("resize", resizeCanvasToPane); - resizeCanvasToPane(); -})(); \ No newline at end of file +let network = null; +const nodes = new vis.DataSet(); +const edges = new vis.DataSet(); + + +const automaton = { + states: ["q0", "q1"], + initialState: "q0", + acceptStates: ["q1"], + + transitions: [ + { + from: "q0", + to: "q0", + label: "ε, z0 → A z0\n" + }, + { + from: "q0", + to: "q0", + label: "ε, z0 → B z0" + }, + { + from: "q0", + to: "q1", + label: "ε, z0 → z0" + }, + { + from: "q1", + to: "q1", + label: "a, A → ε" + }, + { + from: "q1", + to: "q1", + label: "b, B → ε" + } + ] +}; + +/**@param {{ctx: CanvasRenderingContext2D}} */ +function renderNode({ + ctx, + id, + x, + y, + state: { selected, hover }, + style, + label, +}) { + return { + drawNode() { + ctx.save(); + var r = style.size; + + + ctx.beginPath(); + ctx.arc(x, y, r, 0, 2 * Math.PI); + ctx.fillStyle = "red"; + ctx.fill(); + ctx.lineWidth = 4; + ctx.strokeStyle = "blue"; + ctx.stroke(); + + ctx.fillStyle = "black"; + ctx.textAlign = 'center'; + ctx.fillText(label, x, y, r); + + + ctx.textAlign = 'center'; + ctx.strokeStyle = 'white'; + ctx.fillStyle = "black"; + let cy = y - (r + 10); + for (const part of "meow[]\nbeeep".split("\n").reverse()) { + const metrics = ctx.measureText(part); + cy -= metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; + ctx.strokeText(part, x, cy); + ctx.fillText(part, x, cy); + } + + + ctx.restore(); + }, + nodeDimensions: { width: 20, height: 20 }, + }; +} + + +// Populate nodes +for (const state of automaton.states) { + nodes.add({ + id: state, + label: state, + }); +} + +// Populate edges +automaton.transitions.forEach((t, i) => { + edges.add({ + id: `e${i}`, + from: t.from, + to: t.to, + label: t.label + }); +}); + +// updateGraphFromText(); +ensureGraph(); +function updateGraphFromText() { + ensureGraph(); + + const trans = [] + + // Collect state ids + const stateSet = new Set(); + for (const tr of trans) { + stateSet.add(tr.from); + stateSet.add(tr.to); + } + + // Update nodes (add missing, remove stale) + const existingNodeIds = new Set(nodes.getIds()); + const desiredNodeIds = new Set([...stateSet]); + + // remove stale + for (const id of existingNodeIds) { + if (!desiredNodeIds.has(id)) nodes.remove(id); + } + // add/update desired + for (const id of desiredNodeIds) { + const pos = pinnedPositions.get(id); + if (!existingNodeIds.has(id)) { + nodes.add({ + id, + label: id, + ...(pos ? { x: pos.x, y: pos.y, fixed: true } : {}) + }); + } else if (pos) { + nodes.update({ id, x: pos.x, y: pos.y, fixed: true }); + } + } + + // Update edges (stable IDs so edits don't flicker) + const desiredEdgeIds = new Set(); + const nextEdges = []; + + for (let i = 0; i < trans.length; i++) { + const tr = trans[i]; + const id = `${tr.from}::${tr.to}::${tr.label}::${i}`; + desiredEdgeIds.add(id); + nextEdges.push({ id, from: tr.from, to: tr.to, label: tr.label }); + } + + const existingEdgeIds = new Set(edges.getIds()); + for (const id of existingEdgeIds) { + if (!desiredEdgeIds.has(id)) edges.remove(id); + } + // add/update in batch + for (const e of nextEdges) { + if (!existingEdgeIds.has(e.id)) edges.add(e); + else edges.update(e); + } + + // If positions exist for all nodes, we can disable physics to “respect” manual layout + // Otherwise leave physics on to auto-layout new nodes. + const allPinned = [...desiredNodeIds].every((id) => pinnedPositions.has(id)); + network.setOptions({ physics: { enabled: !allPinned } }); + + // Redraw nicely after updates + network.fit({ animation: { duration: 200, easingFunction: "easeInOutQuad" } }); +} + +// ---------- 4) Hook graph updates into your existing single-pass analysis ---------- +const graphPlugin = ViewPlugin.fromClass(class { + constructor(view) { + updateGraphFromText(view.state.doc.toString()); + } + update(update) { + if (update.docChanged) { + updateGraphFromText(update.state.doc.toString()); + } + } +}); + +function chosen_node(values, id, selected, hovering) { + + console.log(values, id, selected, hovering) +} + +function ensureGraph() { + if (network) return; + + const container = document.getElementById("graph"); + network = new vis.Network( + container, + { nodes, edges }, + { + layout: { improvedLayout: true }, + physics: { + enabled: true, + solver: "barnesHut", + barnesHut: { gravitationalConstant: -8000, springLength: 120, springConstant: 0.04 }, + stabilization: { iterations: 200 } + }, + interaction: { + dragNodes: true, + hover: true, + multiselect: true + }, + nodes: { + shape: 'dot', + size: 14, + font: { color: "#c9d1d9" }, + color: { + background: "#1f6feb", + border: "#79c0ff", + highlight: { background: "#388bfd", border: "#a5d6ff" } + }, + chosen: { + node: chosen_node + }, + shape: "custom", + ctxRenderer: renderNode, + size: 18, + }, + edges: { + arrows: { to: { enabled: true, scaleFactor: 0.8 } }, + arrowStrikethrough: false, + font: { align: "middle", color: "#000000ff" }, + color: { color: "rgba(201,209,217,0.35)", highlight: "#c9d1d9" }, + smooth: { type: "dynamic" }, + arrows: "to", + } + } + ); + + // Save positions when user drags nodes + + network.on("dragEnd", (params) => { + const pos = network.getPositions(params.nodes); + for (const id of params.nodes) { + pinnedPositions.set(id, pos[id]); + } + }); + + window.network = network; +} diff --git a/web/root/index.html b/web/root/index.html index 98613ef..03c86ed 100644 --- a/web/root/index.html +++ b/web/root/index.html @@ -5,6 +5,10 @@ Automata + + + + @@ -20,7 +24,10 @@