UNPKG

@bruju/automata-composer

Version:

Basic finite state automata building by composition

381 lines (380 loc) 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.State = exports.Automata = exports.inverse = exports.modifyTransitions = exports.star = exports.plus = exports.maybe = exports.chain = exports.or = exports.unit = exports.AutomataComposer = exports.EPSILON = void 0; /** Value used to represent epsilon */ exports.EPSILON = null; /** * An helper class to build non deterministic finite state automata with epsilon * transitions by composing automatas */ class AutomataComposer { /** * Build a new automata composer * @param start Starting state * @param end Ending state * @param transitions List of transitions */ constructor(start, end, transitions) { this.start = start; this.end = end; this.transitions = transitions; } build() { return new UndeterministicAutomata(this).toDeterministic(); } } exports.AutomataComposer = AutomataComposer; /** * Build an automata with only one transition * @param symbol The symbol on the transition * @returns The automata */ function unit(symbol) { return new AutomataComposer(0, 1, [{ from: 0, to: 1, symbol: symbol }]); } exports.unit = unit; /** * Build an automata that recognizes both the language in lhs and the language * in rhs * @param lhs The first language * @param rhs The second language * @returns An automata that recognizes both languages */ function or(lhs, rhs) { const remapper = new StateRemapper(2); const lhsOk = remapper.rewrite(lhs); const rhsOk = remapper.rewrite(rhs); return new AutomataComposer(0, 1, [ // either lhs { from: 0, symbol: exports.EPSILON, to: lhsOk.start }, ...lhsOk.transitions, { from: lhsOk.end, symbol: exports.EPSILON, to: 1 }, // or rhs { from: 0, symbol: exports.EPSILON, to: rhsOk.start }, ...rhsOk.transitions, { from: rhsOk.end, symbol: exports.EPSILON, to: 1 }, ]); } exports.or = or; /** * Build an automata that recognizes the language described by the sequence * of given automatas * @param lhs The first part * @param rhs The second part * @returns lhs -eps-> rhs */ function chain(...subAutomatas) { if (subAutomatas.length === 0) { return new AutomataComposer(0, 0, []); } const remapper = new StateRemapper(0); const subOk = subAutomatas.map(sub => remapper.rewrite(sub)); const junctions = []; for (let i = 0; i != subOk.length - 1; ++i) { junctions.push({ from: subOk[i].end, to: subOk[i + 1].start, symbol: exports.EPSILON }); } return new AutomataComposer(subOk[0].start, subOk[subOk.length - 1].end, [ ...subOk.flatMap(sub => sub.transitions), ...junctions ]); } exports.chain = chain; /** Builds the automata (self)? */ function maybe(self) { return bridge(self, { from: 0, to: 1, symbol: exports.EPSILON }); } exports.maybe = maybe; /** Builds the automata (self)+ */ function plus(self) { return bridge(self, { from: 1, to: 0, symbol: exports.EPSILON }); } exports.plus = plus; /** Builds the automata (self)* */ function star(self) { return maybe(plus(self)); } exports.star = star; function bridge(self, extra) { const remapper = new StateRemapper(2); const selfOk = remapper.rewrite(self); return new AutomataComposer(0, 1, [ ...selfOk.transitions, extra, { from: 0, to: selfOk.start, symbol: exports.EPSILON }, { from: selfOk.end, to: 1, symbol: exports.EPSILON }, ]); } /** * Build an automata where all symbols has been modified */ function modifyTransitions(self, modifier) { return new AutomataComposer(self.start, self.end, self.transitions.map(transition => { if (transition.symbol === exports.EPSILON) return transition; return { from: transition.from, to: transition.to, symbol: modifier(transition.symbol) }; })); } exports.modifyTransitions = modifyTransitions; function identity(t) { return t; } /** * Inverse the automata state * @param self The base automata * @param modifier A function to apply to the transition names. Default value * is the identity function (i.e. the transitions are not changed) * @returns An automata composer that is the inverse of the original one */ function inverse(self, modifier = identity) { return new AutomataComposer(self.end, self.start, self.transitions.map(transition => { return { from: transition.to, to: transition.from, symbol: transition.symbol === exports.EPSILON ? exports.EPSILON : modifier(transition.symbol) }; })); } exports.inverse = inverse; /** * Class used to have distinct node ids when composing two distinct automatas */ class StateRemapper { constructor(startFrom) { this.nextState = startFrom; } rewrite(automata) { let oldToNew = new Map(); const get = (stateId) => { let r = oldToNew.get(stateId); if (r === undefined) { r = ++this.nextState; oldToNew.set(stateId, r); } return r; }; const newStart = get(automata.start); const newEnd = get(automata.end); const transitions = automata.transitions.map(transition => ({ from: get(transition.from), symbol: transition.symbol, to: get(transition.to) })); return new AutomataComposer(newStart, newEnd, transitions); } } /** * An automata with undeterministic transitions. Used as a stepping stone from * the automata composer representation with a list of transitions and a real * automata with a state centric representation. */ class UndeterministicAutomata { /** Builds the undeterministic automata */ constructor(composer) { this.allStates = new Map(); const getState = (stateId) => { let state = this.allStates.get(stateId); if (state === undefined) { state = new UndeterministicState(stateId); this.allStates.set(stateId, state); } return state; }; this.start = getState(composer.start); this.end = getState(composer.end); for (const { from, to, symbol } of composer.transitions) { const begin = getState(from); const end = getState(to); if (symbol === exports.EPSILON) { begin.addEpsilon(end); } else { begin.addTransition(symbol, end); } } } /** * Converts this automata into a deterministic finite state automata */ toDeterministic() { let allStates = new Map(); let notVisited = []; const ends = []; /** * From a list of undeterminist state, returns a object that represents the * final deterministic state, ie a state that represents the union of all * passed states and the states that they can reach through an epsilon * transition. */ const getState = (states) => { const stateClosure = CompoundUndeterministicState.computeClosure(states); const key = CompoundUndeterministicState.makeKey(stateClosure); let compoundState = allStates.get(key); if (compoundState !== undefined) { return compoundState; } compoundState = new CompoundUndeterministicState(allStates.size, stateClosure); if (stateClosure.includes(this.end)) { ends.push(compoundState); } allStates.set(key, compoundState); notVisited.push(compoundState); return compoundState; }; const start = getState([this.start]); while (true) { const compoundState = notVisited.shift(); if (compoundState === undefined) break; let transitions = new Map(); for (const state of compoundState.states) { for (const [transition, nextStates] of state.transitions) { let r = transitions.get(transition); if (r === undefined) { r = []; transitions.set(transition, r); } nextStates.forEach(nextState => UndeterministicState.ensureHas(r, nextState)); } } for (const [symbol, stateCollection] of transitions) { const endOfTransition = getState(stateCollection); compoundState.uniqueState.transitions.set(symbol, endOfTransition.uniqueState); } } const allUniqueStates = [...allStates.values()].map(x => x.uniqueState); return new Automata(start.uniqueState, ends.map(end => end.uniqueState), allUniqueStates); } } /** A state that has unterministic transitions */ class UndeterministicState { /** Build an undterministic state */ constructor(id) { /** The list of transitions and where they may lead */ this.transitions = new Map(); /** States that may be reached through an epsilon transition */ this.epsilon = []; this.id = id; } /** Adds the given transition */ addTransition(symbol, target) { let r = this.transitions.get(symbol); if (r === undefined) { r = []; this.transitions.set(symbol, r); } UndeterministicState.ensureHas(r, target); } /** Adds a state reachable with no symbol */ addEpsilon(target) { UndeterministicState.ensureHas(this.epsilon, target); } /** Ensures that the list of states has the given state, without duping it */ static ensureHas(states, state) { if (!states.includes(state)) { states.push(state); } } } ; /** A state formed of multiple states. Used during automata minimization */ class CompoundUndeterministicState { /** Build a composed state */ constructor(id, states) { this.uniqueState = new State(id, new Map()); this.states = states; } /** * Generates a list of states with the states passed and the states reached by * an epsilon transition */ static computeClosure(states_) { let states = [...states_]; for (let i = 0; i != states.length; ++i) { for (const next of states[i].epsilon) { UndeterministicState.ensureHas(states, next); } } states.sort((lhs, rhs) => lhs.id - rhs.id); return states; } /** Supposing the list of sorted, generates a unique key */ static makeKey(states) { return states.map(s => s.id.toString(16)).join("-"); } } /** A finite state automata */ class Automata { /** * Builds a new finite state automata * @param start The starting node * @param ends The end node * @param allStates Every known states. start and ends should be included in * it. */ constructor(start, ends, allStates) { this.start = start; this.ends = ends; this.states = allStates; } /** * Test if the given sequence is accepted by the automata. * @param sequence The sequence * @returns True if from the starting node, the given sequence can be used * to reach one of the end of the automata. */ test(sequence) { let current = this.start; for (const word of sequence) { let nextState = current.next(word); if (nextState === null) return false; current = nextState; } return this.ends.includes(current); } /** * Gives a string representation of the automata */ /* istanbul ignore next */ toLongString() { let s = []; for (const state of this.states) { let line = `State ${state.id}`; for (const [symbol, next] of state.transitions) { line += "\n- " + symbol + " -> " + next.id; } s.push(line); } return s.join("\n"); } } exports.Automata = Automata; /** * A state in a finite state automata */ class State { /** * Builds a new state * @param id The id of the state * @param transitions The list of transitions */ constructor(id, transitions) { this.id = id; this.transitions = transitions; } /** * Returns the state after travelling through the given symbol * @param symbol The symbol used * @returns The state at the end of the transition corresponding to the given * symbol, or null if there is no such state */ next(symbol) { const nextState = this.transitions.get(symbol); if (nextState === undefined) return null; return nextState; } } exports.State = State;