package bjc.pratt; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import bjc.pratt.commands.CommandResult; import bjc.pratt.commands.CommandResult.Status; import bjc.pratt.commands.InitialCommand; import bjc.pratt.commands.MetaInitialCommand; import bjc.pratt.commands.MetaNonInitialCommand; import bjc.pratt.commands.NonInitialCommand; import bjc.pratt.commands.impls.DefaultInitialCommand; import bjc.pratt.commands.impls.DefaultNonInitialCommand; import bjc.pratt.tokens.ExpectionNotMet; import bjc.pratt.tokens.Token; import bjc.pratt.tokens.TokenStream; import bjc.data.TransformIterator; import bjc.data.Tree; import bjc.utils.funcutils.NumberUtils; import bjc.utils.parserutils.ParserException; /** * A configurable Pratt parser for expressions. * * @author EVE * * @param The key type for the tokens. * * @param The value type for the tokens. * * @param The state type of the parser. * * */ public class PrattParser { /* * Default commands that error when used. */ private final NonInitialCommand DEFAULT_LEFT_COMMAND = new DefaultNonInitialCommand<>(); private final List> DEFAULT_LEFT_LIST = Arrays.asList(DEFAULT_LEFT_COMMAND); private final InitialCommand DEFAULT_NULL_COMMAND = new DefaultInitialCommand<>(); private final List> DEFAULT_NULL_LIST = Arrays.asList(DEFAULT_NULL_COMMAND); /* * Left-commands that depend on what the null command was. */ private final Map>>> dependantLeftCommands; private final Map>>> dependantMetaLeftCommands; /* * The left commands. */ private final Map>> leftCommands; private final Map>> metaLeftCommands; /* * The initial commands. */ private final Map>> nullCommands; private final Map>> metaNullCommands; /* * Initial commands only checked for statements. */ private final Map>> statementCommands; private final Map>> metaStatementCommands; /** * Create a new Pratt parser. * */ public PrattParser() { dependantLeftCommands = new HashMap<>(); dependantMetaLeftCommands = new HashMap<>(); leftCommands = new HashMap<>(); metaLeftCommands = new HashMap<>(); nullCommands = new HashMap<>(); metaNullCommands = new HashMap<>(); statementCommands = new HashMap<>(); metaStatementCommands = new HashMap<>(); } /** * Parse an expression. * * @param precedence The initial precedence for the expression. * * @param tokens The tokens for the expression. * * @param state The state of the parser. * * @param isStatement Whether or not to parse statements. * * @return The expression as an AST. * * @throws ParserException If something goes wrong during parsing. */ public CommandResult parseExpression(final int precedence, final TokenStream tokens, final C state, final boolean isStatement) throws ParserException { if (precedence < 0) throw new IllegalArgumentException("Precedence must be greater than zero"); ParserContext parserContext = new ParserContext<>(tokens, this, state); Tree> ast = null; CommandResult result; tokens.mark(); final Token initToken = tokens.current(); tokens.next(); tokens.mark(); final K initKey = initToken.getKey(); Iterator> nullCommandIter = getInitialCommand(isStatement, initKey, parserContext); do { if (!nullCommandIter.hasNext()) { // Restore to the state we were in before we tried to parse this token. // Need the commit because rollback doesn't remove marks tokens.rollback(); tokens.commit(); tokens.rollback(); return CommandResult.backtrack(); } InitialCommand nullCommand = nullCommandIter.next(); try { result = nullCommand.denote(initToken, parserContext); } catch (ExpectionNotMet enm) { // TODO: Should enm be used for something here? result = CommandResult.backtrack(); } switch (result.status) { case SUCCESS: ast = result.success(); break; case FAIL: return result; case BACKTRACK: tokens.rollback(); break; default: throw new IllegalStateException("Unhandled result status " + result.status); } parserContext.initial = initKey; } while (result.status != Status.SUCCESS); tokens.commit(); // Think this is right... // Will get rid of all our active marks. tokens.commit(); int rightPrec = Integer.MAX_VALUE; outer: while (true) { tokens.mark(); final Token tok = tokens.current(); final K key = tok.getKey(); Iterator> leftCommandIter = getNonInitialCommand(key, parserContext); do { if (!leftCommandIter.hasNext()) { // Restore to the state we were in before we tried to parse this token. // Need the commit because rollback doesn't remove marks tokens.rollback(); tokens.commit(); tokens.rollback(); return CommandResult.backtrack(); } NonInitialCommand leftCommand = leftCommandIter.next(); final int leftBind = leftCommand.leftBinding(); if (NumberUtils.between(precedence, rightPrec, leftBind)) { tokens.next(); tokens.mark(); try { result = leftCommand.denote(ast, tok, parserContext); rightPrec = leftCommand.nextBinding(); } catch (ExpectionNotMet enm) { result = CommandResult.backtrack(); } switch (result.status) { case SUCCESS: tokens.commit(); tokens.commit(); ast = result.success(); break; case FAIL: return result; case BACKTRACK: tokens.rollback(); break; default: throw new IllegalStateException("Unhandled result status " + result.status); } } else { tokens.commit(); break outer; } } while (result.status != Status.SUCCESS); } return CommandResult.success(ast); } /** * Add a non-initial command to this parser. * * @param marker The key that marks the command. * * @param comm The command. */ public void addNonInitialCommand(final K marker, final NonInitialCommand comm) { leftCommands.computeIfAbsent(marker, mrk -> new ArrayList<>()).add(comm); } /** * Add a initial command to this parser. * * @param marker The key that marks the command. * * @param comm The command. */ public void addInitialCommand(final K marker, final InitialCommand comm) { nullCommands.computeIfAbsent(marker, mrk -> new ArrayList<>()).add(comm); } /** * Add a statement command to this parser. * * The difference between statements and initial commands is that statements can * only appear at the start of the expression. * * @param marker The key that marks the command. * * @param comm The command. */ public void addStatementCommand(final K marker, final InitialCommand comm) { statementCommands.computeIfAbsent(marker, mrk -> new ArrayList<>()).add(comm); } /** * Add a dependent non-initial command to this parser. * * @param dependant The dependent that precedes the command. * * @param marker The token key that marks the command. * * @param comm The command. */ public void addDependantCommand(final K dependant, final K marker, final NonInitialCommand comm) { Map>> dependantMap = dependantLeftCommands.getOrDefault(dependant, new HashMap<>()); dependantMap.computeIfAbsent(marker, mrk -> new ArrayList<>()).add(comm); } /** * Lookup an initial command. * * @param isStatement Whether to look for statement commands or not. * * @param key The key of the command. * * @param ctx The context for meta-commands. * * @return A command attached to that key, or a default implementation. */ public Iterator> getInitialCommand(boolean isStatement, K key, ParserContext ctx) { if (isStatement) { if (metaStatementCommands.containsKey(key)) { List> lst = metaStatementCommands.get(key); return new TransformIterator<>(lst.iterator(), (itm) -> itm.getCommand(ctx)); } else if (statementCommands.containsKey(key)) { return statementCommands.get(key).iterator(); } } if (metaNullCommands.containsKey(key)) { List> lst = metaNullCommands.get(key); return new TransformIterator<>(lst.iterator(), (itm) -> itm.getCommand(ctx)); } return nullCommands.getOrDefault(key, DEFAULT_NULL_LIST).iterator(); } /** * Lookup a non-initial command. * * @param key The key of the command. * * @param ctx The context for meta-commands. * * @return A command attached to that key, or a default implementation. */ public Iterator> getNonInitialCommand(K key, ParserContext ctx) { if (dependantMetaLeftCommands.containsKey(ctx.initial)) { Map>> dependantCommands = dependantMetaLeftCommands.get(ctx.initial); if (dependantCommands.containsKey(key)) { List> lst = dependantCommands.get(key); return new TransformIterator<>(lst.iterator(), (itm) -> itm.getCommand(ctx)); } } if (dependantLeftCommands.containsKey(ctx.initial)) { Map>> dependantCommands = dependantLeftCommands.get(ctx.initial); if (dependantCommands.containsKey(key)) { return dependantCommands.getOrDefault(key, DEFAULT_LEFT_LIST).iterator(); } } if (metaLeftCommands.containsKey(key)) { List> lst = metaLeftCommands.get(key); return new TransformIterator<>(lst.iterator(), (itm) -> itm.getCommand(ctx)); } return leftCommands.getOrDefault(key, DEFAULT_LEFT_LIST).iterator(); } }