@webwriter/automaton
Version:
Build, visualize, and interact with different kinds of automata (DFA, NFA, PDA).
230 lines (183 loc) • 8.37 kB
text/typescript
import { Automaton, SimulationResult, Node, Transition } from '../automata';
import { ManualAutoSimulator } from './manual-auto';
import { Logger } from '@u/logger';
import { AutomatonType } from 'index';
export class NFA extends Automaton {
public simulator: NFASimulator;
public type: AutomatonType = 'nfa';
constructor(nodes: Node[], transitions: Transition[]) {
super(nodes, transitions);
this.simulator = new NFASimulator(this);
}
public loadAutomaton(data: { nodes: Node[]; transitions: Transition[] }): void {
this.nodes.clear();
this.transitions.clear();
this.nodes.update(data.nodes);
this.transitions.update(data.transitions);
for (const node of this.nodes.get()) {
if (node.final) this.updateNode(node.id, { final: true });
if (node.initial) this.updateNode(node.id, { initial: true });
}
Logger.log('Loaded NFA', this.nodes.get(), this.transitions.get());
}
public saveAutomaton(): string {
throw new Error('Method not implemented.');
}
public getTransitionLabel(transition: Transition): string {
if (transition.symbols.length === 1) {
return transition.symbols[0] === '' ? 'ε' : transition.symbols[0];
}
return transition.symbols.map((s) => (s === '' ? 'ε' : s)).join(', ');
}
}
export class NFASimulator extends ManualAutoSimulator {
constructor(automaton: NFA) {
super(automaton);
}
protected getPath(): SimulationResult {
this._errors = this._a.checkAutomaton();
if (this._errors.length > 0) {
return { accepted: false, path: { nodes: [], transitions: [] }, errors: this._errors };
}
type PathTransition = { transition: Transition; symbol: string };
// Cache the epsilon closure computations for performance
const epsilonClosures = new Map<number, Set<{ nodes: Node[]; transitions: PathTransition[] }>>();
const getEpsilonClosure = (node: Node): Set<{ nodes: Node[]; transitions: PathTransition[] }> => {
if (epsilonClosures.has(node.id)) {
return epsilonClosures.get(node.id)!;
}
const closure = new Set<{ nodes: Node[]; transitions: PathTransition[] }>();
const visited = new Set<number>();
const queue: { nodes: Node[]; transitions: PathTransition[] }[] = [{ nodes: [node], transitions: [] }];
while (queue.length > 0) {
const current = queue.shift()!;
const currentNode = current.nodes[current.nodes.length - 1];
if (visited.has(currentNode.id)) continue;
visited.add(currentNode.id);
closure.add(current);
const currentEpsilonTransitions = this._a
.getTransitionsFromNode(currentNode)
.filter((t) => t.symbols.includes(''));
for (const transition of currentEpsilonTransitions) {
const targetNode = this._a.getNode(transition.to);
if (targetNode && !visited.has(targetNode.id)) {
queue.push({
nodes: [...current.nodes, targetNode],
transitions: [...current.transitions, { transition, symbol: '' }]
});
}
}
}
epsilonClosures.set(node.id, closure);
return closure;
};
const initialNode = this._a.getInitialNode();
if (!initialNode) {
return { accepted: false, path: { nodes: [], transitions: [] } };
}
type SearchState = {
node: Node;
wordIndex: number;
path: { nodes: Node[]; transitions: PathTransition[] };
};
// Start with epsilon closure of initial node
const initialClosure = getEpsilonClosure(initialNode);
const queue: SearchState[] = Array.from(initialClosure).map((closurePath) => ({
node: closurePath.nodes[closurePath.nodes.length - 1],
wordIndex: 0,
path: closurePath
}));
while (queue.length > 0) {
const { node, wordIndex, path } = queue.shift()!;
// Check if we have processed the entire word
if (wordIndex >= this._word.length) {
if (node.final) {
return { accepted: true, path };
}
continue;
}
const currentLetter = this._word[wordIndex];
const transitions = this._a.getTransitionsFromNode(node);
for (const transition of transitions) {
if (!transition.symbols.includes(currentLetter)) continue;
const targetNode = this._a.getNode(transition.to);
if (!targetNode) continue;
const targetClosure = getEpsilonClosure(targetNode);
for (const closurePath of targetClosure) {
const newPathNodes = [...path.nodes, ...closurePath.nodes];
const newPathTransitions = [
...path.transitions,
{ transition, symbol: currentLetter },
...closurePath.transitions
];
queue.push({
node: closurePath.nodes[closurePath.nodes.length - 1],
wordIndex: wordIndex + 1,
path: { nodes: newPathNodes, transitions: newPathTransitions }
});
}
}
}
// No accepting path found
return { accepted: false, path: { nodes: [], transitions: [] } };
}
protected isValidTransition(transition: Transition): boolean {
const validSymbol =
transition.symbols.includes(this._word[this._currentWordPosition]) || transition.symbols.includes('');
const validFrom = transition.from === this._currentNode.id;
return validSymbol && validFrom;
}
protected highlightPossibleTransitions(node: Node): boolean {
const transitions = this._a.getTransitionsFromNode(node);
const currentSymbol = this._word[this._currentWordPosition];
const validTransitions = transitions.filter(
(t) => t.symbols.includes(currentSymbol) || t.symbols.includes('')
);
this._a.clearHighlights();
// reset selected transitions
this._graph?.network.setSelection({ edges: [] });
if (validTransitions.length === 0) {
return false;
}
for (const transition of validTransitions) {
this._a.highlightTransition(transition);
}
this._a.highlightNode(node);
return true;
}
protected async getManualMove(transition: Transition): Promise<{ to: Node; symbol: string } | null> {
const possibleSymbols = transition.symbols.filter(
(s) => s === '' || s === this._word[this._currentWordPosition]
);
if (possibleSymbols.length === 0) {
return null;
}
const to = this._a.getNode(transition.to) as Node;
if (possibleSymbols.length > 1) {
return new Promise((resolve) => {
const dialog = document.createElement('dialog');
const buttons = possibleSymbols.map((s) => {
const button = document.createElement('button');
button.innerHTML = s === '' ? 'ε' : s;
button.addEventListener('click', () => {
dialog.close();
dialog.remove();
resolve({ to, symbol: s });
});
return button;
});
dialog.append(...buttons);
this._graph?.component.shadowRoot?.appendChild(dialog);
dialog.showModal();
});
}
return { to, symbol: possibleSymbols[0] };
}
protected updateStateAfterManualMove(move: { to: Node; symbol: string }): void {
// do nothing
// win
}
protected updateStateAfterGoToStep(_step: number): void {
// NFA has no additional state to update
}
}