/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.routing.automata; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; /** * A deterministic finite automaton, with a transition table for fast incremental parsing. * * @author abyrd */ public class DFA extends NFA { final int[][] table; /** Build a deterministic finite automaton from an existing, potentially nondeterministic one. */ public DFA(NFA nfa) { super(nfa.nt, false); this.table = determinize(nfa); this.relabelNodes(); } /** Build a deterministic finite automaton that accepts the given nonterminal from a grammar */ public DFA(Nonterminal nonterminal) { this(new NFA(nonterminal)); } /** * A glorified set of automaton states, used in determinizing NFAs. Semantic equality inherited * from HashSet is needed for determinization. */ private class NFAStateSet extends HashSet<AutomatonState> { private static final long serialVersionUID = 1L; NFAStateSet(AutomatonState... automatonStates) { this.addAll(Arrays.asList(automatonStates)); } NFAStateSet(Collection<AutomatonState> automatonStates) { this.addAll(automatonStates); } public void followEpsilons() { Queue<AutomatonState> queue = new LinkedList<AutomatonState>(); queue.addAll(this); while (!queue.isEmpty()) { AutomatonState as = queue.poll(); for (AutomatonState target : as.epsilonTransitions) if (this.add(target)) queue.add(target); } } } /** * convert a (potentially) nondeterministic automaton into a new deterministic automaton with no * epsilon edges */ private int[][] determinize(NFA nfa) { int maxTerminal = 0; // track max token value so we know how big to make the transition table Map<NFAStateSet, AutomatonState> dfaStates = new HashMap<NFAStateSet, AutomatonState>(); Queue<NFAStateSet> queue = new LinkedList<NFAStateSet>(); /* initialize the work queue with the set of all states reachable from the NFA start state */ { AutomatonState dfaStart = new AutomatonState("START"); this.startStates.add(dfaStart); this.states.add(dfaStart); NFAStateSet startSet = new NFAStateSet(nfa.startStates); startSet.followEpsilons(); // be sure to follow episilons *before* using hashcode dfaStates.put(startSet, dfaStart); queue.add(startSet); } /* find all transitions between epsilon-connected subsets of the NFA states */ while (!queue.isEmpty()) { NFAStateSet nfaFromStates = queue.poll(); AutomatonState dfaFromState = dfaStates.get(nfaFromStates); Map<Integer, NFAStateSet> dfaTransitions = new HashMap<Integer, NFAStateSet>(); for (AutomatonState nfaFromState : nfaFromStates) { if (nfa.acceptStates.contains(nfaFromState)) this.acceptStates.add(dfaFromState); // multiple adds OK, it's a set for (Transition t : nfaFromState.transitions) { if (t.terminal > maxTerminal) maxTerminal = t.terminal; NFAStateSet nfaTargetStates = dfaTransitions.get(t.terminal); if (nfaTargetStates == null) { nfaTargetStates = new NFAStateSet(); dfaTransitions.put(t.terminal, nfaTargetStates); } nfaTargetStates.add(t.target); } } for (Entry<Integer, NFAStateSet> t : dfaTransitions.entrySet()) { int terminal = t.getKey(); NFAStateSet nfaToStates = t.getValue(); nfaToStates.followEpsilons(); AutomatonState dfaToState = dfaStates.get(nfaToStates); if (dfaToState == null) { dfaToState = new AutomatonState(); // nfaToStates.deriveLabel()); dfaStates.put(nfaToStates, dfaToState); this.states.add(dfaToState); queue.add(nfaToStates); } dfaFromState.transitions.add(new Transition(terminal, dfaToState)); } } /* create a transition table, filled with the reserved value for the reject state */ int[][] table = new int[this.states.size()][maxTerminal + 1]; for (int[] row : table) Arrays.fill(row, AutomatonState.REJECT); /* copy all edges from the new DFA states into the table */ for (int row = 0; row < this.states.size(); row++) for (Transition t : this.states.get(row).transitions) table[row][t.terminal] = this.states.indexOf(t.target); /* return the transition table to the DFA constructor */ return table; } /** Dump the transition table to a string. Rows are states, columns are terminal symbols. */ public String dumpTable() { StringBuilder sb = new StringBuilder(); int r = 0; sb.append(" "); for (int i = 0; i < table[0].length; i++) sb.append(String.format("%2d ", i)); sb.append(" \n"); for (int[] row : table) { sb.append(String.format("%5s ", states.get(r).label)); for (int i : row) { if (i == AutomatonState.REJECT) sb.append(" - "); else sb.append(String.format("%2s ", states.get(i).label)); } sb.append("\n"); r += 1; } return sb.toString(); } /** * Return true if this DFA (and the nonterminal it is built from) match the given sequence of * terminal symbols. */ public boolean parse(int... terminals) { int state = AutomatonState.START; for (int terminal : terminals) { state = transition(state, terminal); if (state == AutomatonState.REJECT) return false; } return this.accepts(state); } /** this method will not catch reject states; the caller must do so. */ public int transition(int initState, int terminal) { return table[initState][terminal]; } public boolean accepts(int state) { if (state == AutomatonState.REJECT) return false; return acceptStates.contains(states.get(state)); } }