diff --git a/automata/src/automatan/tm.rs b/automata/src/automatan/tm.rs index 626b18c..a8838dc 100644 --- a/automata/src/automatan/tm.rs +++ b/automata/src/automatan/tm.rs @@ -3,12 +3,11 @@ use std::collections::HashSet; use super::*; use crate::{ - delta_lower, dual_struct_serde, - loader::{ + delta_lower, dual_struct_serde, gamma_upper, loader::{ BLANK_SYMBOL, Context, INITIAL_STATE, Spanned, ast::{self, Symbol as Sym}, log::LogSink, - }, + } }; dual_struct_serde! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] @@ -23,8 +22,11 @@ dual_struct_serde! { #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Direction { + #[serde(rename = "<")] Left, + #[serde(rename = ">")] Right, + #[serde(rename = "_")] None, } @@ -185,7 +187,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> { use ast::TopLevel as TL; match element { TL::Item(S("Q", _), list) => self.compile_states(list, span), - TL::Item(S(delta_lower!(pat), _), list) => self.compile_symbols(list, span), + TL::Item(S(gamma_upper!(pat), _), list) => self.compile_symbols(list, span), TL::Item(S("F", _), list) => self.compile_final_states(list, span), TL::Item(S(INITIAL_STATE, _), item) => self.compile_initial_state(item, span), TL::Item(S(BLANK_SYMBOL, _), item) => self.compile_blank_symbol(item, span), @@ -335,11 +337,11 @@ impl<'a, 'b> TmCompiler<'a, 'b> { .emit_error("blank symbol already set", top_level) .emit_help("previously defined here", previous); } - if self.states.contains_key(&State(ident)) { + if self.symbols.contains_key(&Symbol(ident)) { self.blank_symbol = Some((Symbol(ident), top_level)) } else { self.ctx - .emit_error("blank symbol not defined as a state", src_d); + .emit_error("blank symbol not defined as a symbol", src_d); } } _ => _ = self.ctx.emit_error("expected ident", src_d), diff --git a/web/root/src/automata.ts b/web/root/src/automata.ts index 6577551..8077648 100644 --- a/web/root/src/automata.ts +++ b/web/root/src/automata.ts @@ -100,11 +100,11 @@ export type State = string; export type Symbol = string; export type Letter = string; -export type Span = [number, number]; +export type Span = readonly [number, number]; -export type StateInfo = { definition: Span }; -export type LetterInfo = { definition: Span }; -export type SymbolInfo = { definition: Span }; +export type StateInfo = { readonly definition: Span }; +export type LetterInfo = { readonly definition: Span }; +export type SymbolInfo = { readonly definition: Span }; export type FaTransFrom = { state: State; @@ -112,16 +112,16 @@ export type FaTransFrom = { }; export type FaTransTo = { - state: State; + readonly state: State; - transition: Span; - function: Span; + readonly transition: Span; + readonly function: Span; }; export type Edge = { - repr: string; - function: Span; - transition: Span; + readonly repr: string; + readonly function: Span; + readonly transition: Span; }; export type Fa = { @@ -139,17 +139,17 @@ export type Fa = { }; export type PdaTransFrom = { - state: State; - letter: Letter | null; - symbol: Symbol; + readonly state: State; + readonly letter: Letter | null; + readonly symbol: Symbol; }; export type PdaTransTo = { - state: State; - stack: Symbol[]; + readonly state: State; + readonly stack: readonly Symbol[]; - transition: Span; - function: Span; + readonly transition: Span; + readonly function: Span; }; export type Pda = { @@ -172,17 +172,17 @@ export type Pda = { }; export type TmTransFrom = { - state: State; - symbol: Symbol; + readonly state: State; + readonly symbol: Symbol; }; export type TmTransTo = { - state: State; - symbol: Symbol; - direction: "L" | "R" | "N"; + readonly state: State; + readonly symbol: Symbol; + readonly direction: "<" | ">" | "_"; - transition: Span; - function: Span; + readonly transition: Span; + readonly function: Span; }; export type Tm = { diff --git a/web/root/src/examples.ts b/web/root/src/examples.ts index 593e253..bf2304b 100644 --- a/web/root/src/examples.ts +++ b/web/root/src/examples.ts @@ -163,6 +163,30 @@ d(q0, epsilon, B) = { (q1, B) } d(q1, a, A) = { (q1, epsilon) } d(q1, b, B) = { (q1, epsilon) }`, ), + + new Example("TM", "a^nb^n", + `// accepts all strings on {a,b}+ of the form anbn + +type = TM +Q = { q0, q1, q2, q3, q4 } // set of internal states +F = { q4 } // set of final states +T = { a, b, X, Y, B } // tape alphabet +B = B // the blank symbol (tape initializer symbol) +q0 = q0 // initial state + +d(q0,a)=(q1,x,R) +d(q1,a)=(q1,a,R) +d(q1,Y)=(q1,y,R) +d(q1,b)=(q2,y,L) + +d(q2,Y)=(q2,y,L) +d(q2,a)=(q2,a,L) +d(q2,X)=(q0,x,R) + +d(q0,Y)=(q3,y,R) +d(q3,Y)=(q3,y,R) +d(q3,B)=(q4,B,R) +`) ]; const CATEGORY_ORDER: Category[] = [ diff --git a/web/root/src/simulation.ts b/web/root/src/simulation.ts index bafd19b..84b9fa1 100644 --- a/web/root/src/simulation.ts +++ b/web/root/src/simulation.ts @@ -1,14 +1,21 @@ import { bus } from "./bus.ts"; import type { - Fa, Machine, + Fa, Pda, - State, - Symbol, Tm, } from "./automata.ts"; import {parse_machine_from_json} from "./automata.ts"; +import { FaSim } from "./simulation/fa.ts"; +export { FaSim } from "./simulation/fa.ts"; + +import { PdaSim } from "./simulation/pda.ts"; +export { PdaSim } from "./simulation/pda.ts"; + +import { TmSim } from "./simulation/tm.ts"; +export { TmSim } from "./simulation/tm.ts"; + export type SimStepResult = "pending" | "accept" | "reject"; export type Sim = FaSim | PdaSim | TmSim; @@ -93,234 +100,3 @@ bus.on("automata/sim/after_step", ({result}) => { } }); -export class FaState { - readonly state: State; - - readonly position: number; - readonly input: string; - readonly accepted: boolean = false; - private repr!: string; - - constructor(state: State, position: number, input: string) { - this.state = state; - this.position = position; - this.input = input; - } - - toString(): string { - if (!this.repr) { - this.repr = this.state + " >" + this.input.substring(this.position); - } - return this.repr; - } -} - -export class FaSim { - machine: Fa; - paths: FaState[]; - input: string; - - current_states: Map = new Map(); - accepted: FaState[] = []; - - constructor(machine: Fa, input: string) { - this.machine = machine; - this.paths = [new FaState(machine.initial_state, 0, input)]; - this.current_states.set(machine.initial_state, [this.paths[0]]); - this.input = input; - } - - step(): SimStepResult { - if (this.paths.length == 0) return "reject"; - if (this.accepted.length != 0) return "accept"; - - const paths: FaState[] = []; - this.current_states.clear(); - - const push = (state: FaState) => { - paths.push(state); - if (!this.current_states.has(state.state)) { - this.current_states.set(state.state, []); - } - this.current_states.get(state.state)?.push(state); - - if ( - state.position == this.input.length && - this.machine.final_states.has(state.state) - ) { - // @ts-expect-error sillllyyyy - state.accepted = true; - this.accepted.push(state); - } - }; - - for (const path of this.paths) { - const letter_map = this.machine.transitions_components.get(path.state)!; - if (!letter_map) continue; - - for (const to of letter_map.get(null) ?? []) { - push(new FaState(to.state, path.position, this.input)); - } - - if (path.position >= this.input.length) continue; - - const char = this.input.charAt(path.position); - - for (const to of letter_map.get(char) ?? []) { - push(new FaState(to.state, path.position + 1, this.input)); - } - } - this.paths = paths; - - if (this.paths.length == 0) return "reject"; - if (this.accepted.length != 0) return "accept"; - return "pending"; - } -} - -export class PdaState { - readonly state: State; - readonly stack: Symbol[]; - - readonly position: number; - readonly input: string; - readonly accepted: boolean = false; - private repr!: string; - - constructor(state: State, stack: Symbol[], position: number, input: string) { - this.state = state; - this.stack = stack; - this.position = position; - this.input = input; - } - - toString(): string { - if (!this.repr) { - this.repr = this.state + " [" + this.stack + "]" + " >" + - this.input.substring(this.position); - } - return this.repr; - } -} - -export class PdaSim { - machine: Pda; - paths: PdaState[]; - input: string; - - current_states: Map = new Map(); - accepted: PdaState[] = []; - - constructor(machine: Pda, input: string) { - this.machine = machine; - this.paths = [ - new PdaState(machine.initial_state, [machine.initial_stack], 0, input), - ]; - this.current_states.set(machine.initial_state, [this.paths[0]]); - this.input = input; - } - - step(): SimStepResult { - if (this.paths.length == 0) return "reject"; - if (this.accepted.length != 0) return "accept"; - - const paths: PdaState[] = []; - this.current_states.clear(); - - const push = (state: PdaState) => { - paths.push(state); - if (!this.current_states.has(state.state)) { - this.current_states.set(state.state, []); - } - this.current_states.get(state.state)?.push(state); - - if ( - state.position == this.input.length && this.machine.final_states && - this.machine.final_states.has(state.state) || - state.position == this.input.length && !this.machine.final_states && - state.stack.length == 1 && - state.stack[0] == this.machine.initial_stack - ) { - // @ts-expect-error sillllyyyy - state.accepted = true; - this.accepted.push(state); - } - }; - - for (const path of this.paths) { - const stack = path.stack.pop()!; - const letter_map = this.machine.transitions_components.get(path.state) - ?.get(stack); - if (!letter_map) continue; - - for (const to of letter_map.get(null) ?? []) { - push( - new PdaState( - to.state, - path.stack.concat(to.stack), - path.position, - this.input, - ), - ); - } - - if (path.position >= this.input.length) continue; - - const char = this.input.charAt(path.position); - - for (const to of letter_map.get(char) ?? []) { - push( - new PdaState( - to.state, - path.stack.concat(to.stack), - path.position + 1, - this.input, - ), - ); - } - } - this.paths = paths; - - if (this.paths.length == 0) return "reject"; - if (this.accepted.length != 0) return "accept"; - return "pending"; - } -} - -export class TmState { - readonly state: State; - readonly tape: Symbol[]; - - readonly position: number; - readonly input: string; - readonly accepted: boolean = false; - private repr!: string; - - constructor(state: State, tape: Symbol[], position: number, input: string) { - this.state = state; - this.tape = tape; - this.position = position; - this.input = input; - } - - toString(): string { - if (!this.repr) this.repr = this.state + " " + this.position; - return this.repr; - } -} - -export class TmSim { - machine: Tm; - input: string; - current_states: Map = new Map(); - accepted: TmState[] = []; - - constructor(machine: Tm, input: string) { - this.machine = machine; - this.input = input; - } - - step(): SimStepResult { - return "pending"; - } -} diff --git a/web/root/src/simulation/fa.ts b/web/root/src/simulation/fa.ts new file mode 100644 index 0000000..476fe41 --- /dev/null +++ b/web/root/src/simulation/fa.ts @@ -0,0 +1,117 @@ +import type { + Fa, + FaTransTo, + State, +} from "../automata.ts"; +import { SimStepResult } from "../simulation.ts"; + +export type FaState = { + readonly state: State; + readonly position: number; + + readonly accepted: boolean; + readonly repr: string; + + readonly path: readonly FaTransTo[]; +}; + +type Initializer = { -readonly [P in keyof T]?: T[P] | undefined }; + +export class FaSim { + readonly machine: Fa; + readonly input: string; + + paths: FaState[] = []; + current_states: Map = new Map(); + accepted: FaState[] = []; + rejected: FaState[] = []; + + constructor(machine: Fa, input: string) { + this.machine = machine; + this.input = input; + this.initial(); + } + + private accept(state: Initializer): boolean { + const pos = state.position ?? 0; + const st = state.state!; + return pos === this.input.length && this.machine.final_states.has(st); + } + + private init_state(state: Initializer) { + state.position ??= 0; + + state.accepted = this.accept(state); + state.repr = state.state + " >" + this.input.substring(state.position); + + const frozen = state as FaState; + + if (frozen.accepted) this.accepted.push(frozen); + this.paths.push(frozen); + + if (!this.current_states.has(frozen.state)) { + this.current_states.set(frozen.state, []); + } + this.current_states.get(frozen.state)!.push(frozen); + } + + private initial() { + const state: Initializer = { + state: this.machine.initial_state, + position: 0, + path: [], + }; + + this.init_state(state); + } + + private transition(from: FaState, to: FaTransTo, consume: boolean) { + const state: Initializer = { + state: to.state, + position: from.position + (consume ? 1 : 0), + path: from.path.concat([to]), + }; + + this.init_state(state); + } + + step(): SimStepResult { + if (this.accepted.length !== 0) return "accept"; + if (this.paths.length === 0) return "reject"; + + const paths = this.paths; + this.paths = []; + this.current_states.clear(); + + for (const from of paths) { + const letterMap = this.machine.transitions_components.get(from.state); + + if (!letterMap) { + this.rejected.push(from); + continue; + } + + // epsilon transitions + const eps = letterMap.get(null) ?? []; + for (const to of eps) this.transition(from, to, false); + + // consuming transitions + if (from.position >= this.input.length) { + if (eps.length === 0) this.rejected.push(from); + continue; + } + + const ch = this.input.charAt(from.position); + const trs = letterMap.get(ch) ?? []; + for (const to of trs) this.transition(from, to, true); + + if (eps.length === 0 && trs.length === 0) { + this.rejected.push(from); + } + } + + if (this.accepted.length !== 0) return "accept"; + if (this.paths.length === 0) return "reject"; + return "pending"; + } +} diff --git a/web/root/src/simulation/pda.ts b/web/root/src/simulation/pda.ts new file mode 100644 index 0000000..80ec4be --- /dev/null +++ b/web/root/src/simulation/pda.ts @@ -0,0 +1,146 @@ +import type { + Pda, + PdaTransTo, + State, + Symbol +} from "../automata.ts"; +import { SimStepResult } from "../simulation.ts"; + +export type PdaState = { + readonly state: State; + readonly stack: Symbol[]; + readonly position: number; + + readonly accepted: boolean; + readonly repr: string; + + readonly path: readonly PdaTransTo[]; +}; + +type Initializer = { -readonly [P in keyof T]?: T[P] | undefined }; + +export class PdaSim { + readonly machine: Pda; + readonly input: string; + + paths: PdaState[] = []; + current_states: Map = new Map(); + accepted: PdaState[] = []; + rejected: PdaState[] = []; + + constructor(machine: Pda, input: string) { + this.machine = machine; + this.input = input; + this.initial(); + } + + private accept(state: Initializer): boolean { + const pos = state.position ?? 0; + const st = state.state!; + const stack = state.stack ?? []; + + //accept by final state + if (pos === this.input.length && this.machine.final_states && this.machine.final_states.has(st)) { + return true; + } + //accept by empty stack + if (pos === this.input.length && !this.machine.final_states && stack.length === 1 && stack[0] === this.machine.initial_stack) { + return true; + } + + return false; + } + + private init_state(state: Initializer) { + state.stack ??= [this.machine.initial_stack]; + state.position ??= 0; + + state.accepted = this.accept(state); + state.repr = state.state + " [" + state.stack.join(",") + "] >" + this.input.substring(state.position); + + const frozen = state as PdaState; + + if (frozen.accepted) this.accepted.push(frozen); + this.paths.push(frozen); + + if (!this.current_states.has(frozen.state)) { + this.current_states.set(frozen.state, []); + } + this.current_states.get(frozen.state)!.push(frozen); + } + + private initial() { + const state: Initializer = { + state: this.machine.initial_state, + stack: [this.machine.initial_stack], + position: 0, + path: [], + }; + + this.init_state(state); + } + + private transition(from: PdaState, to: PdaTransTo, consume: boolean) { + const stackCopy = from.stack.slice(0, from.stack.length - 1); // pop off top + const nextStack = stackCopy.concat(to.stack); + if (nextStack.length == 0) { + this.rejected.push(from) + return; + } + + const state: Initializer = { + state: to.state, + stack: nextStack, + position: from.position + (consume ? 1 : 0), + path: from.path.concat([to]), + }; + + this.init_state(state); + } + + step(): SimStepResult { + if (this.accepted.length !== 0) return "accept"; + if (this.paths.length === 0) return "reject"; + + const paths = this.paths; + this.paths = []; + this.current_states.clear(); + + for (const from of paths) { + const top = from.stack[from.stack.length - 1]; + + const letterMap = this.machine.transitions_components.get(from.state)?.get(top); + if (!letterMap) { + this.rejected.push(from); + continue; + } + + // epsilon transitions + const epsilon_transitions = letterMap.get(null) ?? []; + for (const to of epsilon_transitions) { + this.transition(from, to, false); + } + + if (from.position >= this.input.length) { + if (epsilon_transitions.length == 0){ + this.rejected.push(from); + } + continue; + } + // consuming transitions + const ch = this.input.charAt(from.position); + + const transitions = letterMap.get(ch) ?? []; + for (const to of transitions) { + this.transition(from, to, true); + } + if (epsilon_transitions.length == 0 && transitions.length == 0){ + this.rejected.push(from); + } + } + + if (this.accepted.length !== 0) return "accept"; + if (this.paths.length === 0) return "reject"; + return "pending"; + } +} \ No newline at end of file diff --git a/web/root/src/simulation/tm.ts b/web/root/src/simulation/tm.ts new file mode 100644 index 0000000..74052d9 --- /dev/null +++ b/web/root/src/simulation/tm.ts @@ -0,0 +1,129 @@ +import type { + State, + Symbol, + Tm, + TmTransTo +} from "../automata.ts"; +import { SimStepResult } from "../simulation.ts"; + + +export type TmState = { + readonly state: State; + readonly tape: Symbol[]; + readonly head: number; + + readonly accepted: boolean; + readonly repr: string; + + readonly path: readonly TmTransTo[]; +} + + +type Initializer = { -readonly [P in keyof T]?: T[P] | undefined }; + +export class TmSim { + readonly machine: Tm; + paths: TmState[] = []; + readonly input: string; + + current_states: Map = new Map(); + accepted: TmState[] = []; + rejected: TmState[] = []; + + constructor(machine: Tm, input: string) { + this.machine = machine; + this.input = input; + this.initial(); + } + + private init_state(state: Initializer) { + state.repr = state.state + " [ " + this.machine.blank_symbol + " " + state.tape!.map((s, i, _) => i == state.head ? `[${s}]` : s).join(" ") + " " + this.machine.blank_symbol + " ]"; + + + const frozen = state as TmState; + if (frozen.accepted) this.accepted.push(frozen); + this.paths.push(frozen); + if (!this.current_states.has(frozen.state)) { + this.current_states.set(frozen.state, []); + } + this.current_states.get(frozen.state)!.push(frozen); + } + + private initial() { + const state: Initializer = { + state: this.machine.initial_state, + accepted: this.machine.final_states.has(this.machine.initial_state), + tape: this.input.split(''), + head: 0, + + path: [], + }; + + if (state.tape!.length == 0) state.tape!.push(this.machine.blank_symbol) + + this.init_state(state); + } + + private transition(from: TmState, to: TmTransTo) { + const state: Initializer = { + state: to.state, + accepted: this.machine.final_states.has(to.state), + + path: from.path.concat([to]), + }; + + switch (to.direction) { + case "_": + state.tape = from.tape.slice(); + state.tape![from.head] = to.symbol; + state.head = from.head; + break; + case "<": + if (from.head == 0) { + state.tape = from.tape.splice(0, 0, to.symbol); + state.head = 0; + } else { + state.tape = from.tape.slice(); + state.tape![from.head] = to.symbol; + state.head = from.head - 1; + } + break; + case ">": + state.head = from.head + 1; + state.tape = from.tape.slice(); + state.tape![from.head] = to.symbol; + if (state.head == from.tape.length) { + state.tape!.push(this.machine.blank_symbol); + } + break; + } + + this.init_state(state) + } + + step(): SimStepResult { + if (this.accepted.length != 0) return "accept"; + if (this.paths.length == 0) return "reject"; + + const paths: TmState[] = this.paths; + this.paths = []; + this.current_states.clear(); + + for (const from of paths) { + const symbol = from.tape[from.head]; + const transitions = this.machine.transitions_components.get(from.state)?.get(symbol) ?? []; + if (transitions.length == 0) { + this.rejected.push(from); + continue; + } + + for (const to of transitions) { + this.transition(from, to); + } + } + + if (this.accepted.length != 0) return "accept"; + if (this.paths.length == 0) return "reject"; + return "pending"; + } +} \ No newline at end of file diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index 48f035e..8e1ba30 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -398,7 +398,7 @@ function renderNode({ const lineH = 14; let w = 0; - for (const ln of paths) w = Math.max(w, ctx.measureText(ln.toString()).width); + for (const ln of paths) w = Math.max(w, ctx.measureText(ln.repr).width); const boxW = w + padX * 2; const boxH = paths.length * lineH + padY * 2; @@ -415,7 +415,7 @@ function renderNode({ ctx.textBaseline = "top"; for (let i = 0; i < paths.length; i++) { ctx.fillStyle = paths[i].accepted ? t.current_node_border : t.fg_0; - ctx.fillText(paths[i].toString(), x, by + padY + i * lineH); + ctx.fillText(paths[i].repr, x, by + padY + i * lineH); } }