The expression parser can now parse more than a simple expression

- Added sequencing (; and {}).
- Added =, +=, -=, *=, /=, %=, ^= to the expression parser. (left-associative for now, will change later)
- Added pre-increment(++) and pre-decrement(--) operators.
- Adjusted/added tests.
This commit is contained in:
TomyLobo 2011-10-23 05:15:23 +02:00
parent 9c070c323f
commit ee79abff67
9 changed files with 345 additions and 19 deletions

View File

@ -78,4 +78,8 @@ public class Expression {
public String toString() {
return root.toString();
}
public Invokable getVariable(String name) {
return variables.get(name);
}
}

View File

@ -20,6 +20,7 @@
package com.sk89q.worldedit.expression.lexer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -43,15 +44,28 @@ public class Lexer {
}
private final DecisionTree operatorTree = new DecisionTree(null,
'-', new DecisionTree("-"),
'+', new DecisionTree("+"),
'*', new DecisionTree("*",
'*', new DecisionTree("^")
'+', new DecisionTree("+",
'=', new DecisionTree("+="),
'+', new DecisionTree("++")
),
'/', new DecisionTree("/"),
'%', new DecisionTree("%"),
'^', new DecisionTree("^"),
'=', new DecisionTree(null, // not implemented
'-', new DecisionTree("-",
'=', new DecisionTree("-="),
'-', new DecisionTree("--")
),
'*', new DecisionTree("*",
'=', new DecisionTree("*="),
'*', new DecisionTree("**")
),
'/', new DecisionTree("/",
'=', new DecisionTree("/=")
),
'%', new DecisionTree("%",
'=', new DecisionTree("%=")
),
'^', new DecisionTree("^",
'=', new DecisionTree("^=")
),
'=', new DecisionTree("=",
'=', new DecisionTree("==")
),
'!', new DecisionTree("!",
@ -81,8 +95,13 @@ public class Lexer {
characterTokens.add(',');
characterTokens.add('(');
characterTokens.add(')');
characterTokens.add('{');
characterTokens.add('}');
characterTokens.add(';');
}
private static final Set<String> keywords = new HashSet<String>(Arrays.asList("if", "else"));
private static final Pattern numberPattern = Pattern.compile("^([0-9]*(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)");
private static final Pattern identifierPattern = Pattern.compile("^([A-Za-z][0-9A-Za-z_]*)");
@ -128,7 +147,12 @@ public class Lexer {
if (identifierMatcher.lookingAt()) {
String identifierPart = identifierMatcher.group(1);
if (!identifierPart.isEmpty()) {
tokens.add(new IdentifierToken(position, identifierPart));
if (keywords.contains(identifierPart)) {
tokens.add(new KeywordToken(position, identifierPart));
}
else {
tokens.add(new IdentifierToken(position, identifierPart));
}
position += identifierPart.length();
continue;

View File

@ -0,0 +1,39 @@
// $Id$
/*
* WorldEdit
* Copyright (C) 2010, 2011 sk89q <http://www.sk89q.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 com.sk89q.worldedit.expression.lexer.tokens;
public class KeywordToken extends Token {
public final String value;
public KeywordToken(int position, String value) {
super(position);
this.value = value;
}
@Override
public char id() {
return 'i';
}
@Override
public String toString() {
return "KeywordToken(" + value + ")";
}
}

View File

@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map;
import com.sk89q.worldedit.expression.Identifiable;
import com.sk89q.worldedit.expression.lexer.tokens.CharacterToken;
import com.sk89q.worldedit.expression.lexer.tokens.IdentifierToken;
import com.sk89q.worldedit.expression.lexer.tokens.NumberToken;
import com.sk89q.worldedit.expression.lexer.tokens.OperatorToken;
@ -36,6 +37,7 @@ import com.sk89q.worldedit.expression.runtime.Constant;
import com.sk89q.worldedit.expression.runtime.Functions;
import com.sk89q.worldedit.expression.runtime.Invokable;
import com.sk89q.worldedit.expression.runtime.Operators;
import com.sk89q.worldedit.expression.runtime.Sequence;
public class Parser {
private final class NullToken extends Token {
@ -66,7 +68,7 @@ public class Parser {
}
private Invokable parse() throws ParserException {
final Invokable ret = parseInternal();
final Invokable ret = parseInternal(true);
if (position < tokens.size()) {
final Token token = peek();
throw new ParserException(token.getPosition(), "Extra token at the end of the input: " + token);
@ -74,7 +76,7 @@ public class Parser {
return ret;
}
private final Invokable parseInternal() throws ParserException {
private final Invokable parseInternal(boolean isStatement) throws ParserException {
LinkedList<Identifiable> halfProcessed = new LinkedList<Identifiable>();
// process brackets, numbers, functions, variables and detect prefix operators
@ -112,8 +114,15 @@ public class Parser {
expressionStart = false;
break;
case '{':
halfProcessed.add(parseBlock());
halfProcessed.add(new CharacterToken(-1, ';'));
expressionStart = false;
break;
case ',':
case ')':
case '}':
break loop;
case 'o':
@ -135,8 +144,57 @@ public class Parser {
}
}
// process binary operators
return processBinaryOps(halfProcessed, binaryOpMaps.length - 1);
if (isStatement) {
return processStatement(halfProcessed);
}
else {
// process binary operators
return processExpression(halfProcessed);
}
}
private Invokable processStatement(LinkedList<Identifiable> input) throws ParserException {
LinkedList<Identifiable> lhs = new LinkedList<Identifiable>();
LinkedList<Identifiable> rhs = new LinkedList<Identifiable>();
boolean semicolonFound = false;
for (Iterator<Identifiable> it = input.descendingIterator(); it.hasNext();) {
Identifiable identifiable = it.next();
if (semicolonFound) {
lhs.addFirst(identifiable);
}
else {
if (identifiable.id() == ';') {
semicolonFound = true;
}
else {
rhs.addFirst(identifiable);
}
}
}
if (lhs.isEmpty()) {
if (rhs.isEmpty()) {
return new Sequence(semicolonFound ? input.get(0).getPosition() : -1);
}
return processExpression(rhs);
}
else if (rhs.isEmpty()) {
return processStatement(lhs);
}
else {
assert(semicolonFound);
Invokable rhsInvokable = processExpression(rhs);
Invokable lhsInvokable = processStatement(lhs);
return new Sequence(position, lhsInvokable, rhsInvokable);
}
}
private Invokable processExpression(LinkedList<Identifiable> input) throws ParserException {
return processBinaryOps(input, binaryOpMaps.length - 1);
}
private static final Map<String, String>[] binaryOpMaps;
@ -146,6 +204,7 @@ public class Parser {
final Object[][][] binaryOps = {
{
{ "^", "pow" },
{ "**", "pow" },
},
{
{ "*", "mul" },
@ -177,6 +236,15 @@ public class Parser {
{
{ "||", "or" },
},
{
{ "=", "ass" },
{ "+=", "aadd" },
{ "-=", "asub" },
{ "*=", "amul" },
{ "/=", "adiv" },
{ "%=", "amod" },
{ "^=", "aexp" },
},
};
@SuppressWarnings("unchecked")
@ -205,6 +273,8 @@ public class Parser {
unaryOpMap.put("-", "neg");
unaryOpMap.put("!", "not");
unaryOpMap.put("~", "inv");
unaryOpMap.put("++", "inc");
unaryOpMap.put("--", "dec");
}
private Invokable processBinaryOps(LinkedList<Identifiable> input, int level) throws ParserException {
@ -315,7 +385,7 @@ public class Parser {
List<Invokable> args = new ArrayList<Invokable>();
loop: while (true) {
args.add(parseInternal());
args.add(parseInternal(false));
final Token current = peek();
++position;
@ -345,7 +415,7 @@ public class Parser {
}
++position;
final Invokable ret = parseInternal();
final Invokable ret = parseInternal(false);
if (peek().id() != ')') {
throw new ParserException(peek().getPosition(), "Unmatched opening bracket");
@ -354,4 +424,20 @@ public class Parser {
return ret;
}
private final Invokable parseBlock() throws ParserException {
if (peek().id() != '{') {
throw new ParserException(peek().getPosition(), "Unexpected character in parseBlock");
}
++position;
final Invokable ret = parseInternal(true);
if (peek().id() != '}') {
throw new ParserException(peek().getPosition(), "Unmatched opening brace");
}
++position;
return ret;
}
}

View File

@ -0,0 +1,28 @@
// $Id$
/*
* WorldEdit
* Copyright (C) 2010, 2011 sk89q <http://www.sk89q.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 com.sk89q.worldedit.expression.runtime;
public abstract class Assignable extends Invokable {
public Assignable(int position) {
super(position);
}
public abstract double assign(double value) throws EvaluationException;
}

View File

@ -21,10 +21,22 @@ package com.sk89q.worldedit.expression.runtime;
public final class Operators {
public static final Function getOperator(int position, String name, Invokable lhs, Invokable rhs) throws NoSuchMethodException {
if (lhs instanceof Assignable) {
try {
return new Function(position, Operators.class.getMethod(name, Assignable.class, Invokable.class), lhs, rhs);
}
catch (NoSuchMethodException e) {}
}
return new Function(position, Operators.class.getMethod(name, Invokable.class, Invokable.class), lhs, rhs);
}
public static final Function getOperator(int position, String name, Invokable argument) throws NoSuchMethodException {
if (argument instanceof Assignable) {
try {
return new Function(position, Operators.class.getMethod(name, Assignable.class), argument);
}
catch (NoSuchMethodException e) {}
}
return new Function(position, Operators.class.getMethod(name, Invokable.class), argument);
}
@ -116,6 +128,43 @@ public final class Operators {
}
public static final double ass(Assignable lhs, Invokable rhs) throws EvaluationException {
return lhs.assign(rhs.invoke());
}
public static final double aadd(Assignable lhs, Invokable rhs) throws EvaluationException {
return lhs.assign(lhs.invoke() + rhs.invoke());
}
public static final double asub(Assignable lhs, Invokable rhs) throws EvaluationException {
return lhs.assign(lhs.invoke() - rhs.invoke());
}
public static final double amul(Assignable lhs, Invokable rhs) throws EvaluationException {
return lhs.assign(lhs.invoke() * rhs.invoke());
}
public static final double adiv(Assignable lhs, Invokable rhs) throws EvaluationException {
return lhs.assign(lhs.invoke() / rhs.invoke());
}
public static final double amod(Assignable lhs, Invokable rhs) throws EvaluationException {
return lhs.assign(lhs.invoke() % rhs.invoke());
}
public static final double aexp(Assignable lhs, Invokable rhs) throws EvaluationException {
return lhs.assign(Math.pow(lhs.invoke(), rhs.invoke()));
}
public static final double inc(Assignable x) throws EvaluationException {
return x.assign(x.invoke() + 1);
}
public static final double dec(Assignable x) throws EvaluationException {
return x.assign(x.invoke() - 1);
}
// Usable AlmostEqual function, based on http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm
private static boolean almostEqual2sComplement(double A, double B, long maxUlps) {
// Make sure maxUlps is non-negative and small enough that the

View File

@ -0,0 +1,82 @@
// $Id$
/*
* WorldEdit
* Copyright (C) 2010, 2011 sk89q <http://www.sk89q.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 com.sk89q.worldedit.expression.runtime;
import java.util.ArrayList;
import java.util.List;
public class Sequence extends Invokable {
private final Invokable[] sequence;
public Sequence(int position, Invokable... sequence) {
super(position);
this.sequence = sequence;
}
@Override
public char id() {
// TODO Auto-generated method stub
return 0;
}
@Override
public double invoke() throws EvaluationException {
double ret = 0;
for (Invokable invokable : sequence) {
ret = invokable.invoke();
}
return ret;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("seq(");
boolean first = true;
for (Invokable invokable : sequence) {
if (!first) {
sb.append(", ");
}
sb.append(invokable);
first = false;
}
return sb.append(')').toString();
}
@Override
public Invokable optimize() throws EvaluationException {
List<Invokable> newSequence = new ArrayList<Invokable>();
for (Invokable invokable : sequence) {
invokable = invokable.optimize();
if (invokable instanceof Sequence) {
for (Invokable subInvokable : ((Sequence) invokable).sequence) {
newSequence.add(subInvokable);
}
}
else {
newSequence.add(invokable);
}
}
return new Sequence(getPosition(), newSequence.toArray(new Invokable[newSequence.size()]));
}
}

View File

@ -19,7 +19,7 @@
package com.sk89q.worldedit.expression.runtime;
public final class Variable extends Invokable {
public final class Variable extends Assignable {
public double value;
public Variable(double value) {
@ -41,4 +41,9 @@ public final class Variable extends Invokable {
public char id() {
return 'v';
}
@Override
public double assign(double value) {
return this.value = value;
}
}

View File

@ -15,8 +15,8 @@ public class ExpressionTest {
assertEquals(1-2+3, simpleEval("1-2+3"), 0);
// check unary ops
assertEquals(2+ +4, simpleEval("2++4"), 0);
assertEquals(2- -4, simpleEval("2--4"), 0);
assertEquals(2+ +4, simpleEval("2+ +4"), 0);
assertEquals(2- -4, simpleEval("2- -4"), 0);
assertEquals(2*-4, simpleEval("2*-4"), 0);
// check functions
@ -31,7 +31,7 @@ public class ExpressionTest {
public void testErrors() throws ExpressionException {
// test lexer errors
try {
Expression.compile("{");
Expression.compile("#");
fail("Error expected");
} catch (LexerException e) {
assertEquals("Error position", 0, e.getPosition());
@ -60,6 +60,15 @@ public class ExpressionTest {
} catch (ParserException e) {}
}
@Test
public void testAssign() throws ExpressionException {
Expression foo = Expression.compile("{a=x} b=y; c=z", "x", "y", "z", "a", "b", "c");
foo.evaluate(2, 3, 5);
assertEquals(2, foo.getVariable("a").invoke(), 0);
assertEquals(3, foo.getVariable("b").invoke(), 0);
assertEquals(5, foo.getVariable("c").invoke(), 0);
}
private double simpleEval(String expression) throws Exception {
return Expression.compile(expression).evaluate();
}