mirror of
https://github.com/ParkerTenBroeck/automata.git
synced 2026-06-06 21:24:06 -04:00
Merge branch 'main' into gh-pages
This commit is contained in:
commit
8e55774b66
17 changed files with 883 additions and 618 deletions
|
|
@ -2,261 +2,377 @@ use std::collections::HashSet;
|
|||
|
||||
use super::*;
|
||||
|
||||
use crate::loader::{
|
||||
Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned,
|
||||
ast::{self, Symbol as Sym},
|
||||
use crate::{
|
||||
delta_lower, dual_struct_serde, epsilon, loader::{
|
||||
Context, INITIAL_STATE, Spanned,
|
||||
ast::{self, Symbol as Sym, TopLevel},
|
||||
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<Letter<'a>>,
|
||||
dual_struct_serde! {
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
pub struct TransitionFrom<'a> {
|
||||
#[serde(borrow)]
|
||||
pub state: State<'a>,
|
||||
pub letter: Option<Letter<'a>>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct TransitionTo<'a> {
|
||||
pub state: State<'a>,
|
||||
dual_struct_serde! {
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub struct TransitionTo<'a> {
|
||||
#[serde(borrow)]
|
||||
pub state: State<'a>,
|
||||
|
||||
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 Fa<'a> {
|
||||
pub initial_state: State<'a>,
|
||||
pub states: HashMap<State<'a>, StateInfo>,
|
||||
pub alphabet: HashMap<Letter<'a>, LetterInfo>,
|
||||
pub final_states: HashMap<State<'a>, StateInfo>,
|
||||
dual_struct_serde! { {#[serde_with::serde_as]}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Fa<'a> {
|
||||
#[serde(borrow)]
|
||||
pub initial_state: State<'a>,
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[serde_as(as = "serde_with::Seq<(_, _)>")]
|
||||
pub transitions: HashMap<TransitionFrom<'a>, HashSet<TransitionTo<'a>>>,
|
||||
#[cfg(not(feature = "serde"))]
|
||||
pub transitions: HashMap<TransitionFrom<'a>, HashSet<TransitionTo<'a>>>,
|
||||
#[serde(borrow)]
|
||||
pub states: HashMap<State<'a>, StateInfo>,
|
||||
|
||||
#[serde(borrow)]
|
||||
pub alphabet: HashMap<Letter<'a>, LetterInfo>,
|
||||
|
||||
#[serde(borrow)]
|
||||
pub final_states: HashMap<State<'a>, StateInfo>,
|
||||
|
||||
#[serde(borrow)]
|
||||
#[serde_as(as = "serde_with::Seq<(_, _)>")]
|
||||
pub transitions: HashMap<TransitionFrom<'a>, HashSet<TransitionTo<'a>>>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Fa<'a> {
|
||||
pub fn parse(
|
||||
pub fn compile(
|
||||
items: impl Iterator<Item = Spanned<ast::TopLevel<'a>>>,
|
||||
ctx: &mut Context<'a>,
|
||||
options: Options,
|
||||
) -> Option<Fa<'a>> {
|
||||
FaCompiler::new(ctx, options).compile(items)
|
||||
}
|
||||
}
|
||||
|
||||
let mut initial_state = None;
|
||||
pub struct FaCompiler<'a, 'b> {
|
||||
ctx: &'b mut Context<'a>,
|
||||
options: Options,
|
||||
|
||||
let mut states = HashMap::new();
|
||||
let mut alphabet = HashMap::new();
|
||||
let mut final_states = HashMap::new();
|
||||
initial_state: Option<(State<'a>, Span)>,
|
||||
|
||||
let mut transitions: HashMap<TransitionFrom<'a>, HashSet<TransitionTo<'a>>> =
|
||||
HashMap::new();
|
||||
states: HashMap<State<'a>, StateInfo>,
|
||||
states_def: Option<Span>,
|
||||
|
||||
alphabet: HashMap<Letter<'a>, LetterInfo>,
|
||||
alphabet_def: Option<Span>,
|
||||
|
||||
final_states: HashMap<State<'a>, StateInfo>,
|
||||
final_states_def: Option<Span>,
|
||||
|
||||
transitions: HashMap<TransitionFrom<'a>, HashSet<TransitionTo<'a>>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> FaCompiler<'a, 'b> {
|
||||
pub fn new(ctx: &'b mut Context<'a>, options: Options) -> Self {
|
||||
Self {
|
||||
ctx,
|
||||
options,
|
||||
|
||||
initial_state: Default::default(),
|
||||
states: Default::default(),
|
||||
states_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<Item = Spanned<ast::TopLevel<'a>>>,
|
||||
) -> Option<Fa<'a>> {
|
||||
for Spanned(element, span) in items {
|
||||
use Spanned as S;
|
||||
use ast::TopLevel as TL;
|
||||
match element {
|
||||
TL::Item(S("Q", _), list) => {
|
||||
if !states.is_empty() {
|
||||
ctx.emit_error("states already set", span);
|
||||
}
|
||||
let Some(list) = list.expect_set(ctx) else {
|
||||
continue;
|
||||
};
|
||||
for item in list {
|
||||
let Some(ident) = item.expect_ident(ctx) else {
|
||||
continue;
|
||||
};
|
||||
if states
|
||||
.insert(State(ident), StateInfo { definition: item.1 })
|
||||
.is_some()
|
||||
{
|
||||
ctx.emit_error("state redefined", item.1);
|
||||
}
|
||||
}
|
||||
|
||||
if list.is_empty() {
|
||||
ctx.emit_error("states cannot be empty", span);
|
||||
}
|
||||
}
|
||||
TL::Item(S("E" | SIGMA_UPPER | "sigma", _), list) => {
|
||||
if !alphabet.is_empty() {
|
||||
ctx.emit_error("alphabet already set", span);
|
||||
}
|
||||
let Some(list) = list.expect_set(ctx) else {
|
||||
continue;
|
||||
};
|
||||
for item in list {
|
||||
let Some(ident) = item.expect_ident(ctx) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if ident.chars().count() != 1 {
|
||||
ctx.emit_error("letter cannot be longer than one char", item.1);
|
||||
}
|
||||
|
||||
if alphabet
|
||||
.insert(Letter(ident), LetterInfo { definition: item.1 })
|
||||
.is_some()
|
||||
{
|
||||
ctx.emit_error("letter redefined", item.1);
|
||||
}
|
||||
}
|
||||
if list.is_empty() {
|
||||
ctx.emit_error("alphabet cannot be empty", span);
|
||||
}
|
||||
}
|
||||
TL::Item(S("F", _), list) => {
|
||||
if !final_states.is_empty() {
|
||||
ctx.emit_error("final states already set", span);
|
||||
}
|
||||
let Some(list) = list.expect_set(ctx) else {
|
||||
continue;
|
||||
};
|
||||
for item in list {
|
||||
let Some(ident) = item.expect_ident(ctx) else {
|
||||
continue;
|
||||
};
|
||||
if states.contains_key(&State(ident)) {
|
||||
if final_states
|
||||
.insert(State(ident), StateInfo { definition: item.1 })
|
||||
.is_some()
|
||||
{
|
||||
ctx.emit_error("final state redefined", item.1);
|
||||
}
|
||||
} else {
|
||||
ctx.emit_error("final state not defined in set of states", item.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
TL::Item(S("I" | "q0", _), S(src, src_d)) => match src {
|
||||
ast::Item::Symbol(Sym::Ident(ident)) => {
|
||||
if initial_state.is_some() {
|
||||
ctx.emit_error("initial state already set", span);
|
||||
}
|
||||
if states.contains_key(&State(ident)) {
|
||||
initial_state = Some(State(ident))
|
||||
} else {
|
||||
ctx.emit_error("initial state symbol not defined as a state", src_d);
|
||||
}
|
||||
}
|
||||
_ => ctx.emit_error("expected ident", src_d),
|
||||
},
|
||||
TL::Item(S(name, dest_s), _) => {
|
||||
ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s);
|
||||
}
|
||||
|
||||
TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => {
|
||||
let list = list.set_weak();
|
||||
let Some((state, letter)) = tuple.as_ref().expect_fa_transition_function(ctx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !states.contains_key(&State(state.0)) {
|
||||
ctx.emit_error("transition state not defined as state", state.1);
|
||||
continue;
|
||||
};
|
||||
|
||||
let letter: Option<Letter<'_>> = match letter.0 {
|
||||
Sym::Epsilon => {
|
||||
if !options.epsilon_moves {
|
||||
ctx.emit_error("epsilon moves not permitted", letter.1);
|
||||
}
|
||||
None
|
||||
}
|
||||
Sym::Ident(val) => {
|
||||
if !alphabet.contains_key(&Letter(val)) {
|
||||
ctx.emit_error(
|
||||
"transition letter not defined in alphabet",
|
||||
letter.1,
|
||||
);
|
||||
}
|
||||
Some(Letter(val))
|
||||
}
|
||||
};
|
||||
|
||||
for item in list {
|
||||
let Some(next_state) = item.expect_ident(ctx) else {
|
||||
continue;
|
||||
};
|
||||
let next_state = Spanned(next_state, item.1);
|
||||
|
||||
if !states.contains_key(&State(next_state.0)) {
|
||||
ctx.emit_error("transition state not defined as state", next_state.1);
|
||||
continue;
|
||||
};
|
||||
|
||||
let entry: &mut _ = transitions
|
||||
.entry(TransitionFrom {
|
||||
letter,
|
||||
state: State(state.0),
|
||||
})
|
||||
.or_default();
|
||||
if !entry.is_empty() && !options.non_deterministic {
|
||||
ctx.emit_error("transition already defined for this starting point (non determinism not permitted)", item.1);
|
||||
}
|
||||
if !entry.insert(TransitionTo {
|
||||
state: State(next_state.0),
|
||||
|
||||
function: tuple.1,
|
||||
transition: item.1,
|
||||
}) {
|
||||
ctx.emit_warning("duplicate transition", item.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
||||
ctx.emit_error(
|
||||
format!(
|
||||
"unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'"
|
||||
),
|
||||
dest_s,
|
||||
);
|
||||
}
|
||||
|
||||
TL::ProductionRule(_, _) => {
|
||||
ctx.emit_error("unexpected production rule", span);
|
||||
}
|
||||
TL::Table() => ctx.emit_error("unexpected table", span),
|
||||
}
|
||||
self.compile_top_level(element, span);
|
||||
}
|
||||
|
||||
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.states_def.is_none() {
|
||||
self.ctx
|
||||
.emit_error_locless("states never defined")
|
||||
.emit_help_logless("add: Q = {...}");
|
||||
}
|
||||
|
||||
let initial_state = match initial_state {
|
||||
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 states.contains_key(&State("q0")) {
|
||||
ctx.emit_warning_locless("initial state not defined, defaulting to 'q0'");
|
||||
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 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() {
|
||||
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, ...}")
|
||||
.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(Fa {
|
||||
initial_state,
|
||||
states,
|
||||
alphabet,
|
||||
final_states,
|
||||
transitions,
|
||||
states: self.states,
|
||||
alphabet: self.alphabet,
|
||||
final_states: self.final_states,
|
||||
transitions: self.transitions,
|
||||
})
|
||||
}
|
||||
|
||||
fn compile_top_level(&mut self, element: 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(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(name, 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) => {
|
||||
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<ast::Item<'a>>, 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_alphabet(&mut self, list: Spanned<ast::Item<'a>>, 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<ast::Item<'a>>, 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<ast::Item<'a>>,
|
||||
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_transition_function(
|
||||
&mut self,
|
||||
args: Spanned<ast::Tuple<'a>>,
|
||||
list: Spanned<ast::Item<'a>>,
|
||||
) {
|
||||
let list = list.set_weak();
|
||||
let Some((state, letter)) = args.as_ref().expect_fa_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;
|
||||
};
|
||||
|
||||
let letter: Option<Letter<'_>> = 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) = item.expect_ident(self.ctx) else {
|
||||
continue;
|
||||
};
|
||||
let next_state = Spanned(next_state, item.1);
|
||||
|
||||
if !self.states.contains_key(&State(next_state.0)) {
|
||||
self.ctx
|
||||
.emit_error("transition state not defined as state", next_state.1);
|
||||
continue;
|
||||
};
|
||||
|
||||
let entry: &mut _ = self
|
||||
.transitions
|
||||
.entry(TransitionFrom {
|
||||
letter,
|
||||
state: State(state.0),
|
||||
})
|
||||
.or_default();
|
||||
if let Some(entry) = entry.iter().next()
|
||||
&& !self.options.non_deterministic
|
||||
{
|
||||
self.ctx.emit_error("transition already defined for this starting point (non determinism not permitted)", item.1)
|
||||
.emit_info("previously defined here", entry.transition);
|
||||
}
|
||||
if let Some(previous) = entry.replace(TransitionTo {
|
||||
state: State(next_state.0),
|
||||
|
||||
function: args.1,
|
||||
transition: item.1,
|
||||
}) {
|
||||
self.ctx
|
||||
.emit_warning("duplicate transition", item.1)
|
||||
.emit_info("previously defined here", previous.transition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Spanned<&ast::Tuple<'a>> {
|
||||
|
|
@ -271,7 +387,12 @@ impl<'a> Spanned<&ast::Tuple<'a>> {
|
|||
] => {
|
||||
return Some((Spanned(state, *state_span), Spanned(*letter, *letter_span)));
|
||||
}
|
||||
_ => ctx.emit_error("expected FA transition function (ident, ident|~)", self.1),
|
||||
_ => {
|
||||
_ = ctx.emit_error(
|
||||
"expected FA transition function (state, letter|epsilon)",
|
||||
self.1,
|
||||
)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,31 +14,31 @@ pub struct Options {
|
|||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize), 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(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(transparent))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
|
||||
pub struct Letter<'a>(pub &'a str);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct StateInfo {
|
||||
pub definition: Span,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SymbolInfo {
|
||||
pub definition: Span,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct LetterInfo {
|
||||
pub definition: Span,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ use std::collections::HashSet;
|
|||
|
||||
use super::*;
|
||||
|
||||
use crate::loader::{
|
||||
Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned,
|
||||
ast::{self, Symbol as Sym},
|
||||
};
|
||||
use crate::{delta_lower, 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))]
|
||||
|
|
@ -90,7 +89,7 @@ impl<'a> Pda<'a> {
|
|||
ctx.emit_error("states cannot be empty", span);
|
||||
}
|
||||
}
|
||||
TL::Item(S("E" | SIGMA_UPPER | "sigma", _), list) => {
|
||||
TL::Item(S(sigma_upper!(pat), _), list) => {
|
||||
if !alphabet.is_empty() {
|
||||
ctx.emit_error("alphabet already set", span);
|
||||
}
|
||||
|
|
@ -142,7 +141,7 @@ impl<'a> Pda<'a> {
|
|||
}
|
||||
final_states = Some(map);
|
||||
}
|
||||
TL::Item(S("T" | GAMMA_UPPER | "gamma", _), list) => {
|
||||
TL::Item(S(gamma_upper!(pat), _), list) => {
|
||||
if !symbols.is_empty() {
|
||||
ctx.emit_error("stack symbols already set", span);
|
||||
}
|
||||
|
|
@ -166,7 +165,7 @@ impl<'a> Pda<'a> {
|
|||
ctx.emit_error("stack symbols cannot be empty", span);
|
||||
}
|
||||
}
|
||||
TL::Item(S("I" | "q0", _), S(src, src_d)) => match src {
|
||||
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);
|
||||
|
|
@ -177,9 +176,9 @@ impl<'a> Pda<'a> {
|
|||
ctx.emit_error("initial state symbol not defined as a state", src_d);
|
||||
}
|
||||
}
|
||||
_ => ctx.emit_error("expected ident", src_d),
|
||||
_ => _ = ctx.emit_error("expected ident", src_d),
|
||||
},
|
||||
TL::Item(S("S" | "z0", _), S(src, src_d)) => match src {
|
||||
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);
|
||||
|
|
@ -193,13 +192,13 @@ impl<'a> Pda<'a> {
|
|||
);
|
||||
}
|
||||
}
|
||||
_ => ctx.emit_error("expected ident", src_d),
|
||||
_ => _ = ctx.emit_error("expected ident", src_d),
|
||||
},
|
||||
TL::Item(S(name, dest_s), _) => {
|
||||
ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s);
|
||||
ctx.emit_error(format!("unknown item {name:?}, expected states, alphabet, symbols, final states, initial state, initial stack"), dest_s);
|
||||
}
|
||||
|
||||
TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => {
|
||||
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)
|
||||
|
|
@ -219,7 +218,7 @@ impl<'a> Pda<'a> {
|
|||
};
|
||||
|
||||
let letter: Option<Letter<'_>> = match letter.0 {
|
||||
Sym::Epsilon => {
|
||||
Sym::Epsilon(_) => {
|
||||
if !options.epsilon_moves {
|
||||
ctx.emit_error("epsilon moves not permitted", letter.1);
|
||||
}
|
||||
|
|
@ -253,7 +252,7 @@ impl<'a> Pda<'a> {
|
|||
.iter()
|
||||
.rev()
|
||||
.filter_map(|symbol| {
|
||||
if matches!(symbol.0, ast::Item::Symbol(Sym::Epsilon)) {
|
||||
if matches!(symbol.0, ast::Item::Symbol(Sym::Epsilon(_))) {
|
||||
return None;
|
||||
}
|
||||
let ident = symbol.expect_ident(ctx)?;
|
||||
|
|
@ -290,7 +289,7 @@ impl<'a> Pda<'a> {
|
|||
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
||||
ctx.emit_error(
|
||||
format!(
|
||||
"unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'"
|
||||
"unknown function {name:?}, expected transition function ( {} )", delta_lower!(str)
|
||||
),
|
||||
dest_s,
|
||||
);
|
||||
|
|
@ -299,7 +298,7 @@ impl<'a> Pda<'a> {
|
|||
TL::ProductionRule(_, _) => {
|
||||
ctx.emit_error("unexpected production rule", span);
|
||||
}
|
||||
TL::Table() => ctx.emit_error("unexpected table", span),
|
||||
TL::Table() => _ = ctx.emit_error("unexpected table", span),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -318,14 +317,14 @@ impl<'a> Pda<'a> {
|
|||
let initial_stack = match initial_stack {
|
||||
Some(some) => some,
|
||||
None => {
|
||||
if symbols.contains_key(&Symbol("z0")) {
|
||||
if symbols.contains_key(&Symbol("Z0")) {
|
||||
ctx.emit_warning_locless(
|
||||
"initial stack symbol not defined, defaulting to 'z0'",
|
||||
"initial stack symbol not defined, defaulting to 'Z0'",
|
||||
);
|
||||
} else {
|
||||
ctx.emit_error_locless("initial stack symbol not defined");
|
||||
}
|
||||
Symbol("z0")
|
||||
Symbol("Z0")
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -374,8 +373,8 @@ impl<'a, 'b> Spanned<&'b ast::Tuple<'a>> {
|
|||
Spanned(symbol, *symbol_span),
|
||||
));
|
||||
}
|
||||
_ => ctx.emit_error(
|
||||
"expected PDA transition function (ident, ident|~, ident)",
|
||||
_ => _ = ctx.emit_error(
|
||||
"expected PDA transition function (state, letter|epsilon, symbol)",
|
||||
self.1,
|
||||
),
|
||||
}
|
||||
|
|
@ -392,7 +391,7 @@ impl<'a, 'b> Spanned<&'b ast::Tuple<'a>> {
|
|||
] => {
|
||||
return Some((Spanned(state, *state_span), list.list_weak()));
|
||||
}
|
||||
_ => ctx.emit_error("expected PDA transition (ident, item|[item])", self.1),
|
||||
_ => _ = ctx.emit_error("expected PDA transition (state, symbol|[symbol])", self.1),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ use std::collections::HashSet;
|
|||
|
||||
use super::*;
|
||||
|
||||
use crate::loader::{
|
||||
Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned,
|
||||
ast::{self, Symbol as Sym},
|
||||
};
|
||||
use crate::{delta_lower, 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))]
|
||||
|
|
@ -118,7 +117,7 @@ impl<'a> Tm<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
TL::Item(S("T" | GAMMA_UPPER | "gamma", _), list) => {
|
||||
TL::Item(S(gamma_upper!(pat), _), list) => {
|
||||
if !symbols.is_empty() {
|
||||
ctx.emit_error("tape symbols already set", span);
|
||||
}
|
||||
|
|
@ -142,7 +141,7 @@ impl<'a> Tm<'a> {
|
|||
ctx.emit_error("tape symbols cannot be empty", span);
|
||||
}
|
||||
}
|
||||
TL::Item(S("I" | "q0", _), S(src, src_d)) => match src {
|
||||
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);
|
||||
|
|
@ -153,9 +152,9 @@ impl<'a> Tm<'a> {
|
|||
ctx.emit_error("initial state symbol not defined as a state", src_d);
|
||||
}
|
||||
}
|
||||
_ => ctx.emit_error("expected ident", src_d),
|
||||
_ => _ = ctx.emit_error("expected ident", src_d),
|
||||
},
|
||||
TL::Item(S("S" | "z0", _), S(src, src_d)) => match src {
|
||||
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);
|
||||
|
|
@ -169,13 +168,13 @@ impl<'a> Tm<'a> {
|
|||
);
|
||||
}
|
||||
}
|
||||
_ => ctx.emit_error("expected ident", src_d),
|
||||
_ => _ = ctx.emit_error("expected ident", src_d),
|
||||
},
|
||||
TL::Item(S(name, dest_s), _) => {
|
||||
ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s);
|
||||
ctx.emit_error(format!("unknown item {name:?}, expected states, symbols, final states, initial state, blank symbol"), dest_s);
|
||||
}
|
||||
|
||||
TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => {
|
||||
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)
|
||||
|
|
@ -229,9 +228,9 @@ impl<'a> Tm<'a> {
|
|||
}
|
||||
}
|
||||
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
|
||||
ctx.emit_error(
|
||||
ctx.emit_error(
|
||||
format!(
|
||||
"unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'"
|
||||
"unknown function {name:?}, expected transition function ( {} )", delta_lower!(str)
|
||||
),
|
||||
dest_s,
|
||||
);
|
||||
|
|
@ -240,7 +239,7 @@ impl<'a> Tm<'a> {
|
|||
TL::ProductionRule(_, _) => {
|
||||
ctx.emit_error("unexpected production rule", span);
|
||||
}
|
||||
TL::Table() => ctx.emit_error("unexpected table", span),
|
||||
TL::Table() => _ = ctx.emit_error("unexpected table", span),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -303,7 +302,7 @@ impl<'a> Spanned<&ast::Tuple<'a>> {
|
|||
] => {
|
||||
return Some((Spanned(state, *state_span), Spanned(*tape, *tape_span)));
|
||||
}
|
||||
_ => ctx.emit_error("expected TM transition function (ident, ident)", self.1),
|
||||
_ => _ = ctx.emit_error("expected TM transition function (state, symbol)", self.1),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -321,7 +320,7 @@ impl<'a> Spanned<&ast::Tuple<'a>> {
|
|||
let direction = match direction {
|
||||
ast::Symbol::Ident("left" | "L" | "<") => Direction::Left,
|
||||
ast::Symbol::Ident("right" | "R" | ">") => Direction::Right,
|
||||
ast::Symbol::Epsilon | ast::Symbol::Ident("~") => Direction::None,
|
||||
ast::Symbol::Epsilon(_) | ast::Symbol::Ident("~") => Direction::None,
|
||||
ast::Symbol::Ident(ident) => {
|
||||
ctx.emit_error(
|
||||
format!("invalid direction specified '{ident}'"),
|
||||
|
|
@ -336,8 +335,8 @@ impl<'a> Spanned<&ast::Tuple<'a>> {
|
|||
Spanned(direction, *direction_span),
|
||||
));
|
||||
}
|
||||
_ => ctx.emit_error(
|
||||
"expected TM transition function (ident, ident, ident)",
|
||||
_ => _ = ctx.emit_error(
|
||||
"expected TM transition function (state, symbol, direction)",
|
||||
self.1,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,36 @@
|
|||
pub mod automatan;
|
||||
pub mod loader;
|
||||
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! dual_struct_serde {
|
||||
($({$(#[$serde_specific:meta])*})?
|
||||
$(#[$struct_meta:meta])*
|
||||
$vis:vis struct $Name:ident $(<$($gen:tt),*>)?
|
||||
{
|
||||
$(
|
||||
$(#[$field_meta:meta])*
|
||||
$fvis:vis $fname:ident : $fty:ty
|
||||
),* $(,)?
|
||||
}
|
||||
) => {
|
||||
#[cfg(feature = "serde")]
|
||||
$(#[$struct_meta])*
|
||||
$( $(#[$serde_specific])* )?
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
$vis struct $Name $(<$($gen)*>)? {
|
||||
$(
|
||||
$(#[$field_meta])*
|
||||
$fvis $fname: $fty
|
||||
),*
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "serde"))]
|
||||
$(#[$struct_meta])*
|
||||
$vis struct $Name $(<$($gen)*>)? {
|
||||
$(
|
||||
$fvis $fname: $fty
|
||||
),*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ pub struct Tuple<'a>(pub Vec<Spanned<Item<'a>>>);
|
|||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Symbol<'a> {
|
||||
Epsilon,
|
||||
Epsilon(&'a str),
|
||||
Ident(&'a str),
|
||||
}
|
||||
|
||||
|
|
@ -62,14 +62,14 @@ pub enum TopLevel<'a> {
|
|||
Table(),
|
||||
}
|
||||
|
||||
use crate::loader::Context;
|
||||
use crate::loader::{Context, log::LogSink};
|
||||
|
||||
impl<'a> Spanned<Item<'a>> {
|
||||
pub fn expect_symbol(&self, ctx: &mut Context<'a>) -> Option<Symbol<'a>> {
|
||||
match &self.0 {
|
||||
Item::Symbol(sym) => return Some(*sym),
|
||||
Item::Tuple(_) => ctx.emit_error("expected ident found tuple", self.1),
|
||||
Item::List(_) => ctx.emit_error("expected ident found list", self.1),
|
||||
Item::Tuple(_) => _ = ctx.emit_error("expected ident found tuple", self.1),
|
||||
Item::List(_) => _ = ctx.emit_error("expected ident found list", self.1),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
@ -77,18 +77,18 @@ impl<'a> Spanned<Item<'a>> {
|
|||
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::Tuple(_) => ctx.emit_error("expected ident found tuple", self.1),
|
||||
Item::List(_) => ctx.emit_error("expected ident found list", 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),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn expect_set(&self, ctx: &mut Context<'a>) -> Option<&[Spanned<Item<'a>>]> {
|
||||
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::Tuple(_) => ctx.emit_error("expected set found tuple", 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),
|
||||
}
|
||||
None
|
||||
|
|
@ -96,9 +96,9 @@ impl<'a> Spanned<Item<'a>> {
|
|||
|
||||
pub fn expect_list(&self, ctx: &mut Context<'a>) -> Option<&[Spanned<Item<'a>>]> {
|
||||
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::Tuple(_) => ctx.emit_error("expected list found tuple", 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),
|
||||
}
|
||||
None
|
||||
|
|
@ -120,10 +120,10 @@ impl<'a> Spanned<Item<'a>> {
|
|||
|
||||
pub fn expect_tuple(&self, ctx: &mut Context<'a>) -> Option<Spanned<&Tuple<'a>>> {
|
||||
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),
|
||||
Item::List(_) => _ = ctx.emit_error("expected tuple found list", self.1),
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,97 @@ use std::fmt::Display;
|
|||
|
||||
use crate::loader::Span;
|
||||
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct Logs {
|
||||
logs: Vec<LogEntry>,
|
||||
has_error: bool,
|
||||
}
|
||||
|
||||
pub trait LogSink {
|
||||
fn emit(&mut self, entry: LogEntry) -> &mut LogEntry;
|
||||
|
||||
fn emit_error_locless(&mut self, msg: impl Into<String>) -> &mut LogEntry {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: None,
|
||||
level: LogLevel::Error,
|
||||
child: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_error(&mut self, msg: impl Into<String>, span: Span) -> &mut LogEntry {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: Some(span),
|
||||
level: LogLevel::Error,
|
||||
child: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_warning(&mut self, msg: impl Into<String>, span: Span) -> &mut LogEntry {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: Some(span),
|
||||
level: LogLevel::Warning,
|
||||
child: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_warning_locless(&mut self, msg: impl Into<String>) -> &mut LogEntry {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: None,
|
||||
level: LogLevel::Warning,
|
||||
child: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_info(&mut self, msg: impl Into<String>, span: Span) -> &mut LogEntry {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: Some(span),
|
||||
level: LogLevel::Info,
|
||||
child: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_info_logless(&mut self, msg: impl Into<String>) -> &mut LogEntry {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: None,
|
||||
level: LogLevel::Info,
|
||||
child: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_help(&mut self, msg: impl Into<String>, span: Span) -> &mut LogEntry {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: Some(span),
|
||||
level: LogLevel::Help,
|
||||
child: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_help_logless(&mut self, msg: impl Into<String>) -> &mut LogEntry {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: None,
|
||||
level: LogLevel::Help,
|
||||
child: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LogSink for Logs {
|
||||
fn emit(&mut self, entry: LogEntry) -> &mut LogEntry {
|
||||
self.has_error |= matches!(entry.level, LogLevel::Error);
|
||||
self.logs.push(entry);
|
||||
self.logs.last_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Logs {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
|
@ -19,51 +105,6 @@ impl Logs {
|
|||
self.has_error
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, entry: LogEntry) {
|
||||
self.has_error |= matches!(entry.level, LogLevel::Error);
|
||||
self.logs.push(entry);
|
||||
}
|
||||
|
||||
pub fn emit_error_locless(&mut self, msg: impl Into<String>) {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: None,
|
||||
level: LogLevel::Error,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn emit_error(&mut self, msg: impl Into<String>, span: Span) {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: Some(span),
|
||||
level: LogLevel::Error,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn emit_warning(&mut self, msg: impl Into<String>, span: Span) {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: Some(span),
|
||||
level: LogLevel::Warning,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn emit_warning_locless(&mut self, msg: impl Into<String>) {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: None,
|
||||
level: LogLevel::Warning,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn emit_info(&mut self, msg: impl Into<String>, span: Span) {
|
||||
self.emit(LogEntry {
|
||||
message: msg.into(),
|
||||
span: Some(span),
|
||||
level: LogLevel::Info,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn displayable_with<'a>(
|
||||
&'a self,
|
||||
src: &'a str,
|
||||
|
|
@ -86,16 +127,27 @@ impl Default for Logs {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub enum LogLevel {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Help,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct LogEntry {
|
||||
pub message: String,
|
||||
pub span: Option<Span>,
|
||||
pub level: LogLevel,
|
||||
pub child: Option<Box<LogEntry>>,
|
||||
}
|
||||
|
||||
impl LogSink for LogEntry {
|
||||
fn emit(&mut self, entry: LogEntry) -> &mut LogEntry {
|
||||
self.child = Some(Box::new(entry));
|
||||
self.child.as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LogEntryDisplay<'a> {
|
||||
|
|
@ -109,76 +161,86 @@ impl<'a> Display for LogEntryDisplay<'a> {
|
|||
pub const BOLD: &str = "\x1b[1m";
|
||||
// pub const UNDERLINE: &str = "\x1b[4m";
|
||||
pub const RED: &str = "\x1b[31m";
|
||||
// pub const GREEN: &str = "\x1b[32m";
|
||||
pub const GREEN: &str = "\x1b[32m";
|
||||
pub const YELLOW: &str = "\x1b[33m";
|
||||
// pub const BLUE: &str = "\x1b[34m";
|
||||
pub const CYAN: &str = "\x1b[36m";
|
||||
|
||||
match self.entry.level {
|
||||
LogLevel::Info => write!(f, "{BOLD}{CYAN}info{RESET}{BOLD}: ")?,
|
||||
LogLevel::Warning => write!(f, "{BOLD}{YELLOW}warning{RESET}{BOLD}: ")?,
|
||||
LogLevel::Error => write!(f, "{BOLD}{RED}error{RESET}{BOLD}: ")?,
|
||||
}
|
||||
writeln!(f, "{}{RESET}", self.entry.message)?;
|
||||
let mut next_entry = Some(self.entry);
|
||||
|
||||
if let Some(span) = self.entry.span {
|
||||
let line_start = self.src.get(..=span.0).unwrap_or("").lines().count();
|
||||
let line_end = self.src.get(..span.1).unwrap_or("").lines().count();
|
||||
while let Some(entry) = next_entry {
|
||||
match entry.level {
|
||||
LogLevel::Help => write!(f, "{BOLD}{GREEN}help{RESET}{BOLD}: ")?,
|
||||
LogLevel::Info => write!(f, "{BOLD}{CYAN}info{RESET}{BOLD}: ")?,
|
||||
LogLevel::Warning => write!(f, "{BOLD}{YELLOW}warning{RESET}{BOLD}: ")?,
|
||||
LogLevel::Error => write!(f, "{BOLD}{RED}error{RESET}{BOLD}: ")?,
|
||||
}
|
||||
writeln!(f, "{}{RESET}", entry.message)?;
|
||||
|
||||
let padding = if line_end == 0 {1} else {line_end.ilog10() as usize};
|
||||
if let Some(span) = entry.span {
|
||||
let line_start = self.src.get(..=span.0).unwrap_or("").lines().count();
|
||||
let line_end = self.src.get(..span.1).unwrap_or("").lines().count();
|
||||
|
||||
let start = self
|
||||
.src
|
||||
.get(..span.0)
|
||||
.and_then(|s| s.rfind('\n'))
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or(0);
|
||||
let padding = if line_end == 0 {
|
||||
1
|
||||
} else {
|
||||
line_end.ilog10() as usize
|
||||
};
|
||||
|
||||
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 start = self
|
||||
.src
|
||||
.get(..span.0)
|
||||
.and_then(|s| s.rfind('\n'))
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut index = start;
|
||||
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' {
|
||||
write!(f, " ")?
|
||||
} else {
|
||||
write!(f, "{char}")?
|
||||
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("")
|
||||
.split_inclusive("\n")
|
||||
.enumerate()
|
||||
{
|
||||
write!(f, "{BOLD}{CYAN}{:>padding$}: {RESET}", i + line_start)?;
|
||||
for char in line.chars() {
|
||||
if char == '\t' {
|
||||
write!(f, " ")?
|
||||
} else {
|
||||
write!(f, "{char}")?
|
||||
}
|
||||
}
|
||||
}
|
||||
if !line.ends_with("\n") {
|
||||
writeln!(f)?;
|
||||
}
|
||||
write!(f, "{BOLD}{CYAN}")?;
|
||||
for _ in 0..padding + 3 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
for char in line.chars() {
|
||||
if (span.0..span.1).contains(&index) {
|
||||
write!(f, "~")?;
|
||||
} else {
|
||||
if !line.ends_with("\n") {
|
||||
writeln!(f)?;
|
||||
}
|
||||
write!(f, "{BOLD}{CYAN}")?;
|
||||
for _ in 0..padding + 3 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
index += char.len_utf8();
|
||||
for char in line.chars() {
|
||||
if (span.0..span.1).contains(&index) {
|
||||
write!(f, "~")?;
|
||||
} else {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
index += char.len_utf8();
|
||||
}
|
||||
write!(f, "{RESET}")?;
|
||||
index += '\n'.len_utf8();
|
||||
writeln!(f)?;
|
||||
}
|
||||
write!(f, "{RESET}")?;
|
||||
index += '\n'.len_utf8();
|
||||
writeln!(f)?;
|
||||
}
|
||||
next_entry = entry.child.as_deref()
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,23 +1,67 @@
|
|||
use crate::{automatan::*, loader::ast::TopLevel};
|
||||
use crate::{
|
||||
automatan::*,
|
||||
loader::{
|
||||
ast::TopLevel,
|
||||
log::{LogEntry, LogSink},
|
||||
},
|
||||
};
|
||||
|
||||
pub mod ast;
|
||||
pub mod lexer;
|
||||
pub mod log;
|
||||
pub mod parser;
|
||||
|
||||
pub const EPSILON_LOWER: &str = "Ɛ";
|
||||
pub const EPSILON_LOWER_MATH: &str = "𝛆";
|
||||
pub const DELTA_LOWER: &str = "δ";
|
||||
pub const SIGMA_UPPER: &str = "Σ";
|
||||
pub const GAMMA_UPPER: &str = "Γ";
|
||||
pub const GAMMA_LOWER: &str = "γ";
|
||||
#[macro_export]
|
||||
macro_rules! maker {
|
||||
(pat: $($pat:pat),*) => {
|
||||
$($pat)|*
|
||||
};
|
||||
(arr: $($expr:expr),*) => {
|
||||
[$($expr),*]
|
||||
};
|
||||
(str: $first:literal, $($remainder:literal),+) => {
|
||||
concat!($crate::maker!(str: $first), " | ", $crate::maker!(str: $($remainder),*))
|
||||
};
|
||||
(str: $first:literal) => {
|
||||
concat!("'",$first,"'")
|
||||
};
|
||||
}
|
||||
|
||||
pub const INITIAL_STATE: &str = "q0";
|
||||
pub const INITIAL_STACK: &str = "z0";
|
||||
pub const BLANK_SYMBOL: &str = "B";
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! epsilon {
|
||||
($ident: ident) => {
|
||||
$crate::maker!($ident: "epsilon","~", "Ɛ", "ε", "ϵ", "𝛆", "𝛜", "𝜀", "𝜖", "𝜺", "𝝐", "𝝴", "𝞊", "𝞮", "𝟄")
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delta_lower {
|
||||
($ident: ident) => {
|
||||
$crate::maker!($ident: "delta","D","d","ẟ","δ", "𝛅", "𝛿", "𝜹", "𝝳", "𝞭")
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! sigma_upper {
|
||||
($ident: ident) => {
|
||||
$crate::maker!($ident: "E","S", "sigma","Σ","𝚺", "𝛴", "𝜮", "𝝨", "𝞢")
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! gamma_upper {
|
||||
($ident: ident) => {
|
||||
$crate::maker!($ident: "T","G","gamma","Γ","Ⲅ", "𝚪", "𝛤", "𝜞", "𝝘", "𝞒")
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct Span(
|
||||
#[cfg_attr(feature = "serde", serde(rename = "start"))] pub usize,
|
||||
#[cfg_attr(feature = "serde", serde(rename = "end"))] pub usize,
|
||||
);
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Span(pub usize, pub usize);
|
||||
impl Span {
|
||||
pub fn join(&self, end: Span) -> Span {
|
||||
Span(self.0, end.1)
|
||||
|
|
@ -41,6 +85,12 @@ pub struct Context<'a> {
|
|||
src: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> LogSink for Context<'a> {
|
||||
fn emit(&mut self, entry: log::LogEntry) -> &mut LogEntry {
|
||||
self.logs.emit(entry)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
pub fn new(src: &'a str) -> Self {
|
||||
Self {
|
||||
|
|
@ -61,30 +111,6 @@ impl<'a> Context<'a> {
|
|||
Span(self.src.len(), self.src.len())
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, entry: log::LogEntry) {
|
||||
self.logs.emit(entry);
|
||||
}
|
||||
|
||||
pub fn emit_error_locless(&mut self, msg: impl Into<String>) {
|
||||
self.logs.emit_error_locless(msg);
|
||||
}
|
||||
|
||||
pub fn emit_error(&mut self, msg: impl Into<String>, span: Span) {
|
||||
self.logs.emit_error(msg, span);
|
||||
}
|
||||
|
||||
pub fn emit_warning(&mut self, msg: impl Into<String>, span: Span) {
|
||||
self.logs.emit_warning(msg, span);
|
||||
}
|
||||
|
||||
pub fn emit_warning_locless(&mut self, msg: impl Into<String>) {
|
||||
self.logs.emit_warning_locless(msg);
|
||||
}
|
||||
|
||||
pub fn emit_info(&mut self, msg: impl Into<String>, span: Span) {
|
||||
self.logs.emit_info(msg, span);
|
||||
}
|
||||
|
||||
pub fn contains_errors(&self) -> bool {
|
||||
self.logs.contains_errors()
|
||||
}
|
||||
|
|
@ -127,11 +153,13 @@ pub fn parse_universal<'a>(ctx: &mut Context<'a>) -> Option<Machine<'a>> {
|
|||
(item.expect_ident(ctx)?, span)
|
||||
}
|
||||
Some(S(_, span)) => {
|
||||
ctx.emit_error("expected type=<type> as first item", span);
|
||||
ctx.emit_error("expected type=<type> as first item", span)
|
||||
.emit_help_logless("add: type = ...");
|
||||
return None;
|
||||
}
|
||||
None => {
|
||||
ctx.emit_error("expected type=<type> as first item", ctx.eof());
|
||||
ctx.emit_error("expected type=<type> as first item", ctx.eof())
|
||||
.emit_help_logless("add: type = ...");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
|
@ -164,8 +192,8 @@ pub fn parse_universal<'a>(ctx: &mut Context<'a>) -> Option<Machine<'a>> {
|
|||
};
|
||||
|
||||
Some(match parse_type(items.next(), ctx)? {
|
||||
Type::Dfa => Machine::Fa(fa::Fa::parse(items, ctx, D)?),
|
||||
Type::Nfa => Machine::Fa(fa::Fa::parse(items, ctx, N)?),
|
||||
Type::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)?),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use crate::epsilon;
|
||||
use crate::loader::log::LogSink;
|
||||
use crate::loader::{Context, Span};
|
||||
|
||||
use super::lexer::Token as T;
|
||||
|
|
@ -49,7 +51,7 @@ impl<'a, 'b> Parser<'a, 'b> {
|
|||
return self.peek;
|
||||
}
|
||||
Some(S(Ok(ok), r)) => return Some(S(ok, r)),
|
||||
Some(S(Err(err), span)) => self.ctx.emit_error(format!("lexer: {err:?}"), span),
|
||||
Some(S(Err(err), span)) => _ = self.ctx.emit_error(format!("lexer: {err:?}"), span),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
|
|
@ -90,9 +92,8 @@ impl<'a, 'b> Parser<'a, 'b> {
|
|||
|
||||
fn parse_as_symbol(&mut self, tok: S<T<'a>>) -> S<Symbol<'a>> {
|
||||
match tok {
|
||||
S(T::Tilde, r) => S(Symbol::Epsilon, r),
|
||||
S(T::Ident("epsilon"), r) => S(Symbol::Epsilon, r),
|
||||
S(T::Ident(super::EPSILON_LOWER), r) => S(Symbol::Epsilon, r),
|
||||
S(T::Tilde, r) => S(Symbol::Epsilon("~"), 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(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// deno-lint-ignore-file
|
||||
|
||||
import type { Machine } 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";
|
||||
|
|
@ -73,13 +74,16 @@ type AppEvents = {
|
|||
"automata/sim/after_step": { simulation: Sim, result: SimStepResult };
|
||||
"automata/update": { automaton: Machine };
|
||||
|
||||
"controls/physics": {enabled: boolean},
|
||||
"controls/reset_network": void,
|
||||
"example/selected": {example: Example};
|
||||
|
||||
"controls/editor/set_text": {text: string};
|
||||
|
||||
"controls/step_simulation": void,
|
||||
"controls/reload_simulation": void,
|
||||
"controls/clear_simulation": void,
|
||||
"controls/vis/physics": {enabled: boolean};
|
||||
"controls/vis/reset_network": void;
|
||||
|
||||
"controls/sim/step": void;
|
||||
"controls/sim/reload": void;
|
||||
"controls/sim/clear": void;
|
||||
|
||||
"theme/update": void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,59 +15,6 @@ const speedLabel = document.getElementById("speedSimLabel") as HTMLSpanElement;
|
|||
const reloadSimBtn = document.getElementById("reloadSim") as HTMLButtonElement;
|
||||
const clearSimBtn = document.getElementById("clearSim") as HTMLButtonElement;
|
||||
|
||||
bus.on("controls/physics", ({ enabled }) => {
|
||||
togglePhysicsBtn.classList.toggle("active", enabled);
|
||||
togglePhysicsBtn.textContent = enabled ? "Physics: ON" : "Physics: OFF";
|
||||
});
|
||||
|
||||
togglePhysicsBtn.onclick = () => {
|
||||
const enabled = !togglePhysicsBtn.classList.contains("active");
|
||||
bus.emit("controls/physics", { enabled });
|
||||
};
|
||||
|
||||
bus.emit("controls/physics", {
|
||||
enabled: togglePhysicsBtn.classList.contains("active"),
|
||||
});
|
||||
|
||||
resetLayoutBtn.onclick = () => bus.emit("controls/reset_network", undefined);
|
||||
|
||||
clearSimBtn.onclick = () => bus.emit("controls/clear_simulation", undefined);
|
||||
|
||||
stepBtn.onclick = () => {
|
||||
bus.emit("controls/step_simulation", undefined);
|
||||
};
|
||||
|
||||
reloadSimBtn.onclick = () => bus.emit("controls/reload_simulation", undefined);
|
||||
|
||||
function updateButtons() {
|
||||
stepBtn.disabled = !simulation_active || running;
|
||||
playPauseBtn.disabled = !simulation_active;
|
||||
clearSimBtn.disabled = !simulation_active;
|
||||
}
|
||||
|
||||
bus.on("controls/reload_simulation", (_) => {
|
||||
if (running) setRunning(false);
|
||||
updateButtons();
|
||||
});
|
||||
|
||||
bus.on("automata/sim/update", ({ simulation }) => {
|
||||
simulation_active = !!simulation;
|
||||
if (!simulation) {
|
||||
if (running) setRunning(false);
|
||||
}
|
||||
updateButtons();
|
||||
});
|
||||
|
||||
bus.on("automata/sim/after_step", ({ result }) => {
|
||||
if (result !== "pending") {
|
||||
if (running) setRunning(false);
|
||||
}
|
||||
});
|
||||
|
||||
let simulation_active = false;
|
||||
let running = false;
|
||||
let timer: number | null = null;
|
||||
|
||||
// speed slider is "steps per second"
|
||||
function getStepsPerSecond() {
|
||||
return Math.max(1, Math.min(60, Number(speedSlider.value) || 10));
|
||||
|
|
@ -77,39 +24,82 @@ function updateSpeedUI() {
|
|||
}
|
||||
updateSpeedUI();
|
||||
|
||||
speedSlider.addEventListener("input", () => {
|
||||
updateSpeedUI();
|
||||
if (running) restartTimer();
|
||||
});
|
||||
class Controls {
|
||||
static simulation_active = false;
|
||||
static running = false;
|
||||
static timer: number | null = null;
|
||||
|
||||
function stopTimer() {
|
||||
if (timer !== null) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
static updateButtons() {
|
||||
stepBtn.disabled = !Controls.simulation_active || Controls.running;
|
||||
playPauseBtn.disabled = !Controls.simulation_active;
|
||||
clearSimBtn.disabled = !Controls.simulation_active;
|
||||
}
|
||||
static setRunning(on: boolean) {
|
||||
Controls.running = on;
|
||||
playPauseBtn.textContent = Controls.running ? "⏸ Pause" : "▶ Play";
|
||||
playPauseBtn.classList.toggle("btn-primary", !Controls.running);
|
||||
playPauseBtn.classList.toggle("btn-secondary", Controls.running);
|
||||
|
||||
if (Controls.running) Controls.restartTimer();
|
||||
else Controls.stopTimer();
|
||||
Controls.updateButtons();
|
||||
}
|
||||
static stop() {
|
||||
if (Controls.running) Controls.setRunning(false);
|
||||
}
|
||||
static stopTimer() {
|
||||
if (Controls.timer !== null) {
|
||||
clearInterval(Controls.timer);
|
||||
Controls.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
static restartTimer() {
|
||||
Controls.stopTimer();
|
||||
const sps = getStepsPerSecond();
|
||||
const intervalMs = Math.round(1000 / sps);
|
||||
|
||||
Controls.timer = globalThis.window.setInterval(() => {
|
||||
bus.emit("controls/sim/step", undefined);
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
static {
|
||||
speedSlider.addEventListener("input", () => {
|
||||
updateSpeedUI();
|
||||
if (Controls.running) Controls.restartTimer();
|
||||
});
|
||||
playPauseBtn.onclick = () => Controls.setRunning(!Controls.running);
|
||||
resetLayoutBtn.onclick = () =>
|
||||
bus.emit("controls/vis/reset_network", undefined);
|
||||
clearSimBtn.onclick = () => bus.emit("controls/sim/clear", undefined);
|
||||
stepBtn.onclick = () => bus.emit("controls/sim/step", undefined);
|
||||
reloadSimBtn.onclick = () => bus.emit("controls/sim/reload", undefined);
|
||||
togglePhysicsBtn.onclick = () => {
|
||||
const enabled = !togglePhysicsBtn.classList.contains("active");
|
||||
bus.emit("controls/vis/physics", { enabled });
|
||||
};
|
||||
|
||||
bus.on("controls/vis/physics", ({ enabled }) => {
|
||||
togglePhysicsBtn.classList.toggle("active", enabled);
|
||||
togglePhysicsBtn.textContent = enabled ? "Physics: ON" : "Physics: OFF";
|
||||
});
|
||||
|
||||
bus.on("controls/sim/reload", (_) => {
|
||||
if (Controls.running) Controls.setRunning(false);
|
||||
});
|
||||
|
||||
bus.on("automata/sim/update", ({ simulation }) => {
|
||||
Controls.simulation_active = !!simulation;
|
||||
if (!simulation) Controls.stop();
|
||||
});
|
||||
|
||||
bus.on("automata/sim/after_step", ({ result }) => {
|
||||
if (result !== "pending") Controls.stop();
|
||||
});
|
||||
|
||||
bus.emit("controls/vis/physics", {
|
||||
enabled: togglePhysicsBtn.classList.contains("active"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function restartTimer() {
|
||||
stopTimer();
|
||||
const sps = getStepsPerSecond();
|
||||
const intervalMs = Math.round(1000 / sps);
|
||||
|
||||
timer = globalThis.window.setInterval(() => {
|
||||
bus.emit("controls/step_simulation", undefined);
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
function setRunning(on: boolean) {
|
||||
running = on;
|
||||
playPauseBtn.textContent = running ? "⏸ Pause" : "▶ Play";
|
||||
playPauseBtn.classList.toggle("btn-primary", !running);
|
||||
playPauseBtn.classList.toggle("btn-secondary", running);
|
||||
|
||||
// Disable step while running (optional, but feels nice)
|
||||
stepBtn.disabled = running;
|
||||
|
||||
if (running) restartTimer();
|
||||
else stopTimer();
|
||||
}
|
||||
|
||||
playPauseBtn.onclick = () => setRunning(!running);
|
||||
|
|
|
|||
|
|
@ -1,67 +1,71 @@
|
|||
// deno-lint-ignore-file
|
||||
|
||||
import {
|
||||
EditorView,
|
||||
keymap,
|
||||
hoverTooltip,
|
||||
Decoration,
|
||||
lineNumbers,
|
||||
EditorView,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightActiveLine
|
||||
hoverTooltip,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
} from "npm:@codemirror/view";
|
||||
|
||||
import { EditorState, StateField, Text } from "npm:@codemirror/state";
|
||||
import { defaultKeymap, history, historyKeymap } from "npm:@codemirror/commands";
|
||||
import {
|
||||
defaultKeymap,
|
||||
history,
|
||||
historyKeymap,
|
||||
} from "npm:@codemirror/commands";
|
||||
import { bracketMatching, indentOnInput } from "npm:@codemirror/language";
|
||||
import { closeBrackets } from "npm:@codemirror/autocomplete";
|
||||
|
||||
import wasm from "./wasm.ts";
|
||||
|
||||
import wasm from "./wasm.ts"
|
||||
|
||||
import { sharedText } from "./share.ts";
|
||||
import { Share } from "./share.ts";
|
||||
import { examples } from "./examples.ts";
|
||||
import { bus } from "./bus.ts";
|
||||
|
||||
|
||||
function tokenize(text: string) {
|
||||
function tokenize(text: string): wasm.Tok[] {
|
||||
try {
|
||||
return wasm.lex(text);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return []
|
||||
console.log(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function compile(text: string): wasm.CompileResult {
|
||||
function compile(
|
||||
text: string,
|
||||
): { log: wasm.CompileLog[]; ansi_log: string; machine: string | undefined } {
|
||||
try {
|
||||
return wasm.compile(text);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
// @ts-expect-error wasm defines extra cleanup
|
||||
return {log: [], log_formatted: "", graph: ""};
|
||||
return { log: [], ansi_log: "", machine: "" };
|
||||
}
|
||||
}
|
||||
|
||||
const eventBusConnection = StateField.define({
|
||||
create(state) {
|
||||
const text = state.doc.toString();
|
||||
bus.emit("editor/change", {text, doc: state.doc});
|
||||
bus.emit("editor/change", { text, doc: state.doc });
|
||||
return buildAnalysis(text, state.doc);
|
||||
},
|
||||
update(value, tr) {
|
||||
if (!tr.docChanged) return value;
|
||||
const text = tr.state.doc.toString();
|
||||
bus.emit("editor/change", {text, doc: state.doc});
|
||||
bus.emit("editor/change", { text, doc: state.doc });
|
||||
return buildAnalysis(text, tr.state.doc);
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
|
||||
});
|
||||
|
||||
function buildAnalysis(text: string, doc: Text) {
|
||||
save(text);
|
||||
const tokens = tokenize(text);
|
||||
const { log, ansi_log, machine } = compile(text);
|
||||
|
||||
bus.emit("compiled", {log, ansi_log, machine})
|
||||
bus.emit("compiled", { log, ansi_log, machine });
|
||||
|
||||
const marks = [];
|
||||
const docLen = doc.length;
|
||||
|
|
@ -88,7 +92,9 @@ function buildAnalysis(text: string, doc: Text) {
|
|||
marks.push(Decoration.mark({ class: cls }).range(start, end));
|
||||
} else {
|
||||
const end = Math.min(docLen, start + 1);
|
||||
if (end > start) marks.push(Decoration.mark({ class: cls }).range(start, end));
|
||||
if (end > start) {
|
||||
marks.push(Decoration.mark({ class: cls }).range(start, end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,8 +102,7 @@ function buildAnalysis(text: string, doc: Text) {
|
|||
return { tokens, log, ansi_log, deco };
|
||||
}
|
||||
|
||||
const tokenClass = (t: string) =>
|
||||
({
|
||||
const tokenClass = (t: string) => ({
|
||||
comment: "tok-comment",
|
||||
keyword: "tok-keyword",
|
||||
error: "tok-error",
|
||||
|
|
@ -113,7 +118,6 @@ const tokenClass = (t: string) =>
|
|||
rbracket: "rb-",
|
||||
}[t] || "tok-ident");
|
||||
|
||||
|
||||
function severityClass(sev: string) {
|
||||
const s = (sev || "error").toLowerCase();
|
||||
if (s === "warning") return "cm-diag-warning";
|
||||
|
|
@ -129,10 +133,16 @@ function sevRank(sev: string) {
|
|||
// ===================== Hover tooltip (uses cached diags) =====================
|
||||
const diagHover = hoverTooltip((view, pos) => {
|
||||
const { log } = view.state.field(eventBusConnection);
|
||||
const hits = log.filter((d) => d.start !== undefined && d.end !== undefined && pos >= d.start && pos <= d.end);
|
||||
const hits = log.filter((d) =>
|
||||
d.start !== undefined && d.end !== undefined && pos >= d.start &&
|
||||
pos <= d.end
|
||||
);
|
||||
if (hits.length === 0) return null;
|
||||
|
||||
const top = hits.reduce((a, b) => (sevRank(b.level) > sevRank(a.level) ? b : a), hits[0]);
|
||||
const top = hits.reduce(
|
||||
(a, b) => (sevRank(b.level) > sevRank(a.level) ? b : a),
|
||||
hits[0],
|
||||
);
|
||||
|
||||
return {
|
||||
pos,
|
||||
|
|
@ -144,8 +154,9 @@ const diagHover = hoverTooltip((view, pos) => {
|
|||
|
||||
const title = document.createElement("div");
|
||||
title.className = `tipTitle ${top.level}`;
|
||||
title.textContent =
|
||||
hits.length === 1 ? top.level.toUpperCase() : `${top.level.toUpperCase()} (${hits.length})`;
|
||||
title.textContent = hits.length === 1
|
||||
? top.level.toUpperCase()
|
||||
: `${top.level.toUpperCase()} (${hits.length})`;
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "tipBody";
|
||||
|
|
@ -162,24 +173,20 @@ const diagHover = hoverTooltip((view, pos) => {
|
|||
};
|
||||
});
|
||||
|
||||
function save(text: string){
|
||||
function save(text: string) {
|
||||
globalThis.localStorage.save = text;
|
||||
}
|
||||
|
||||
function getSaved(): string | undefined{
|
||||
function getSaved(): string | undefined {
|
||||
return globalThis.localStorage.save;
|
||||
}
|
||||
|
||||
export function setText(text: string){
|
||||
editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: text } });
|
||||
}
|
||||
|
||||
export function getText(): string{
|
||||
return editor.state.doc.toString()
|
||||
function defaultText(): string {
|
||||
return Share.sharedText() ?? getSaved() ?? examples[0].machine;
|
||||
}
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: "",
|
||||
doc: defaultText(),
|
||||
extensions: [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
|
|
@ -202,4 +209,17 @@ const editor = new EditorView({
|
|||
parent: document.getElementById("editor")!,
|
||||
});
|
||||
|
||||
bus.on("begin", _ => setText(sharedText() ?? getSaved() ?? examples[0].machine))
|
||||
bus.on(
|
||||
"begin",
|
||||
(_) => bus.emit("controls/editor/set_text", { text: defaultText() }),
|
||||
);
|
||||
|
||||
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 });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { setText } from "./editor.ts";
|
||||
import { bus } from "./bus.ts";
|
||||
|
||||
export type Category =
|
||||
| "Tutorial"
|
||||
|
|
@ -241,6 +241,6 @@ function buildExamplesDropdown(
|
|||
}
|
||||
|
||||
const selectEl = document.getElementById("exampleSelect") as HTMLSelectElement;
|
||||
buildExamplesDropdown(selectEl, examples, (picked) => {
|
||||
setText(picked.machine);
|
||||
buildExamplesDropdown(selectEl, examples, (example) => {
|
||||
bus.emit("example/selected", {example});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,41 +1,49 @@
|
|||
import { getText } from "./editor.ts";
|
||||
import { bus } from "./bus.ts";
|
||||
|
||||
const btn = document.getElementById("shareBtn")!;
|
||||
const toast = document.getElementById("shareToast")!;
|
||||
export class Share {
|
||||
private static readonly btn: HTMLButtonElement = document.getElementById(
|
||||
"shareBtn",
|
||||
)! as HTMLButtonElement;
|
||||
private static readonly toast: HTMLElement = document.getElementById(
|
||||
"shareToast",
|
||||
)!;
|
||||
|
||||
function generateShareLink() {
|
||||
return `${globalThis.window.location.href}?share=${encodeURIComponent(btoa(getText()))}`;
|
||||
}
|
||||
private static docText: string;
|
||||
private static shareText: string;
|
||||
|
||||
async function copy(text: string) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
}
|
||||
static {
|
||||
bus.on("editor/change", ({ text }) => Share.docText = text);
|
||||
|
||||
btn.addEventListener("click", async () => {
|
||||
await copy(generateShareLink());
|
||||
Share.btn.onclick = async (_) => {
|
||||
const link = `${globalThis.window.location.href}?share=${
|
||||
encodeURIComponent(btoa(Share.docText))
|
||||
}`;
|
||||
await navigator.clipboard.writeText(link);
|
||||
|
||||
toast.classList.remove("show");
|
||||
void toast.offsetWidth;
|
||||
toast.classList.add("show");
|
||||
});
|
||||
Share.toast.classList.remove("show");
|
||||
void Share.toast.offsetWidth;
|
||||
Share.toast.classList.add("show");
|
||||
};
|
||||
|
||||
|
||||
export function sharedText(): string|null {
|
||||
try{
|
||||
const url = new URL(globalThis.window.location.href);
|
||||
let text: string | null = url.searchParams.get("share");
|
||||
if (text !== null) {
|
||||
text = atob(text);
|
||||
url.searchParams.delete("share");
|
||||
globalThis.window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
url.pathname + url.search + url.hash
|
||||
);
|
||||
try {
|
||||
const url = new URL(globalThis.window.location.href);
|
||||
let text: string | null = url.searchParams.get("share");
|
||||
if (text !== null) {
|
||||
text = atob(text);
|
||||
url.searchParams.delete("share");
|
||||
globalThis.window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
url.pathname + url.search + url.hash,
|
||||
);
|
||||
Share.shareText = text;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
return text;
|
||||
}catch(e){
|
||||
console.log(e)
|
||||
}
|
||||
return null;
|
||||
|
||||
public static sharedText(): string | null {
|
||||
return Share.shareText;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { bus } from "./bus.ts";
|
||||
import {
|
||||
import type {
|
||||
Fa,
|
||||
Machine,
|
||||
parse_machine_from_json,
|
||||
Pda,
|
||||
State,
|
||||
Symbol,
|
||||
Tm,
|
||||
} from "./automata.ts";
|
||||
import {parse_machine_from_json} from "./automata.ts";
|
||||
|
||||
export type SimStepResult = "pending" | "accept" | "reject";
|
||||
export type Sim = FaSim | PdaSim | TmSim;
|
||||
|
|
@ -26,7 +26,7 @@ let automaton: Machine = {
|
|||
bus.on("compiled", ({ machine }) => {
|
||||
if (machine) {
|
||||
try {
|
||||
bus.emit("controls/clear_simulation", undefined);
|
||||
bus.emit("controls/sim/clear", undefined);
|
||||
automaton = parse_machine_from_json(machine);
|
||||
bus.emit("automata/update", { automaton });
|
||||
} catch (e) {
|
||||
|
|
@ -34,11 +34,11 @@ bus.on("compiled", ({ machine }) => {
|
|||
}
|
||||
}
|
||||
});
|
||||
bus.on("controls/clear_simulation", (_) => {
|
||||
bus.on("controls/sim/clear", (_) => {
|
||||
simulation = null;
|
||||
bus.emit("automata/sim/update", { simulation: null });
|
||||
});
|
||||
bus.on("controls/step_simulation", (_) => {
|
||||
bus.on("controls/sim/step", (_) => {
|
||||
if (simulation) {
|
||||
bus.emit("automata/sim/before_step", { simulation });
|
||||
bus.emit("automata/sim/after_step", {
|
||||
|
|
@ -51,10 +51,10 @@ const machineInput = document.getElementById("machineInput") as HTMLInputElement
|
|||
machineInput.addEventListener("input", () => bus.emit("automata/sim/update", {simulation: null}));
|
||||
machineInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
bus.emit("controls/reload_simulation", undefined)
|
||||
bus.emit("controls/sim/reload", undefined)
|
||||
}
|
||||
});
|
||||
bus.on("controls/reload_simulation", (_) => {
|
||||
bus.on("controls/sim/reload", (_) => {
|
||||
const input = machineInput.value;
|
||||
switch (automaton.type) {
|
||||
case "fa":
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import type { Sim } from "./simulation.ts";
|
|||
import type { Machine } from "./automata.ts";
|
||||
|
||||
|
||||
bus.on("controls/physics", ({enabled}) => {
|
||||
bus.on("controls/vis/physics", ({enabled}) => {
|
||||
network.setOptions({ physics: { enabled } });
|
||||
network.setOptions({edges: {smooth: enabled}});
|
||||
});
|
||||
bus.on("controls/reset_network", _ => {
|
||||
bus.on("controls/vis/reset_network", _ => {
|
||||
try {
|
||||
nodes.forEach((n) => {
|
||||
n.physics = true;
|
||||
|
|
@ -285,17 +285,16 @@ function createGraph(): vis.Network {
|
|||
nodes: {
|
||||
shape: "custom",
|
||||
size: 18,
|
||||
// // @ts-expect-error bad library
|
||||
// chosen: {
|
||||
// node: chosen_node,
|
||||
// },
|
||||
// @ts-expect-error bad library
|
||||
chosen: {
|
||||
node: chosen_node,
|
||||
},
|
||||
ctxRenderer: renderNode,
|
||||
},
|
||||
edges: {
|
||||
chosen: {
|
||||
// // @ts-expect-error bad library
|
||||
// edge: chosen_edge,
|
||||
// @ts-expect-error bad library
|
||||
edge: chosen_edge,
|
||||
},
|
||||
arrowStrikethrough: false,
|
||||
arrows: "to",
|
||||
|
|
@ -304,9 +303,8 @@ function createGraph(): vis.Network {
|
|||
);
|
||||
vis.DataSet;
|
||||
|
||||
network.on("doubleClick", (params: any) => {
|
||||
network.on("doubleClick", (params: {nodes: string[]}) => {
|
||||
for (const node_id of params.nodes) {
|
||||
// @ts-expect-error bad library
|
||||
const node: vis.Node = nodes.get(node_id)!;
|
||||
node.physics = !node.physics;
|
||||
nodes.update(node);
|
||||
|
|
@ -325,7 +323,7 @@ function renderNode({
|
|||
state: { selected, hover },
|
||||
style,
|
||||
label,
|
||||
}: {ctx: CanvasRenderingContext2D, id: string, x: number, y: number, state: {selected: boolean, hover: boolean}, style: any, 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();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use automata::loader::{self, Context, Span, Spanned, lexer::Lexer};
|
||||
use automata::{
|
||||
delta_lower, epsilon, gamma_upper,
|
||||
loader::{self, Context, Span, Spanned, lexer::Lexer},
|
||||
sigma_upper,
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
|
@ -88,16 +92,11 @@ pub fn lex(input: &str) -> Vec<Tok> {
|
|||
{
|
||||
Kind::Keyword
|
||||
}
|
||||
|
||||
// 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(
|
||||
loader::EPSILON_LOWER
|
||||
| "epsilon"
|
||||
| loader::DELTA_LOWER
|
||||
| "delta"
|
||||
| loader::GAMMA_UPPER
|
||||
| "gamma"
|
||||
| loader::GAMMA_LOWER
|
||||
| loader::SIGMA_UPPER
|
||||
| "sigma",
|
||||
epsilon!(pat) | delta_lower!(pat) | sigma_upper!(pat) | gamma_upper!(pat),
|
||||
) => Kind::Keyword,
|
||||
Token::Ident(_) => Kind::Ident,
|
||||
Token::LineEnd => Kind::Punc,
|
||||
|
|
@ -127,6 +126,7 @@ pub fn lex(input: &str) -> Vec<Tok> {
|
|||
#[wasm_bindgen]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum LogLevel {
|
||||
Help = "help",
|
||||
Info = "info",
|
||||
Warning = "warning",
|
||||
Error = "error",
|
||||
|
|
@ -165,7 +165,7 @@ pub fn compile(input: &str) -> CompileResult {
|
|||
|
||||
use std::fmt::Write;
|
||||
let ansi_log = ctx.logs_display().fold(String::new(), |mut s, e| {
|
||||
write!(&mut s, "{e}").unwrap();
|
||||
writeln!(&mut s, "{e}").unwrap();
|
||||
s
|
||||
});
|
||||
|
||||
|
|
@ -174,6 +174,7 @@ pub fn compile(input: &str) -> CompileResult {
|
|||
.into_entries()
|
||||
.map(|e| CompileLog {
|
||||
level: match e.level {
|
||||
loader::log::LogLevel::Help => LogLevel::Help,
|
||||
loader::log::LogLevel::Info => LogLevel::Info,
|
||||
loader::log::LogLevel::Warning => LogLevel::Warning,
|
||||
loader::log::LogLevel::Error => LogLevel::Error,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue