diff --git a/src/main/java/com/sk89q/worldedit/expression/Expression.java b/src/main/java/com/sk89q/worldedit/expression/Expression.java index 28bb0ff80..973bf313f 100644 --- a/src/main/java/com/sk89q/worldedit/expression/Expression.java +++ b/src/main/java/com/sk89q/worldedit/expression/Expression.java @@ -78,4 +78,8 @@ public class Expression { public String toString() { return root.toString(); } + + public Invokable getVariable(String name) { + return variables.get(name); + } } diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/Lexer.java b/src/main/java/com/sk89q/worldedit/expression/lexer/Lexer.java index 47ea711bb..9851c9341 100644 --- a/src/main/java/com/sk89q/worldedit/expression/lexer/Lexer.java +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/Lexer.java @@ -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 keywords = new HashSet(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; diff --git a/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/KeywordToken.java b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/KeywordToken.java new file mode 100644 index 000000000..d73d70db8 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/lexer/tokens/KeywordToken.java @@ -0,0 +1,39 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * 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 . +*/ + +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 + ")"; + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/parser/Parser.java b/src/main/java/com/sk89q/worldedit/expression/parser/Parser.java index 1ad0ceefd..3be63ddee 100644 --- a/src/main/java/com/sk89q/worldedit/expression/parser/Parser.java +++ b/src/main/java/com/sk89q/worldedit/expression/parser/Parser.java @@ -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 halfProcessed = new LinkedList(); // 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 input) throws ParserException { + LinkedList lhs = new LinkedList(); + LinkedList rhs = new LinkedList(); + boolean semicolonFound = false; + + for (Iterator 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 input) throws ParserException { + return processBinaryOps(input, binaryOpMaps.length - 1); } private static final Map[] 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 input, int level) throws ParserException { @@ -315,7 +385,7 @@ public class Parser { List args = new ArrayList(); 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; + } } diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Assignable.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Assignable.java new file mode 100644 index 000000000..1f140c1a4 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Assignable.java @@ -0,0 +1,28 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * 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 . +*/ + +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; +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Operators.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Operators.java index 47d7f888f..6452adcfc 100644 --- a/src/main/java/com/sk89q/worldedit/expression/runtime/Operators.java +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Operators.java @@ -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 diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Sequence.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Sequence.java new file mode 100644 index 000000000..489f0ee1c --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Sequence.java @@ -0,0 +1,82 @@ +// $Id$ +/* + * WorldEdit + * Copyright (C) 2010, 2011 sk89q + * + * 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 . +*/ + +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 newSequence = new ArrayList(); + + 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()])); + } +} diff --git a/src/main/java/com/sk89q/worldedit/expression/runtime/Variable.java b/src/main/java/com/sk89q/worldedit/expression/runtime/Variable.java index fc6ea296d..1673732c4 100644 --- a/src/main/java/com/sk89q/worldedit/expression/runtime/Variable.java +++ b/src/main/java/com/sk89q/worldedit/expression/runtime/Variable.java @@ -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; + } } diff --git a/src/test/java/com/sk89q/worldedit/expression/ExpressionTest.java b/src/test/java/com/sk89q/worldedit/expression/ExpressionTest.java index 36a310aaf..255db7406 100644 --- a/src/test/java/com/sk89q/worldedit/expression/ExpressionTest.java +++ b/src/test/java/com/sk89q/worldedit/expression/ExpressionTest.java @@ -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(); }