/*
* $Id$
*
* Copyright (C) 2003-2014 JNode.org
*
* This library 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 2.1 of the License, or
* (at your option) any later version.
*
* This library 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 Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.shell.syntax;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import org.apache.log4j.Logger;
import org.jnode.driver.console.CompletionInfo;
import org.jnode.shell.CommandLine;
import org.jnode.shell.SymbolSource;
import org.jnode.shell.CommandLine.Token;
import org.jnode.shell.syntax.CommandSyntaxException.Context;
/**
* This class implements parsing of a token stream against a MuSyntax graph. The parser
* binds token values against Argument instances as it goes, and does full backtracking
* when it reaches a point where it cannot make forward progress. The backtracking mechanism
* manages the parser state and also records the Arguments that have been 'set', and
* therefore need to be 'unset' when we backtrack.
* <p>
* When we are doing a normal parse, the various alternatives in the MuSyntax graph are
* explored until either there is a successful parse (consuming all tokens), or we run
* out of alternatives. The latter case results in an exception and a failed parse.
* <p>
* When we are doing a completion parse, all alternatives are explored irrespective of
* parse success.
* <p>
* A MuSyntax may contain "infinite" loops, or other pathologies that trigger
* excessive backtracking. To avoid problems, the 'parse' method takes a
* 'stepLimit' parameter that causes the parse to fail if it has not terminated
* soon enough.
* <p>
* The MuParser uses the SharedStack class to record syntax stacks for backtracking.
* This is a special purpose Deque that avoids unnecessary copying of the stack
* state. If you suspect that this is causing problems, replace {@code new SharedStack(...)}
* with {@code new LinkedList(...)}.
*
* @author crawley@jnode.org
*/
public class MuParser {
/**
* This is the default value for the stepLimit parameter.
*/
public static final int DEFAULT_STEP_LIMIT = 10000;
private static final boolean DEBUG = false;
private static final Logger log = Logger.getLogger(MuParser.class);
private static class ChoicePoint {
public final int sourcePos;
public final Deque<MuSyntax> syntaxStack;
public final MuSyntax[] choices;
public int choiceNo;
public final List<Argument<?>> argsModified;
public ChoicePoint(int sourcePos, Deque<MuSyntax> syntaxStack, MuSyntax[] choices) {
super();
this.sourcePos = sourcePos;
this.syntaxStack = syntaxStack;
this.choices = choices;
this.argsModified = new LinkedList<Argument<?>>();
this.choiceNo = 0;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("CP{");
sb.append("sourcePos=").append(sourcePos);
sb.append(",syntaxStack=").append(showStack(syntaxStack, true));
sb.append(",choiceNo=").append(choiceNo).append("...").append("}");
return sb.toString();
}
}
public MuParser() {
super();
}
/**
* Parse Tokens against a MuSyntax using a default stepLimit. On success, tokens will
* have been used to populate Argument values in the ArgumentBundle.
*
* @param rootSyntax the root of the MuSyntax graph.
* @param completions if this is not <code>null</null>, do a completion parse, and record
* the completions here.
* @param source the source of Tokens to be parsed
* @param bundle the container for Argument objects; e.g. provided by the command.
* @throws CommandSyntaxException
*/
public void parse(MuSyntax rootSyntax, CompletionInfo completions,
SymbolSource<Token> source, ArgumentBundle bundle)
throws CommandSyntaxException, SyntaxFailureException {
parse(rootSyntax, completions, source, bundle, DEFAULT_STEP_LIMIT);
}
/**
* Parse Tokens against a MuSyntax using a default stepLimit. On success, tokens will
* have been used to populate Argument values in the ArgumentBundle.
*
* @param rootSyntax the root of the MuSyntax graph.
* @param completions if this is not <code>null</null>, do a completion parse, and record
* the completions here.
* @param source the source of Tokens to be parsed
* @param bundle the container for Argument objects; e.g. provided by the command.
* @param stepLimit the maximum allowed parse steps allowed. A 'stepLimit' of zero or less
* means that there is no limit.
* @throws CommandSyntaxException
*/
public synchronized void parse(MuSyntax rootSyntax, CompletionInfo completions,
SymbolSource<Token> source, ArgumentBundle bundle, int stepLimit)
throws CommandSyntaxException, SyntaxFailureException {
// FIXME - deal with syntax error messages and completion
// FIXME - deal with grammars that cause stack explosion
if (bundle != null) {
// FIXME - why am I doing this here? Is it just for the unit tests?
bundle.clear();
}
Deque<MuSyntax> syntaxStack = new LinkedList<MuSyntax>();
Deque<ChoicePoint> backtrackStack = new LinkedList<ChoicePoint>();
if (DEBUG) {
log.debug("Parsing with rootSyntax = " + (rootSyntax == null ? "null" : rootSyntax.format()));
}
if (rootSyntax == null) {
if (source.hasNext()) {
throw new CommandSyntaxException("No arguments expected for this command");
}
return;
}
List<Context> argFailures = new LinkedList<Context>();
syntaxStack.addFirst(rootSyntax);
int stepCount = 0;
while (true) {
if (stepLimit > 0 && ++stepCount > stepLimit) {
throw new SyntaxFailureException(
"Parse exceeded the step limit (" + stepLimit + "). " +
"Either the command line is too large, " +
"or the syntax is too complex (or pathological)");
}
boolean backtrack = false;
if (DEBUG) {
log.debug("syntaxStack % " + showStack(syntaxStack, true));
}
if (syntaxStack.isEmpty()) {
if (source.hasNext()) {
if (DEBUG) {
log.debug("exhausted syntax stack too soon");
}
} else if (completions != null && !backtrackStack.isEmpty()) {
if (DEBUG) {
log.debug("try alternatives for completion");
}
} else {
if (DEBUG) {
log.debug("parse succeeded");
}
return;
}
backtrack = true;
} else {
MuSyntax syntax = syntaxStack.removeFirst();
if (DEBUG) {
log.debug("Trying kind = " + syntax.getKind() + ", syntax = " + syntax.format());
if (source.hasNext()) {
log.debug("source -> " + source.peek().text);
} else {
log.debug("source at end");
}
}
CommandLine.Token token = null;
switch (syntax.getKind()) {
case SYMBOL:
String symbol = ((MuSymbol) syntax).getSymbol();
token = source.hasNext() ? source.next() : null;
if (completions == null || source.hasNext()) {
backtrack = token == null || !token.text.equals(symbol);
} else {
if (token == null) {
completions.addCompletion(symbol);
backtrack = true;
} else if (source.whitespaceAfterLast()) {
if (!token.text.equals(symbol)) {
backtrack = true;
}
} else {
if (symbol.startsWith(token.text)) {
completions.addCompletion(symbol);
completions.setCompletionStart(token.start);
}
backtrack = true;
}
}
break;
case ARGUMENT:
MuArgument muArg = (MuArgument) syntax;
String argName = muArg.getArgName();
int flags = muArg.getFlags();
Argument<?> arg = bundle.getArgument(argName);
try {
if (source.hasNext()) {
token = source.next();
if (completions == null || source.hasNext() || source.whitespaceAfterLast()) {
arg.accept(token, flags);
if (!backtrackStack.isEmpty()) {
backtrackStack.getFirst().argsModified.add(arg);
if (DEBUG) {
log.debug("recording undo for arg " + argName);
}
}
} else {
arg.complete(completions, token.text, flags);
completions.setCompletionStart(token.start);
backtrack = true;
}
} else {
if (completions != null) {
arg.complete(completions, "", flags);
}
backtrack = true;
}
} catch (CommandSyntaxException ex) {
argFailures.add(new Context(token, syntax, source.tell(), ex));
if (DEBUG) {
log.debug("accept for arg " + argName + " threw SyntaxErrorException('" +
ex.getMessage() + "'");
}
backtrack = true;
}
break;
case PRESET:
MuPreset muPreset = (MuPreset) syntax;
arg = bundle.getArgument(muPreset.getArgName());
flags = muPreset.getFlags();
try {
arg.accept(new CommandLine.Token(muPreset.getPreset()), flags);
if (!backtrackStack.isEmpty()) {
backtrackStack.getFirst().argsModified.add(arg);
if (DEBUG) {
log.debug("recording undo for preset arg " + arg.getLabel());
}
}
} catch (CommandSyntaxException ex) {
argFailures.add(new Context(null, syntax, source.tell(), ex));
backtrack = true;
}
break;
case SEQUENCE:
MuSyntax[] elements = ((MuSequence) syntax).getElements();
for (int i = elements.length - 1; i >= 0; i--) {
syntaxStack.addFirst(elements[i]);
}
break;
case ALTERNATION:
MuSyntax[] choices = ((MuAlternation) syntax).getAlternatives();
// The test below optimizes the case where there is only one
// alternative. This avoids the non-trivial cost of creating
// a choicepoint, backtracking, etc.
if (choices.length > 1) {
ChoicePoint choicePoint =
new ChoicePoint(source.tell(), syntaxStack, choices);
backtrackStack.addFirst(choicePoint);
syntaxStack = new SharedStack<MuSyntax>(syntaxStack);
if (DEBUG) {
log.debug("pushed choicePoint #" + backtrackStack.size() +
" - " + choicePoint);
}
}
if (choices[0] != null) {
syntaxStack.addFirst(choices[0]);
}
if (DEBUG) {
log.debug("syntaxStack " + showStack(syntaxStack, true));
}
break;
case BACK_REFERENCE:
throw new SyntaxFailureException(
"Found an unresolved MuBackReference");
default:
throw new SyntaxFailureException(
"Unknown MuSyntax kind (" + syntax.getKind() + ")");
}
}
if (backtrack) {
if (DEBUG) {
log.debug("backtracking ...");
}
while (!backtrackStack.isEmpty()) {
ChoicePoint choicePoint = backtrackStack.getFirst();
if (DEBUG) {
log.debug("top choicePoint - " + choicePoint);
log.debug("syntaxStack " + showStack(syntaxStack, true));
}
// Issue undo's for any argument values added.
for (Argument<?> arg : choicePoint.argsModified) {
if (DEBUG) {
log.debug("undo for arg " + arg.getLabel());
}
arg.undoLastValue();
}
// If possible, take the next choice in the current choice
// point and stop backtracking
int lastChoice = choicePoint.choices.length - 1;
int choiceNo = ++choicePoint.choiceNo;
if (choiceNo <= lastChoice) {
MuSyntax choice = choicePoint.choices[choiceNo];
choicePoint.argsModified.clear();
source.seek(choicePoint.sourcePos);
// (If this is the last choice in the choice point, we
// won't need to use this choice point's saved syntax
// stack again ...)
if (choiceNo == lastChoice) {
syntaxStack = choicePoint.syntaxStack;
} else {
syntaxStack = new SharedStack<MuSyntax>(choicePoint.syntaxStack);
}
if (choice != null) {
syntaxStack.addFirst(choice);
}
backtrack = false;
if (DEBUG) {
log.debug("taking choice #" + choiceNo);
log.debug("syntaxStack : " + showStack(syntaxStack, true));
}
break;
}
// Otherwise, pop the choice point and keep going.
if (DEBUG) {
log.debug("popped choice point #" + backtrackStack.size());
}
backtrackStack.removeFirst();
}
// If we are still backtracking and we are out of choices ...
if (backtrack) {
if (completions == null) {
throw new CommandSyntaxException("ran out of alternatives", argFailures);
} else {
if (DEBUG) {
log.debug("end completion");
}
return;
}
}
if (DEBUG) {
log.debug("end backtracking");
}
}
}
}
private static String showStack(Deque<MuSyntax> stack, boolean oneLine) {
StringBuilder sb = new StringBuilder();
for (MuSyntax syntax : stack) {
if (sb.length() > 0) {
sb.append(", ");
if (!oneLine) {
sb.append("\n ");
}
}
sb.append(syntax.format());
}
return sb.toString();
}
}