Added an expression parser.

This commit is contained in:
TomyLobo 2011-10-02 12:29:56 +02:00
parent 99002c786b
commit d93d85cd37
22 changed files with 1658 additions and 0 deletions

View File

@ -0,0 +1,81 @@
// $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;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.sk89q.worldedit.expression.lexer.Lexer;
import com.sk89q.worldedit.expression.lexer.tokens.Token;
import com.sk89q.worldedit.expression.parser.Parser;
import com.sk89q.worldedit.expression.parser.ParserException;
import com.sk89q.worldedit.expression.runtime.Constant;
import com.sk89q.worldedit.expression.runtime.EvaluationException;
import com.sk89q.worldedit.expression.runtime.Invokable;
import com.sk89q.worldedit.expression.runtime.Variable;
public class Expression {
private final Map<String, Invokable> variables = new HashMap<String, Invokable>();
private final String[] variableNames;
private Invokable root;
public static Expression compile(String expression, String... variableNames) throws ExpressionException {
return new Expression(expression, variableNames);
}
private Expression(String expression, String... variableNames) throws ExpressionException {
this(Lexer.tokenize(expression), variableNames);
}
private Expression(List<Token> tokens, String... variableNames) throws ParserException {
this.variableNames = variableNames;
variables.put("e", new Constant(-1, Math.E));
variables.put("pi", new Constant(-1, Math.PI));
for (String variableName : variableNames) {
variables.put(variableName, new Variable(0));
}
root = Parser.parse(tokens, variables);
}
public double evaluate(double... values) throws EvaluationException {
for (int i = 0; i < values.length; ++i) {
final String variableName = variableNames[i];
final Invokable invokable = variables.get(variableName);
if (!(invokable instanceof Variable)) {
throw new EvaluationException(invokable.getPosition(), "Tried to assign constant " + variableName + ".");
}
((Variable) invokable).value = values[i];
}
return root.invoke();
}
public void optimize() throws EvaluationException {
root = root.optimize();
}
@Override
public String toString() {
return root.toString();
}
}

View File

@ -0,0 +1,49 @@
// $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;
public class ExpressionException extends Exception {
private static final long serialVersionUID = 1L;
private final int position;
public ExpressionException(int position) {
this.position = position;
}
public ExpressionException(int position, String message, Throwable cause) {
super(message, cause);
this.position = position;
}
public ExpressionException(int position, String message) {
super(message);
this.position = position;
}
public ExpressionException(int position, Throwable cause) {
super(cause);
this.position = position;
}
public int getPosition() {
return position;
}
}

View File

@ -0,0 +1,46 @@
// $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;
public interface Identifiable {
/**
* Returns a character that helps identify the token, pseudo-token or invokable in question.
*
* <pre>
* Tokens:
* i - IdentifierToken
* 0 - NumberToken
* o - OperatorToken
* \0 - NullToken
* CharacterTokens are returned literally
*
* PseudoTokens:
* p - PrefixOperator
*
* Invokables:
* c - Constant
* f - Function
* v - Variable
* </pre>
*/
public abstract char id();
public int getPosition();
}

View File

@ -0,0 +1,197 @@
// $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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.sk89q.worldedit.expression.lexer.tokens.*;
public class Lexer {
private final String expression;
private int position = 0;
private Lexer(String expression) {
this.expression = expression;
}
public static final List<Token> tokenize(String expression) throws LexerException {
return new Lexer(expression).tokenize();
}
private final DecisionTree operatorTree = new DecisionTree(null,
'-', 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(null, // not implemented
'&', new DecisionTree("&&")
),
'|', new DecisionTree(null, // not implemented
'|', new DecisionTree("||")
),
'~', new DecisionTree("~",
'=', new DecisionTree("~=")
)
);
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_]*)");
private final List<Token> tokenize() throws LexerException {
List<Token> tokens = new ArrayList<Token>();
do {
skipWhitespace();
if (position >= expression.length()) {
break;
}
Token token = operatorTree.evaluate(position);
if (token != null) {
tokens.add(token);
continue;
}
final char ch = peek();
switch (ch) {
case ',':
case '(':
case ')':
tokens.add(new CharacterToken(position++, ch));
break;
default:
final Matcher numberMatcher = numberPattern.matcher(expression.substring(position));
if (numberMatcher.lookingAt()) {
String numberPart = numberMatcher.group(1);
if (!numberPart.isEmpty()) {
try {
tokens.add(new NumberToken(position, Double.parseDouble(numberPart)));
}
catch (NumberFormatException e) {
throw new LexerException(position, "Number parsing failed", e);
}
position += numberPart.length();
continue;
}
}
final Matcher identifierMatcher = identifierPattern.matcher(expression.substring(position));
if (identifierMatcher.lookingAt()) {
String identifierPart = identifierMatcher.group(1);
if (!identifierPart.isEmpty()) {
tokens.add(new IdentifierToken(position, identifierPart));
position += identifierPart.length();
continue;
}
}
throw new LexerException(position, "Unknown character '" + ch + "'");
}
}
while (position < expression.length());
return tokens;
}
private char peek() {
return expression.charAt(position);
}
private final void skipWhitespace() {
while (position < expression.length() && Character.isWhitespace(peek())) {
++position;
}
}
public class DecisionTree {
private final String tokenName;
private final Map<Character, DecisionTree> subTrees = new HashMap<Character, Lexer.DecisionTree>();
private DecisionTree(String tokenName, Object... args) {
this.tokenName = tokenName;
if (args.length % 2 != 0) {
throw new UnsupportedOperationException("You need to pass an even number of arguments.");
}
for (int i = 0; i < args.length; i += 2) {
if (!(args[i] instanceof Character)) {
throw new UnsupportedOperationException("Argument #" + i + " expected to be 'Character', not '" + args[i].getClass().getName() + "'.");
}
if (!(args[i + 1] instanceof DecisionTree)) {
throw new UnsupportedOperationException("Argument #" + (i + 1) + " expected to be 'DecisionTree', not '" + args[i + 1].getClass().getName() + "'.");
}
Character next = (Character) args[i];
DecisionTree subTree = (DecisionTree) args[i + 1];
subTrees.put(next, subTree);
}
}
private Token evaluate(int startPosition) throws LexerException {
if (position < expression.length()) {
final char next = peek();
final DecisionTree subTree = subTrees.get(next);
if (subTree != null) {
++position;
final Token subTreeResult = subTree.evaluate(startPosition);
if (subTreeResult != null) {
return subTreeResult;
}
--position;
}
}
if (tokenName == null) {
return null;
}
return new OperatorToken(startPosition, tokenName);
}
}
}

View File

@ -0,0 +1,46 @@
// $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;
import com.sk89q.worldedit.expression.ExpressionException;
public class LexerException extends ExpressionException {
private static final long serialVersionUID = 1L;
public LexerException(int position) {
super(position, getPrefix(position));
}
public LexerException(int position, String message, Throwable cause) {
super(position, getPrefix(position) + ": " + message, cause);
}
public LexerException(int position, String message) {
super(position, getPrefix(position) + ": " + message);
}
public LexerException(int position, Throwable cause) {
super(position, getPrefix(position), cause);
}
private static String getPrefix(int position) {
return position < 0 ? "Lexer error" : ("Lexer error at " + (position + 1));
}
}

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 CharacterToken extends Token {
public final char character;
public CharacterToken(int position, char character) {
super(position);
this.character = character;
}
@Override
public char id() {
return character;
}
@Override
public String toString() {
return "CharacterToken(" + character + ")";
}
}

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 IdentifierToken extends Token {
public final String value;
public IdentifierToken(int position, String value) {
super(position);
this.value = value;
}
@Override
public char id() {
return 'i';
}
@Override
public String toString() {
return "IdentifierToken(" + value + ")";
}
}

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 NumberToken extends Token {
public final double value;
public NumberToken(int position, double value) {
super(position);
this.value = value;
}
@Override
public char id() {
return '0';
}
@Override
public String toString() {
return "NumberToken(" + value + ")";
}
}

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 OperatorToken extends Token {
public final String operator;
public OperatorToken(int position, String operator) {
super(position);
this.operator = operator;
}
@Override
public char id() {
return 'o';
}
@Override
public String toString() {
return "OperatorToken(" + operator + ")";
}
}

View File

@ -0,0 +1,35 @@
// $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;
import com.sk89q.worldedit.expression.Identifiable;
public abstract class Token implements Identifiable {
private final int position;
public Token(int position) {
this.position = position;
}
@Override
public int getPosition() {
return position;
}
}

View File

@ -0,0 +1,357 @@
// $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.parser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.sk89q.worldedit.expression.Identifiable;
import com.sk89q.worldedit.expression.lexer.tokens.IdentifierToken;
import com.sk89q.worldedit.expression.lexer.tokens.NumberToken;
import com.sk89q.worldedit.expression.lexer.tokens.OperatorToken;
import com.sk89q.worldedit.expression.lexer.tokens.Token;
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;
public class Parser {
private final class NullToken extends Token {
private NullToken(int position) {
super(position);
}
public char id() {
return '\0';
}
public String toString() {
return "NullToken";
}
}
private final List<Token> tokens;
private int position = 0;
private Map<String, Invokable> variables;
private Parser(List<Token> tokens, Map<String, Invokable> variables) {
this.tokens = tokens;
this.variables = variables;
}
public static final Invokable parse(List<Token> tokens, Map<String, Invokable> variables) throws ParserException {
return new Parser(tokens, variables).parse();
}
private Invokable parse() throws ParserException {
final Invokable ret = parseInternal();
if (position < tokens.size()) {
final Token token = peek();
throw new ParserException(token.getPosition(), "Extra token at the end of the input: " + token);
}
return ret;
}
private final Invokable parseInternal() throws ParserException {
LinkedList<Identifiable> halfProcessed = new LinkedList<Identifiable>();
// process brackets, numbers, functions, variables and detect prefix operators
boolean expressionStart = true;
loop: while (position < tokens.size()) {
final Token current = peek();
switch (current.id()) {
case '0':
halfProcessed.add(new Constant(current.getPosition(), ((NumberToken) current).value));
++position;
expressionStart = false;
break;
case 'i':
final IdentifierToken identifierToken = (IdentifierToken) current;
++position;
final Token next = peek();
if (next.id() == '(') {
halfProcessed.add(parseFunction(identifierToken));
}
else {
Invokable variable = variables.get(identifierToken.value);
if (variable == null) {
throw new ParserException(current.getPosition(), "Variable '" + identifierToken.value + "' not found");
}
halfProcessed.add(variable);
}
expressionStart = false;
break;
case '(':
halfProcessed.add(parseBracket());
expressionStart = false;
break;
case ',':
case ')':
break loop;
case 'o':
if (expressionStart) {
halfProcessed.add(new PrefixOperator((OperatorToken) current));
}
else {
halfProcessed.add(current);
}
++position;
expressionStart = true;
break;
default:
halfProcessed.add(current);
++position;
expressionStart = false;
break;
}
}
// process binary operators
return processBinaryOps(halfProcessed, binaryOpMaps.length - 1);
}
private static final Map<String, String>[] binaryOpMaps;
private static final Map<String, String> unaryOpMap = new HashMap<String, String>();
static {
final Object[][][] binaryOps = {
{
{ "^", "pow" },
},
{
{ "*", "mul" },
{ "/", "div" },
{ "%", "mod" },
},
{
{ "+", "add" },
{ "-", "sub" },
},
{
{ "<<", "shl" },
{ ">>", "shr" },
},
{
{ "<", "lth" },
{ ">", "gth" },
{ "<=", "leq" },
{ ">=", "geq" },
},
{
{ "==", "equ" },
{ "!=", "neq" },
{ "~=", "near" },
},
{
{ "&&", "and" },
},
{
{ "||", "or" },
},
};
@SuppressWarnings("unchecked")
final Map<String, String>[] tmp = binaryOpMaps = new Map[binaryOps.length];
for (int i = 0; i < binaryOps.length; ++i) {
final Object[][] a = binaryOps[i];
switch (a.length) {
case 0:
tmp[i] = Collections.emptyMap();
break;
case 1:
final Object[] first = a[0];
tmp[i] = Collections.singletonMap((String) first[0], (String) first[1]);
break;
default:
Map<String, String> m = tmp[i] = new HashMap<String, String>();
for (int j = 0; j < a.length; ++j) {
final Object[] element = a[j];
m.put((String) element[0], (String) element[1]);
}
}
}
unaryOpMap.put("-", "neg");
unaryOpMap.put("!", "not");
unaryOpMap.put("~", "inv");
}
private Invokable processBinaryOps(LinkedList<Identifiable> input, int level) throws ParserException {
if (level < 0) {
return processUnaryOps(input);
}
LinkedList<Identifiable> lhs = new LinkedList<Identifiable>();
LinkedList<Identifiable> rhs = new LinkedList<Identifiable>();
String operator = null;
for (Iterator<Identifiable> it = input.descendingIterator(); it.hasNext();) {
Identifiable identifiable = it.next();
if (operator == null) {
rhs.addFirst(identifiable);
if (rhs.isEmpty()) {
continue;
}
if (!(identifiable instanceof OperatorToken)) {
continue;
}
operator = binaryOpMaps[level].get(((OperatorToken) identifiable).operator);
if (operator == null) {
continue;
}
rhs.removeFirst();
}
else {
lhs.addFirst(identifiable);
}
}
Invokable rhsInvokable = processBinaryOps(rhs, level - 1);
if (operator == null) return rhsInvokable;
Invokable lhsInvokable = processBinaryOps(lhs, level);
try {
return Operators.getOperator(-1, operator, lhsInvokable, rhsInvokable); // TODO: get real position
}
catch (NoSuchMethodException e) {
final Token operatorToken = (Token) input.get(lhs.size());
throw new ParserException(operatorToken.getPosition(), "Couldn't find operator '" + operator + "'");
}
}
private Invokable processUnaryOps(LinkedList<Identifiable> input) throws ParserException {
if (input.isEmpty()) {
throw new ParserException(-1, "Expression missing.");
}
Invokable ret = (Invokable) input.removeLast();
while (!input.isEmpty()) {
final Identifiable last = input.removeLast();
if (last instanceof PrefixOperator) {
final String operator = ((PrefixOperator) last).operator;
if (operator.equals("+")) {
continue;
}
String opName = unaryOpMap.get(operator);
if (opName != null) {
try {
ret = Operators.getOperator(last.getPosition(), opName, ret);
continue;
}
catch (NoSuchMethodException e) {
throw new ParserException(last.getPosition(), "No such prefix operator: " + operator);
}
}
}
if (last instanceof Token) {
throw new ParserException(last.getPosition(), "Extra token found in expression: " + last);
}
else if (last instanceof Invokable) {
throw new ParserException(last.getPosition(), "Extra expression found: " + last);
}
else {
throw new ParserException(last.getPosition(), "Extra element found: " + last);
}
}
return ret;
}
private Token peek() {
if (position >= tokens.size()) {
return new NullToken(tokens.get(tokens.size() - 1).getPosition() + 1);
}
return tokens.get(position);
}
private Identifiable parseFunction(IdentifierToken identifierToken) throws ParserException {
if (peek().id() != '(') {
throw new ParserException(peek().getPosition(), "Unexpected character in parseBracket");
}
++position;
try {
if (peek().id() == ')') {
return Functions.getFunction(identifierToken.getPosition(), identifierToken.value);
}
List<Invokable> args = new ArrayList<Invokable>();
loop: while (true) {
args.add(parseInternal());
final Token current = peek();
++position;
switch (current.id()) {
case ',':
continue;
case ')':
break loop;
default:
throw new ParserException(current.getPosition(), "Unmatched opening bracket");
}
}
return Functions.getFunction(identifierToken.getPosition(), identifierToken.value, args.toArray(new Invokable[args.size()]));
}
catch (NoSuchMethodException e) {
throw new ParserException(identifierToken.getPosition(), "Function not found", e);
}
}
private final Invokable parseBracket() throws ParserException {
if (peek().id() != '(') {
throw new ParserException(peek().getPosition(), "Unexpected character in parseBracket");
}
++position;
final Invokable ret = parseInternal();
if (peek().id() != ')') {
throw new ParserException(peek().getPosition(), "Unmatched opening bracket");
}
++position;
return ret;
}
}

View File

@ -0,0 +1,46 @@
// $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.parser;
import com.sk89q.worldedit.expression.ExpressionException;
public class ParserException extends ExpressionException {
private static final long serialVersionUID = 1L;
public ParserException(int position) {
super(position, getPrefix(position));
}
public ParserException(int position, String message, Throwable cause) {
super(position, getPrefix(position) + ": " + message, cause);
}
public ParserException(int position, String message) {
super(position, getPrefix(position) + ": " + message);
}
public ParserException(int position, Throwable cause) {
super(position, getPrefix(position), cause);
}
private static String getPrefix(int position) {
return position < 0 ? "Parser error" : ("Parser error at " + (position + 1));
}
}

View File

@ -0,0 +1,22 @@
package com.sk89q.worldedit.expression.parser;
import com.sk89q.worldedit.expression.lexer.tokens.OperatorToken;
public class PrefixOperator extends PseudoToken {
final String operator;
public PrefixOperator(OperatorToken operatorToken) {
super(operatorToken.getPosition());
operator = operatorToken.operator;
}
@Override
public char id() {
return 'p';
}
@Override
public String toString() {
return "PrefixOperator(" + operator + ")";
}
}

View File

@ -0,0 +1,38 @@
// $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.parser;
import com.sk89q.worldedit.expression.Identifiable;
public abstract class PseudoToken implements Identifiable {
private final int position;
public PseudoToken(int position) {
this.position = position;
}
@Override
public abstract char id();
@Override
public int getPosition() {
return position;
}
}

View File

@ -0,0 +1,44 @@
// $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 final class Constant extends Invokable {
private final double value;
public Constant(int position, double value) {
super(position);
this.value = value;
}
@Override
public double invoke() {
return value;
}
@Override
public String toString() {
return String.valueOf(value);
}
@Override
public char id() {
return 'c';
}
}

View File

@ -0,0 +1,27 @@
package com.sk89q.worldedit.expression.runtime;
import com.sk89q.worldedit.expression.ExpressionException;
public class EvaluationException extends ExpressionException {
private static final long serialVersionUID = 1L;
public EvaluationException(int position) {
super(position, getPrefix(position));
}
public EvaluationException(int position, String message, Throwable cause) {
super(position, getPrefix(position) + ": " + message, cause);
}
public EvaluationException(int position, String message) {
super(position, getPrefix(position) + ": " + message);
}
public EvaluationException(int position, Throwable cause) {
super(position, getPrefix(position), cause);
}
private static String getPrefix(int position) {
return position < 0 ? "Evaluation error" : ("Evaluation error at " + (position + 1));
}
}

View File

@ -0,0 +1,94 @@
// $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.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Function extends Invokable {
@Retention(RetentionPolicy.RUNTIME)
public @interface Dynamic { }
final Method method;
final Invokable[] args;
Function(int position, Method method, Invokable... args) {
super(position);
this.method = method;
this.args = args;
}
@Override
public final double invoke() throws EvaluationException {
try {
return (Double) method.invoke(null, (Object[]) args);
}
catch (InvocationTargetException e) {
if (e.getTargetException() instanceof EvaluationException) {
throw (EvaluationException) e.getTargetException();
}
throw new EvaluationException(-1, "Exception caught while evaluating expression", e.getTargetException());
}
catch (IllegalAccessException e) {
throw new EvaluationException(-1, "Internal error while evaluating expression", e);
}
}
@Override
public String toString() {
final StringBuilder ret = new StringBuilder(method.getName()).append('(');
boolean first = true;
for (Object obj : args) {
if (!first) {
ret.append(", ");
}
first = false;
ret.append(obj);
}
return ret.append(')').toString();
}
@Override
public char id() {
return 'f';
}
@Override
public Invokable optimize() throws EvaluationException {
final Invokable[] optimizedArgs = new Invokable[args.length];
boolean optimizable = !method.isAnnotationPresent(Dynamic.class);
for (int i = 0; i < args.length; ++i) {
final Invokable optimized = optimizedArgs[i] = args[i].optimize();
if (!(optimized instanceof Constant)) {
optimizable = false;
}
}
if (optimizable) {
return new Constant(getPosition(), invoke());
}
else {
return new Function(getPosition(), method, optimizedArgs);
}
}
}

View File

@ -0,0 +1,129 @@
// $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.Arrays;
public final class Functions {
public static final Function getFunction(int position, String name, Invokable... args) throws NoSuchMethodException {
final Class<?>[] parameterTypes = (Class<?>[]) new Class[args.length];
Arrays.fill(parameterTypes, Invokable.class);
return new Function(position, Functions.class.getMethod(name, parameterTypes), args);
}
public static final double sin(Invokable x) throws Exception {
return Math.sin(x.invoke());
}
public static final double cos(Invokable x) throws Exception {
return Math.cos(x.invoke());
}
public static final double tan(Invokable x) throws Exception {
return Math.tan(x.invoke());
}
public static final double asin(Invokable x) throws Exception {
return Math.asin(x.invoke());
}
public static final double acos(Invokable x) throws Exception {
return Math.acos(x.invoke());
}
public static final double atan(Invokable x) throws Exception {
return Math.atan(x.invoke());
}
public static final double atan2(Invokable y, Invokable x) throws Exception {
return Math.atan2(y.invoke(), x.invoke());
}
public static final double sinh(Invokable x) throws Exception {
return Math.sinh(x.invoke());
}
public static final double cosh(Invokable x) throws Exception {
return Math.cosh(x.invoke());
}
public static final double tanh(Invokable x) throws Exception {
return Math.tanh(x.invoke());
}
public static final double sqrt(Invokable x) throws Exception {
return Math.sqrt(x.invoke());
}
public static final double cbrt(Invokable x) throws Exception {
return Math.cbrt(x.invoke());
}
public static final double abs(Invokable x) throws Exception {
return Math.abs(x.invoke());
}
public static final double min(Invokable a, Invokable b) throws Exception {
return Math.min(a.invoke(), b.invoke());
}
public static final double max(Invokable a, Invokable b) throws Exception {
return Math.max(a.invoke(), b.invoke());
}
public static final double ceil(Invokable x) throws Exception {
return Math.ceil(x.invoke());
}
public static final double floor(Invokable x) throws Exception {
return Math.floor(x.invoke());
}
public static final double rint(Invokable x) throws Exception {
return Math.rint(x.invoke());
}
public static final double round(Invokable x) throws Exception {
return Math.round(x.invoke());
}
public static final double exp(Invokable x) throws Exception {
return Math.exp(x.invoke());
}
public static final double ln(Invokable x) throws Exception {
return Math.log(x.invoke());
}
public static final double log(Invokable x) throws Exception {
return Math.log(x.invoke());
}
public static final double log10(Invokable x) throws Exception {
return Math.log10(x.invoke());
}
}

View File

@ -0,0 +1,45 @@
// $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 com.sk89q.worldedit.expression.Identifiable;
public abstract class Invokable implements Identifiable {
private final int position;
public Invokable(int position) {
super();
this.position = position;
}
public abstract double invoke() throws EvaluationException;
@Override
public abstract String toString();
public Invokable optimize() throws EvaluationException {
return this;
}
@Override
public int getPosition() {
return position;
}
}

View File

@ -0,0 +1,136 @@
// $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 final class Operators {
public static final Function getOperator(int position, String name, Invokable lhs, Invokable rhs) throws NoSuchMethodException {
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 {
return new Function(position, Operators.class.getMethod(name, Invokable.class), argument);
}
public static final double add(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() + rhs.invoke();
}
public static final double sub(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() - rhs.invoke();
}
public static final double mul(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() * rhs.invoke();
}
public static final double div(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() / rhs.invoke();
}
public static final double mod(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() % rhs.invoke();
}
public static final double pow(Invokable lhs, Invokable rhs) throws EvaluationException {
return Math.pow(lhs.invoke(), rhs.invoke());
}
public static final double neg(Invokable x) throws EvaluationException {
return -x.invoke();
}
public static final double not(Invokable x) throws EvaluationException {
return x.invoke() > 0.0 ? 0.0 : 1.0;
}
public static final double inv(Invokable x) throws EvaluationException {
return ~(long) x.invoke();
}
public static final double lth(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() < rhs.invoke() ? 1.0 : 0.0;
}
public static final double gth(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() > rhs.invoke() ? 1.0 : 0.0;
}
public static final double leq(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() <= rhs.invoke() ? 1.0 : 0.0;
}
public static final double geq(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() >= rhs.invoke() ? 1.0 : 0.0;
}
public static final double equ(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() == rhs.invoke() ? 1.0 : 0.0;
}
public static final double neq(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() != rhs.invoke() ? 1.0 : 0.0;
}
public static final double near(Invokable lhs, Invokable rhs) throws EvaluationException {
return almostEqual2sComplement(lhs.invoke(), rhs.invoke(), 450359963L) ? 1.0 : 0.0;
//return Math.abs(lhs.invoke() - rhs.invoke()) < 1e-7 ? 1.0 : 0.0;
}
public static final double or(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() > 0.0 || rhs.invoke() > 0.0 ? 1.0 : 0.0;
}
public static final double and(Invokable lhs, Invokable rhs) throws EvaluationException {
return lhs.invoke() > 0.0 && rhs.invoke() > 0.0 ? 1.0 : 0.0;
}
public static final double shl(Invokable lhs, Invokable rhs) throws EvaluationException {
return (long) lhs.invoke() << (long) rhs.invoke();
}
public static final double shr(Invokable lhs, Invokable rhs) throws EvaluationException {
return (long) lhs.invoke() >> (long) rhs.invoke();
}
// 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
// default NAN won't compare as equal to anything.
//assert(maxUlps > 0 && maxUlps < 4 * 1024 * 1024); // this is for floats, not doubles
long aLong = Double.doubleToRawLongBits(A);
// Make aLong lexicographically ordered as a twos-complement long
if (aLong < 0) aLong = 0x8000000000000000L - aLong;
long bLong = Double.doubleToRawLongBits(B);
// Make bLong lexicographically ordered as a twos-complement long
if (bLong < 0) bLong = 0x8000000000000000L - bLong;
long longDiff = Math.abs(aLong - bLong);
return longDiff <= maxUlps;
}
}

View File

@ -0,0 +1,44 @@
// $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 final class Variable extends Invokable {
public double value;
public Variable(double value) {
super(-1);
this.value = value;
}
@Override
public double invoke() {
return value;
}
@Override
public String toString() {
return "var";
}
@Override
public char id() {
return 'v';
}
}

View File

@ -0,0 +1,66 @@
package com.sk89q.worldedit.expression;
import static org.junit.Assert.*;
import static java.lang.Math.*;
import org.junit.*;
import com.sk89q.worldedit.expression.lexer.LexerException;
import com.sk89q.worldedit.expression.parser.ParserException;
public class ExpressionTest {
@Test
public void testEvaluate() throws Exception {
// check
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);
// check functions
assertEquals(sin(5), simpleEval("sin(5)"), 0);
assertEquals(atan2(3,4), simpleEval("atan2(3,4)"), 0);
// check variables
assertEquals(8, Expression.compile("foo+bar", "foo", "bar").evaluate(5, 3), 0);
}
@Test
public void testErrors() throws ExpressionException {
// test lexer errors
try {
Expression.compile("{");
fail("Error expected");
} catch (LexerException e) {
assertEquals("Error position", 0, e.getPosition());
}
// test parser errors
try {
Expression.compile("x");
fail("Error expected");
} catch (ParserException e) {
assertEquals("Error position", 0, e.getPosition());
}
try {
Expression.compile("x()");
fail("Error expected");
} catch (ParserException e) {
assertEquals("Error position", 0, e.getPosition());
}
try {
Expression.compile("(");
fail("Error expected");
} catch (ParserException e) {}
try {
Expression.compile("x(");
fail("Error expected");
} catch (ParserException e) {}
}
private double simpleEval(String expression) throws Exception {
return Expression.compile(expression).evaluate();
}
}