diff --git a/worldedit-core/src/main/antlr/com/sk89q/worldedit/antlr/Expression.g4 b/worldedit-core/src/main/antlr/com/sk89q/worldedit/antlr/Expression.g4 index b244f2c64..c98965af6 100644 --- a/worldedit-core/src/main/antlr/com/sk89q/worldedit/antlr/Expression.g4 +++ b/worldedit-core/src/main/antlr/com/sk89q/worldedit/antlr/Expression.g4 @@ -79,107 +79,158 @@ allStatements : statements EOF ; statements : statement+ ; statement - : block # BlockStmt - | ifStatement # IfStmt - | whileStatement # WhileStmt - | doStatement # DoStmt - | forStatement # ForStmt - | breakStatement # BreakStmt - | continueStatement # ContinueStmt - | returnStatement # ReturnStmt - | switchStatement # SwitchStmt - | expressionStatement # ExpressionStmt - | SEMI_COLON # EmptyStmt + : ( block + | ifStatement + | whileStatement + | doStatement + | forStatement + | simpleForStatement + | breakStatement + | continueStatement + | returnStatement + | switchStatement + | expressionStatement + | emptyStatement + ) SEMI_COLON? ; block : '{' statements '}' ; -ifStatement : IF '(' expression ')' statement ( ELSE statement ) ; +ifStatement : IF '(' condition=expression ')' trueBranch=statement ( ELSE falseBranch=statement )? ; -whileStatement : WHILE '(' expression ')' statement ; +whileStatement : WHILE '(' condition=expression ')' body=statement ; -doStatement : DO statement WHILE '(' expression ')' SEMI_COLON ; +doStatement : DO body=statement WHILE '(' condition=expression ')' ; +// C-Style for loop forStatement - : FOR '(' - // C-style for loop - ( expression ';' expression ';' expression - // Range for loop - | ID ASSIGN ID ',' ID - ) - ')' statement ; + : FOR '(' init=expression ';' condition=expression ';' update=expression ')' body=statement ; + +// Range for loop +simpleForStatement + : FOR '(' counter=ID ASSIGN first=expression ',' last=expression ')' body=statement ; breakStatement : BREAK ; continueStatement : CONTINUE ; -returnStatement : RETURN expression? ; +returnStatement : RETURN value=expression? ; -switchStatement : SWITCH '(' expression ')' '{' (switchLabel ':' statements )+ '}' ; +switchStatement : SWITCH '(' target=expression ')' '{' (labels+=switchLabel ':' bodies+=statements )+ '}' ; switchLabel - : CASE constantExpression # Case + : CASE constant=constantExpression # Case | DEFAULT # Default ; -expressionStatement : expression SEMI_COLON ; +expressionStatement : expression ; -expression - : unaryOp expression # UnaryExpr - | expression binaryOp expression # BinaryExpr - | expression postUnaryOp # PostUnaryExpr - | ID binaryAssignOp expression # AssignExpr - | expression '?' expression ':' expression # TernaryExpr - | functionCall # FunctionCallExpr +emptyStatement: SEMI_COLON ; + +expression : assignmentExpression ; + +assignmentExpression + : conditionalExpression + | assignment + ; + +assignment + : target=ID assignmentOperator source=expression + ; + +assignmentOperator + : ASSIGN + | POWER_ASSIGN + | TIMES_ASSIGN + | DIVIDE_ASSIGN + | MODULO_ASSIGN + | PLUS_ASSIGN + | MINUS_ASSIGN + ; + +conditionalExpression + : conditionalOrExpression # CEFallthrough + | condition=conditionalOrExpression QUESTION_MARK + trueBranch=expression COLON falseBranch=conditionalExpression # TernaryExpr + ; + +conditionalOrExpression + : conditionalAndExpression # COFallthrough + | left=conditionalOrExpression OR_SC right=conditionalAndExpression # ConditionalOrExpr + ; + +conditionalAndExpression + : equalityExpression # CAFallthrough + | left=conditionalAndExpression AND_SC right=equalityExpression # ConditionalAndExpr + ; + +equalityExpression + : relationalExpression # EqFallthrough + | left=equalityExpression + op= + ( EQUAL + | NOT_EQUAL + | NEAR + ) right=relationalExpression # EqualityExpr + ; + +relationalExpression + : shiftExpression # ReFallthrough + | left=relationalExpression + op= + ( LESS_THAN + | GREATER_THAN + | LESS_THAN_OR_EQUAL + | GREATER_THAN_OR_EQUAL + ) right=shiftExpression # RelationalExpr + ; + +shiftExpression + : additiveExpression # ShFallthrough + | left=shiftExpression op=( LEFT_SHIFT | RIGHT_SHIFT ) right=additiveExpression # ShiftExpr + ; + +additiveExpression + : multiplicativeExpression # AdFallthrough + | left=additiveExpression op=( PLUS | MINUS ) right=multiplicativeExpression # AddExpr + ; + +multiplicativeExpression + : powerExpression # MuFallthrough + | left=multiplicativeExpression + op= + ( TIMES + | DIVIDE + | MODULO + ) right=powerExpression # MultiplicativeExpr + ; + +powerExpression + : unaryExpression # PwFallthrough + | left=powerExpression POWER right=unaryExpression # PowerExpr + ; + +unaryExpression + : op=( INCREMENT | DECREMENT ) target=ID # PreCrementExpr + | op=( PLUS | MINUS ) expr=unaryExpression # PlusMinusExpr + | postfixExpression # UaFallthrough + | COMPLEMENT expr=unaryExpression # ComplementExpr + | EXCLAMATION_MARK expr=unaryExpression # NotExpr + ; + +postfixExpression + : unprioritizedExpression # PoFallthrough + | target=ID op=( INCREMENT | DECREMENT) # PostCrementExpr + | expr=postfixExpression op=EXCLAMATION_MARK # PostfixExpr + ; + +unprioritizedExpression + : functionCall # FunctionCallExpr | constantExpression # ConstantExpr - | ID # IdExpr + | source=ID # IdExpr | '(' expression ')' # WrappedExpr ; constantExpression : NUMBER ; -functionCall : ID '(' (expression ( ',' expression )*)? ')' ; - -unaryOp - : MINUS - | EXCLAMATION_MARK - | COMPLEMENT - | INCREMENT - | DECREMENT - ; - -postUnaryOp - : INCREMENT - | DECREMENT - | EXCLAMATION_MARK - ; - -binaryOp - : POWER - | TIMES - | DIVIDE - | MODULO - | PLUS - | MINUS - | LEFT_SHIFT - | RIGHT_SHIFT - | LESS_THAN - | GREATER_THAN - | LESS_THAN_OR_EQUAL - | GREATER_THAN_OR_EQUAL - | EQUAL - | NOT_EQUAL - | NEAR - | AND_SC - | OR_SC - ; - -binaryAssignOp - : ASSIGN - | PLUS_ASSIGN - | MINUS_ASSIGN - | TIMES_ASSIGN - | DIVIDE_ASSIGN - | MODULO_ASSIGN - | POWER_ASSIGN - ; +functionCall : name=ID '(' (args+=expression ( ',' args+=expression )*)? ')' ; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java index f195ed7de..c64e6ca72 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java @@ -80,8 +80,8 @@ import com.sk89q.worldedit.history.changeset.BlockOptimizedHistory; import com.sk89q.worldedit.history.changeset.ChangeSet; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; -import com.sk89q.worldedit.internal.expression.runtime.ExpressionTimeoutException; -import com.sk89q.worldedit.internal.expression.runtime.RValue; +import com.sk89q.worldedit.internal.expression.ExpressionTimeoutException; +import com.sk89q.worldedit.internal.expression.LocalSlot.Variable; import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.MathUtils; @@ -1989,8 +1989,10 @@ public class EditSession implements Extent, AutoCloseable { final Expression expression = Expression.compile(expressionString, "x", "y", "z", "type", "data"); expression.optimize(); - final RValue typeVariable = expression.getVariable("type", false); - final RValue dataVariable = expression.getVariable("data", false); + final Variable typeVariable = expression.getSlots().getVariable("type") + .orElseThrow(IllegalStateException::new); + final Variable dataVariable = expression.getSlots().getVariable("data") + .orElseThrow(IllegalStateException::new); final WorldEditExpressionEnvironment environment = new WorldEditExpressionEnvironment(this, unit, zero); expression.setEnvironment(environment); @@ -2052,9 +2054,12 @@ public class EditSession implements Extent, AutoCloseable { final Expression expression = Expression.compile(expressionString, "x", "y", "z"); expression.optimize(); - final RValue x = expression.getVariable("x", false); - final RValue y = expression.getVariable("y", false); - final RValue z = expression.getVariable("z", false); + final Variable x = expression.getSlots().getVariable("x") + .orElseThrow(IllegalStateException::new); + final Variable y = expression.getSlots().getVariable("y") + .orElseThrow(IllegalStateException::new); + final Variable z = expression.getSlots().getVariable("z") + .orElseThrow(IllegalStateException::new); final WorldEditExpressionEnvironment environment = new WorldEditExpressionEnvironment(this, unit, zero); expression.setEnvironment(environment); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask.java index eba02d0a7..e9508bcee 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask.java @@ -23,7 +23,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; -import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; +import com.sk89q.worldedit.internal.expression.EvaluationException; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.shape.WorldEditExpressionEnvironment; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask2D.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask2D.java index a50c5e375..4ce7c1709 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask2D.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask2D.java @@ -23,7 +23,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; -import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; +import com.sk89q.worldedit.internal.expression.EvaluationException; import com.sk89q.worldedit.math.BlockVector2; import javax.annotation.Nullable; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/BreakException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java similarity index 78% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/BreakException.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java index a3d384117..635c6d6ea 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/BreakException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java @@ -17,18 +17,19 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression.runtime; +package com.sk89q.worldedit.internal.expression; /** * Thrown when a break or continue is encountered. * Loop constructs catch this exception. */ -public class BreakException extends EvaluationException { +public class BreakException extends RuntimeException { - final boolean doContinue; + public final boolean doContinue; public BreakException(boolean doContinue) { - super(-1, doContinue ? "'continue' encountered outside a loop" : "'break' encountered outside a loop"); + super(doContinue ? "'continue' encountered outside a loop" : "'break' encountered outside a loop", + null, true, false); this.doContinue = doContinue; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluatingVisitor.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluatingVisitor.java new file mode 100644 index 000000000..bd282e9e1 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluatingVisitor.java @@ -0,0 +1,625 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program 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 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +import com.google.common.base.Throwables; +import com.google.common.collect.SetMultimap; +import com.sk89q.worldedit.antlr.ExpressionBaseVisitor; +import com.sk89q.worldedit.antlr.ExpressionParser; +import it.unimi.dsi.fastutil.doubles.Double2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; +import it.unimi.dsi.fastutil.doubles.Double2ObjectMaps; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.RuleNode; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.util.List; +import java.util.Optional; +import java.util.function.DoubleBinaryOperator; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static com.sk89q.worldedit.antlr.ExpressionLexer.ASSIGN; +import static com.sk89q.worldedit.antlr.ExpressionLexer.DIVIDE; +import static com.sk89q.worldedit.antlr.ExpressionLexer.DIVIDE_ASSIGN; +import static com.sk89q.worldedit.antlr.ExpressionLexer.EQUAL; +import static com.sk89q.worldedit.antlr.ExpressionLexer.EXCLAMATION_MARK; +import static com.sk89q.worldedit.antlr.ExpressionLexer.GREATER_THAN; +import static com.sk89q.worldedit.antlr.ExpressionLexer.GREATER_THAN_OR_EQUAL; +import static com.sk89q.worldedit.antlr.ExpressionLexer.INCREMENT; +import static com.sk89q.worldedit.antlr.ExpressionLexer.LEFT_SHIFT; +import static com.sk89q.worldedit.antlr.ExpressionLexer.LESS_THAN; +import static com.sk89q.worldedit.antlr.ExpressionLexer.LESS_THAN_OR_EQUAL; +import static com.sk89q.worldedit.antlr.ExpressionLexer.MINUS; +import static com.sk89q.worldedit.antlr.ExpressionLexer.MINUS_ASSIGN; +import static com.sk89q.worldedit.antlr.ExpressionLexer.MODULO; +import static com.sk89q.worldedit.antlr.ExpressionLexer.MODULO_ASSIGN; +import static com.sk89q.worldedit.antlr.ExpressionLexer.NEAR; +import static com.sk89q.worldedit.antlr.ExpressionLexer.NOT_EQUAL; +import static com.sk89q.worldedit.antlr.ExpressionLexer.PLUS; +import static com.sk89q.worldedit.antlr.ExpressionLexer.PLUS_ASSIGN; +import static com.sk89q.worldedit.antlr.ExpressionLexer.POWER_ASSIGN; +import static com.sk89q.worldedit.antlr.ExpressionLexer.RIGHT_SHIFT; +import static com.sk89q.worldedit.antlr.ExpressionLexer.TIMES; +import static com.sk89q.worldedit.antlr.ExpressionLexer.TIMES_ASSIGN; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.WRAPPED_CONSTANT; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.check; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.checkIterations; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.checkTimeout; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.evalException; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.getArgumentHandleName; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.resolveFunction; + +class EvaluatingVisitor extends ExpressionBaseVisitor { + + private final SlotTable slots; + private final SetMultimap functions; + + EvaluatingVisitor(SlotTable slots, + SetMultimap functions) { + this.slots = slots; + this.functions = functions; + } + + private LocalSlot.Variable initVariable(String name, ParserRuleContext ctx) { + return slots.initVariable(name) + .orElseThrow(() -> evalException(ctx, "Cannot overwrite non-variable '" + name + "'")); + } + + private Supplier varNotInitException(String name, ParserRuleContext ctx) { + return () -> evalException(ctx, "'" + name + "' is not initialized yet"); + } + + private LocalSlot.Variable getVariable(String name, ParserRuleContext ctx) { + LocalSlot slot = slots.getSlot(name) + .orElseThrow(varNotInitException(name, ctx)); + check(slot instanceof LocalSlot.Variable, ctx, "'" + name + "' is not a variable"); + return (LocalSlot.Variable) slot; + } + + private double getSlotValue(String name, ParserRuleContext ctx) { + return slots.getSlotValue(name) + .orElseThrow(varNotInitException(name, ctx)); + } + + private Token extractToken(ParserRuleContext ctx) { + List children = ctx.children.stream() + .filter(TerminalNode.class::isInstance) + .map(TerminalNode.class::cast) + .collect(Collectors.toList()); + check(children.size() == 1, ctx, "Expected exactly one token, got " + children.size()); + return children.get(0).getSymbol(); + } + + private Double evaluate(ParserRuleContext ctx) { + return ctx.accept(this); + } + + private double evaluateForValue(ParserRuleContext ctx) { + Double result = evaluate(ctx); + check(result != null, ctx, "Invalid expression for a value"); + return result; + } + + private boolean evaluateBoolean(ParserRuleContext boolExpression) { + Double bool = evaluate(boolExpression); + check(bool != null, boolExpression, "Invalid expression for boolean"); + return doubleToBool(bool); + } + + private boolean doubleToBool(double bool) { + return bool > 0; + } + + private double boolToDouble(boolean bool) { + return bool ? 1 : 0; + } + + private Double evaluateConditional(ParserRuleContext condition, + ParserRuleContext trueBranch, + ParserRuleContext falseBranch) { + ParserRuleContext ctx = evaluateBoolean(condition) ? trueBranch : falseBranch; + return ctx == null ? null : evaluate(ctx); + } + + @Override + public Double visitIfStatement(ExpressionParser.IfStatementContext ctx) { + return evaluateConditional(ctx.condition, ctx.trueBranch, ctx.falseBranch); + } + + @Override + public Double visitTernaryExpr(ExpressionParser.TernaryExprContext ctx) { + return evaluateConditional(ctx.condition, ctx.trueBranch, ctx.falseBranch); + } + + @Override + public Double visitWhileStatement(ExpressionParser.WhileStatementContext ctx) { + Double result = defaultResult(); + int iterations = 0; + while (evaluateBoolean(ctx.condition)) { + checkIterations(iterations, ctx.body); + checkTimeout(); + iterations++; + try { + result = evaluate(ctx.body); + } catch (BreakException ex) { + if (!ex.doContinue) { + break; + } + } + } + return result; + } + + @Override + public Double visitDoStatement(ExpressionParser.DoStatementContext ctx) { + Double result = defaultResult(); + int iterations = 0; + do { + checkIterations(iterations, ctx.body); + checkTimeout(); + iterations++; + try { + result = evaluate(ctx.body); + } catch (BreakException ex) { + if (!ex.doContinue) { + break; + } + } + } while (evaluateBoolean(ctx.condition)); + return result; + } + + @Override + public Double visitForStatement(ExpressionParser.ForStatementContext ctx) { + Double result = defaultResult(); + int iterations = 0; + evaluate(ctx.init); + while (evaluateBoolean(ctx.condition)) { + checkIterations(iterations, ctx.body); + checkTimeout(); + iterations++; + try { + result = evaluate(ctx.body); + } catch (BreakException ex) { + if (!ex.doContinue) { + break; + } + } + evaluate(ctx.update); + } + return result; + } + + @Override + public Double visitSimpleForStatement(ExpressionParser.SimpleForStatementContext ctx) { + Double result = defaultResult(); + int iterations = 0; + double first = evaluateForValue(ctx.first); + double last = evaluateForValue(ctx.last); + String counter = ctx.counter.getText(); + LocalSlot.Variable variable = initVariable(counter, ctx); + for (double i = first; i <= last; i++) { + checkIterations(iterations, ctx.body); + checkTimeout(); + iterations++; + variable.setValue(i); + try { + result = evaluate(ctx.body); + } catch (BreakException ex) { + if (!ex.doContinue) { + break; + } + } + } + return result; + } + + @Override + public Double visitBreakStatement(ExpressionParser.BreakStatementContext ctx) { + throw new BreakException(false); + } + + @Override + public Double visitContinueStatement(ExpressionParser.ContinueStatementContext ctx) { + throw new BreakException(true); + } + + @Override + public Double visitReturnStatement(ExpressionParser.ReturnStatementContext ctx) { + if (ctx.value != null) { + return evaluate(ctx.value); + } + return null; + } + + @Override + public Double visitSwitchStatement(ExpressionParser.SwitchStatementContext ctx) { + Double2ObjectMap cases = new Double2ObjectLinkedOpenHashMap<>(ctx.labels.size()); + ParserRuleContext defaultCase = null; + for (int i = 0; i < ctx.labels.size(); i++) { + ExpressionParser.SwitchLabelContext label = ctx.labels.get(i); + ExpressionParser.StatementsContext body = ctx.bodies.get(i); + if (label instanceof ExpressionParser.CaseContext) { + ExpressionParser.CaseContext caseContext = (ExpressionParser.CaseContext) label; + double key = evaluateForValue(caseContext.constant); + check(!cases.containsKey(key), body, "Duplicate cases detected."); + cases.put(key, body); + } else { + check(defaultCase == null, body, "Duplicate default cases detected."); + defaultCase = body; + } + } + double value = evaluateForValue(ctx.target); + boolean matched = false; + Double evaluated = null; + boolean falling = false; + for (Double2ObjectMap.Entry entry : Double2ObjectMaps.fastIterable(cases)) { + if (falling || entry.getDoubleKey() == value) { + matched = true; + try { + evaluated = evaluate(entry.getValue()); + falling = true; + } catch (BreakException brk) { + check(!brk.doContinue, entry.getValue(), "Cannot continue in a switch"); + falling = false; + break; + } + } + } + // This if is like the one in the loop, default's "case" is `!matched` & present + if ((falling || !matched) && defaultCase != null) { + try { + evaluated = evaluate(defaultCase); + } catch (BreakException brk) { + check(!brk.doContinue, defaultCase, "Cannot continue in a switch"); + } + } + return evaluated; + } + + @Override + public Double visitExpressionStatement(ExpressionParser.ExpressionStatementContext ctx) { + return evaluate(ctx.expression()); + } + + @Override + public Double visitPostCrementExpr(ExpressionParser.PostCrementExprContext ctx) { + String target = ctx.target.getText(); + LocalSlot.Variable variable = getVariable(target, ctx); + double value = variable.getValue(); + if (ctx.op.getType() == INCREMENT) { + value++; + } else { + value--; + } + variable.setValue(value); + return value; + } + + @Override + public Double visitPreCrementExpr(ExpressionParser.PreCrementExprContext ctx) { + String target = ctx.target.getText(); + LocalSlot.Variable variable = getVariable(target, ctx); + double value = variable.getValue(); + double result = value; + if (ctx.op.getType() == INCREMENT) { + value++; + } else { + value--; + } + variable.setValue(value); + return result; + } + + @Override + public Double visitPlusMinusExpr(ExpressionParser.PlusMinusExprContext ctx) { + double value = evaluateForValue(ctx.expr); + switch (ctx.op.getType()) { + case PLUS: + return +value; + case MINUS: + return -value; + } + throw evalException(ctx, "Invalid text for plus/minus expr: " + ctx.op.getText()); + } + + @Override + public Double visitNotExpr(ExpressionParser.NotExprContext ctx) { + return boolToDouble(!evaluateBoolean(ctx.expr)); + } + + @Override + public Double visitComplementExpr(ExpressionParser.ComplementExprContext ctx) { + return (double) ~(long) evaluateForValue(ctx.expr); + } + + @Override + public Double visitConditionalAndExpr(ExpressionParser.ConditionalAndExprContext ctx) { + if (!evaluateBoolean(ctx.left)) { + return boolToDouble(false); + } + return evaluateForValue(ctx.right); + } + + @Override + public Double visitConditionalOrExpr(ExpressionParser.ConditionalOrExprContext ctx) { + double left = evaluateForValue(ctx.left); + if (doubleToBool(left)) { + return left; + } + return evaluateForValue(ctx.right); + } + + private double evaluateBinary(ParserRuleContext left, + ParserRuleContext right, + DoubleBinaryOperator op) { + return op.applyAsDouble(evaluateForValue(left), evaluateForValue(right)); + } + + @Override + public Double visitPowerExpr(ExpressionParser.PowerExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, Math::pow); + } + + @Override + public Double visitMultiplicativeExpr(ExpressionParser.MultiplicativeExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, (l, r) -> { + switch (ctx.op.getType()) { + case TIMES: + return l * r; + case DIVIDE: + return l / r; + case MODULO: + return l % r; + } + throw evalException(ctx, "Invalid text for multiplicative expr: " + ctx.op.getText()); + }); + } + + @Override + public Double visitAddExpr(ExpressionParser.AddExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, (l, r) -> { + switch (ctx.op.getType()) { + case PLUS: + return l + r; + case MINUS: + return l - r; + } + throw evalException(ctx, "Invalid text for additive expr: " + ctx.op.getText()); + }); + } + + @Override + public Double visitShiftExpr(ExpressionParser.ShiftExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, (l, r) -> { + switch (ctx.op.getType()) { + case LEFT_SHIFT: + return (double) ((long) l << (long) r); + case RIGHT_SHIFT: + return (double) ((long) l >> (long) r); + } + throw evalException(ctx, "Invalid text for shift expr: " + ctx.op.getText()); + }); + } + + @Override + public Double visitRelationalExpr(ExpressionParser.RelationalExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, (l, r) -> { + switch (ctx.op.getType()) { + case LESS_THAN: + return boolToDouble(l < r); + case LESS_THAN_OR_EQUAL: + return boolToDouble(l <= r); + case GREATER_THAN: + return boolToDouble(l > r); + case GREATER_THAN_OR_EQUAL: + return boolToDouble(l >= r); + } + throw evalException(ctx, "Invalid text for relational expr: " + ctx.op.getText()); + }); + } + + @Override + public Double visitEqualityExpr(ExpressionParser.EqualityExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, (l, r) -> { + switch (ctx.op.getType()) { + case EQUAL: + return boolToDouble(l == r); + case NOT_EQUAL: + return boolToDouble(l != r); + case NEAR: + return boolToDouble(almostEqual2sComplement(l, r, 450359963L)); + case GREATER_THAN_OR_EQUAL: + return boolToDouble(l >= r); + } + throw evalException(ctx, "Invalid text for equality expr: " + ctx.op.getText()); + }); + } + + // 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; + + final long longDiff = Math.abs(aLong - bLong); + return longDiff <= maxUlps; + } + + @Override + public Double visitPostfixExpr(ExpressionParser.PostfixExprContext ctx) { + double value = evaluateForValue(ctx.expr); + if (ctx.op.getType() == EXCLAMATION_MARK) { + return factorial(value); + } + throw evalException(ctx, + "Invalid text for post-unary expr: " + ctx.op.getText()); + } + + private static final double[] factorials = new double[171]; + + static { + factorials[0] = 1; + for (int i = 1; i < factorials.length; ++i) { + factorials[i] = factorials[i - 1] * i; + } + } + + private static double factorial(double x) throws EvaluationException { + final int n = (int) x; + + if (n < 0) { + return 0; + } + + if (n >= factorials.length) { + return Double.POSITIVE_INFINITY; + } + + return factorials[n]; + } + + @Override + public Double visitAssignment(ExpressionParser.AssignmentContext ctx) { + int type = extractToken(ctx.assignmentOperator()).getType(); + String target = ctx.target.getText(); + double value; + double arg = evaluateForValue(ctx.expression()); + LocalSlot.Variable variable; + if (type == ASSIGN) { + variable = initVariable(target, ctx); + value = arg; + } else { + variable = getVariable(target, ctx); + value = variable.getValue(); + switch (type) { + case POWER_ASSIGN: + value = Math.pow(value, arg); + break; + case TIMES_ASSIGN: + value *= arg; + break; + case DIVIDE_ASSIGN: + value /= arg; + break; + case MODULO_ASSIGN: + value %= arg; + break; + case PLUS_ASSIGN: + value += arg; + break; + case MINUS_ASSIGN: + value -= arg; + break; + default: + throw evalException(ctx, "Invalid text for assign expr: " + + ctx.assignmentOperator().getText()); + } + } + variable.setValue(value); + return value; + } + + @Override + public Double visitFunctionCall(ExpressionParser.FunctionCallContext ctx) { + MethodHandle handle = resolveFunction(functions, ctx); + String fnName = ctx.name.getText(); + Object[] arguments = new Object[ctx.args.size()]; + for (int i = 0; i < arguments.length; i++) { + ExpressionParser.ExpressionContext arg = ctx.args.get(i); + Object transformed = getArgument(fnName, handle.type(), i, arg); + arguments[i] = transformed; + } + try { + // Some methods return other Numbers + Number number = (Number) handle.invokeWithArguments(arguments); + return number == null ? null : number.doubleValue(); + } catch (Throwable throwable) { + Throwables.throwIfUnchecked(throwable); + throw new RuntimeException(throwable); + } + } + + private Object getArgument(String fnName, MethodType type, int i, ParserRuleContext arg) { + // Pass variable handle in for modification? + String handleName = getArgumentHandleName(fnName, type, i, arg); + if (handleName == null) { + return evaluateForValue(arg); + } + if (handleName.equals(WRAPPED_CONSTANT)) { + return new LocalSlot.Constant(evaluateForValue(arg)); + } + return getVariable(handleName, arg); + } + + @Override + public Double visitConstantExpression(ExpressionParser.ConstantExpressionContext ctx) { + try { + return Double.parseDouble(ctx.getText()); + } catch (NumberFormatException e) { + // Rare, but might happen, e.g. if too many digits + throw evalException(ctx, "Invalid constant: " + e.getMessage()); + } + } + + @Override + public Double visitIdExpr(ExpressionParser.IdExprContext ctx) { + String source = ctx.source.getText(); + return getSlotValue(source, ctx); + } + + @Override + public Double visitChildren(RuleNode node) { + Double result = defaultResult(); + int n = node.getChildCount(); + for (int i = 0; i < n; i++) { + ParseTree c = node.getChild(i); + if (c instanceof TerminalNode && ((TerminalNode) c).getSymbol().getType() == Token.EOF) { + break; + } + + Double childResult = c.accept(this); + if (c instanceof ExpressionParser.ReturnStatementContext) { + return childResult; + } + result = aggregateResult(result, childResult); + } + + return result; + } + + @Override + protected Double aggregateResult(Double aggregate, Double nextResult) { + return Optional.ofNullable(nextResult).orElse(aggregate); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/EvaluationException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluationException.java similarity index 96% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/EvaluationException.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluationException.java index fc34a0ffa..c9042d7ca 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/EvaluationException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluationException.java @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression.runtime; +package com.sk89q.worldedit.internal.expression; import com.sk89q.worldedit.internal.expression.ExpressionException; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java index 0e82a4750..9bca48e12 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java @@ -19,24 +19,23 @@ package com.sk89q.worldedit.internal.expression; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.SetMultimap; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.internal.expression.lexer.Lexer; -import com.sk89q.worldedit.internal.expression.lexer.tokens.Token; -import com.sk89q.worldedit.internal.expression.parser.Parser; -import com.sk89q.worldedit.internal.expression.runtime.Constant; -import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; -import com.sk89q.worldedit.internal.expression.runtime.ExpressionEnvironment; -import com.sk89q.worldedit.internal.expression.runtime.ExpressionTimeoutException; -import com.sk89q.worldedit.internal.expression.runtime.Functions; -import com.sk89q.worldedit.internal.expression.runtime.RValue; -import com.sk89q.worldedit.internal.expression.runtime.ReturnException; -import com.sk89q.worldedit.internal.expression.runtime.Variable; +import com.sk89q.worldedit.antlr.ExpressionLexer; +import com.sk89q.worldedit.antlr.ExpressionParser; import com.sk89q.worldedit.session.request.Request; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.misc.ParseCancellationException; +import org.antlr.v4.runtime.tree.ParseTreeWalker; -import java.util.HashMap; +import java.lang.invoke.MethodHandle; import java.util.List; -import java.util.Map; +import java.util.Objects; import java.util.Stack; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -68,13 +67,8 @@ import java.util.concurrent.TimeoutException; * If you wish to run the equation multiple times, you can then optimize it, * by calling {@link #optimize()}. You can then run the equation as many times * as you want by calling {@link #evaluate(double...)}. You do not need to - * pass values for all variables specified while compiling. - * To query variables after evaluation, you can use - * {@link #getVariable(String, boolean)}. To get a value out of these, use - * {@link Variable#getValue()}.

- * - *

Variables are also supported and can be set either by passing values - * to {@link #evaluate(double...)}.

+ * pass values for all slots specified while compiling. + * To query slots after evaluation, you can use the {@linkplain #getSlots() slot table}. */ public class Expression { @@ -85,10 +79,10 @@ public class Expression { .setNameFormat("worldedit-expression-eval-%d") .build()); - private final Map variables = new HashMap<>(); - private final String[] variableNames; - private RValue root; - private final Functions functions = new Functions(); + private final SlotTable slots = new SlotTable(); + private final List providedSlots; + private ExpressionParser.AllStatementsContext root; + private final SetMultimap functions = Functions.getFunctionMap(); private ExpressionEnvironment environment; public static Expression compile(String expression, String... variableNames) throws ExpressionException { @@ -96,25 +90,33 @@ public class Expression { } private Expression(String expression, String... variableNames) throws ExpressionException { - this(Lexer.tokenize(expression), variableNames); - } - - private Expression(List tokens, String... variableNames) throws ExpressionException { - this.variableNames = variableNames; - - variables.put("e", new Constant(-1, Math.E)); - variables.put("pi", new Constant(-1, Math.PI)); - variables.put("true", new Constant(-1, 1)); - variables.put("false", new Constant(-1, 0)); + slots.putSlot("e", new LocalSlot.Constant(Math.E)); + slots.putSlot("pi", new LocalSlot.Constant(Math.PI)); + slots.putSlot("true", new LocalSlot.Constant(1)); + slots.putSlot("false", new LocalSlot.Constant(0)); for (String variableName : variableNames) { - if (variables.containsKey(variableName)) { - throw new ExpressionException(-1, "Tried to overwrite identifier '" + variableName + "'"); - } - variables.put(variableName, new Variable(0)); + slots.initVariable(variableName) + .orElseThrow(() -> new ExpressionException(-1, + "Tried to overwrite identifier '" + variableName + "'")); } + this.providedSlots = ImmutableList.copyOf(variableNames); - root = Parser.parse(tokens, this); + CharStream cs = CharStreams.fromString(expression, ""); + ExpressionLexer lexer = new ExpressionLexer(cs); + lexer.removeErrorListeners(); + lexer.addErrorListener(new LexerErrorListener()); + CommonTokenStream tokens = new CommonTokenStream(lexer); + ExpressionParser parser = new ExpressionParser(tokens); + parser.removeErrorListeners(); + parser.addErrorListener(new ParserErrorListener()); + try { + root = parser.allStatements(); + Objects.requireNonNull(root, "Unable to parse root, but no exceptions?"); + } catch (ParseCancellationException e) { + throw new ParserException(parser.getState(), e); + } + ParseTreeWalker.DEFAULT.walk(new ExpressionValidator(slots.keySet(), functions), root); } public double evaluate(double... values) throws EvaluationException { @@ -123,23 +125,19 @@ public class Expression { public double evaluate(double[] values, int timeout) throws EvaluationException { for (int i = 0; i < values.length; ++i) { - final String variableName = variableNames[i]; - final RValue invokable = variables.get(variableName); - if (!(invokable instanceof Variable)) { - throw new EvaluationException(invokable.getPosition(), "Tried to assign constant " + variableName + "."); - } + String slotName = providedSlots.get(i); + LocalSlot.Variable slot = slots.getVariable(slotName) + .orElseThrow(() -> new EvaluationException(-1, + "Tried to assign to non-variable " + slotName + ".")); - ((Variable) invokable).value = values[i]; + slot.setValue(values[i]); } - try { - if (timeout < 0) { - return evaluateRoot(); - } - return evaluateRootTimed(timeout); - } catch (ReturnException e) { - return e.getValue(); - } // other evaluation exceptions are thrown out of this method + // evaluation exceptions are thrown out of this method + if (timeout < 0) { + return evaluateRoot(); + } + return evaluateRootTimed(timeout); } private double evaluateRootTimed(int timeout) throws EvaluationException { @@ -165,12 +163,8 @@ public class Expression { throw new ExpressionTimeoutException("Calculations exceeded time limit."); } catch (ExecutionException e) { Throwable cause = e.getCause(); - if (cause instanceof EvaluationException) { - throw (EvaluationException) cause; - } - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } + Throwables.throwIfInstanceOf(cause, EvaluationException.class); + Throwables.throwIfUnchecked(cause); throw new RuntimeException(cause); } } @@ -178,14 +172,14 @@ public class Expression { private Double evaluateRoot() throws EvaluationException { pushInstance(); try { - return root.getValue(); + return root.accept(new EvaluatingVisitor(slots, functions)); } finally { popInstance(); } } - public void optimize() throws EvaluationException { - root = root.optimize(); + public void optimize() { + // TODO optimizing } @Override @@ -193,13 +187,8 @@ public class Expression { return root.toString(); } - public RValue getVariable(String name, boolean create) { - RValue variable = variables.get(name); - if (variable == null && create) { - variables.put(name, variable = new Variable(0)); - } - - return variable; + public SlotTable getSlots() { + return slots; } public static Expression getInstance() { @@ -225,10 +214,6 @@ public class Expression { } } - public Functions getFunctions() { - return functions; - } - public ExpressionEnvironment getEnvironment() { return environment; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionEnvironment.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionEnvironment.java similarity index 95% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionEnvironment.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionEnvironment.java index 1a9a57d4a..9fa11e923 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionEnvironment.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionEnvironment.java @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression.runtime; +package com.sk89q.worldedit.internal.expression; /** * Represents a way to access blocks in a world. Has to accept non-rounded coordinates. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionException.java index 2320ad99c..41c6f8863 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionException.java @@ -23,7 +23,7 @@ package com.sk89q.worldedit.internal.expression; * Thrown when there's a problem during any stage of the expression * compilation or evaluation. */ -public class ExpressionException extends Exception { +public class ExpressionException extends RuntimeException { private final int position; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionHelper.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionHelper.java new file mode 100644 index 000000000..f1689f78e --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionHelper.java @@ -0,0 +1,149 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program 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 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +import com.google.common.collect.SetMultimap; +import com.sk89q.worldedit.antlr.ExpressionParser; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.sk89q.worldedit.antlr.ExpressionLexer.ID; + +class ExpressionHelper { + + static void check(boolean condition, ParserRuleContext ctx, String message) { + if (!condition) { + throw evalException(ctx, message); + } + } + + static EvaluationException evalException(ParserRuleContext ctx, String message) { + return new EvaluationException( + ctx.getStart().getCharPositionInLine(), + message + ); + } + + static void checkIterations(int iterations, ParserRuleContext ctx) { + check(iterations <= 256, ctx, "Loop exceeded 256 iterations"); + } + + static void checkTimeout() { + if (Thread.interrupted()) { + throw new ExpressionTimeoutException("Calculations exceeded time limit."); + } + } + + static MethodHandle resolveFunction(SetMultimap functions, + ExpressionParser.FunctionCallContext ctx) { + String fnName = ctx.name.getText(); + Set matchingFns = functions.get(fnName); + check(!matchingFns.isEmpty(), ctx, "Unknown function '" + fnName + "'"); + for (MethodHandle function : matchingFns) { + MethodType type = function.type(); + // Validate argc if not varargs + if (!function.isVarargsCollector() && type.parameterCount() != ctx.args.size()) { + // skip non-matching function + continue; + } + for (int i = 0; i < ctx.args.size(); i++) { + ExpressionParser.ExpressionContext arg = ctx.args.get(i); + getArgumentHandleName(fnName, type, i, arg); + } + // good match! + return function; + } + // We matched no function, fail with appropriate message. + String possibleCounts = matchingFns.stream() + .map(mh -> mh.isVarargsCollector() + ? (mh.type().parameterCount() - 1) + "+" + : String.valueOf(mh.type().parameterCount())) + .collect(Collectors.joining("/")); + throw evalException(ctx, "Incorrect number of arguments for function '" + fnName + "', " + + "expected " + possibleCounts + ", " + + "got " + ctx.args.size()); + } + + // Special argument handle names + /** + * The argument should be wrapped in a {@link LocalSlot.Constant} before being passed. + */ + static final String WRAPPED_CONSTANT = ""; + + /** + * If this argument needs a handle, returns the name of the handle needed. Otherwise, returns + * {@code null}. If {@code arg} isn't a valid handle reference, throws. + */ + static String getArgumentHandleName(String fnName, MethodType type, int i, + ParserRuleContext arg) { + // Pass variable handle in for modification? + Class pType = type.parameterType(i); + Optional id = tryResolveId(arg); + if (pType == LocalSlot.Variable.class) { + // MUST be an id + check(id.isPresent(), arg, + "Function '" + fnName + "' requires a variable in parameter " + i); + return id.get(); + } else if (pType == LocalSlot.class) { + return id.orElse(WRAPPED_CONSTANT); + } + return null; + } + + private static Optional tryResolveId(ParserRuleContext arg) { + Optional wrappedExprContext = + tryAs(arg, ExpressionParser.WrappedExprContext.class); + if (wrappedExprContext.isPresent()) { + return tryResolveId(wrappedExprContext.get().expression()); + } + Token token = arg.start; + int tokenType = token.getType(); + boolean isId = arg.start == arg.stop && tokenType == ID; + return isId ? Optional.of(token.getText()) : Optional.empty(); + } + + private static Optional tryAs( + ParserRuleContext ctx, + Class rule + ) { + if (rule.isInstance(ctx)) { + return Optional.of(rule.cast(ctx)); + } + if (ctx.children.size() != 1) { + return Optional.empty(); + } + List ctxs = ctx.getRuleContexts(ParserRuleContext.class); + if (ctxs.size() != 1) { + return Optional.empty(); + } + return tryAs(ctxs.get(0), rule); + } + + private ExpressionHelper() { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionTimeoutException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionTimeoutException.java similarity index 94% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionTimeoutException.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionTimeoutException.java index ce7d55140..e0395cd7a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionTimeoutException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionTimeoutException.java @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression.runtime; +package com.sk89q.worldedit.internal.expression; /** * Thrown when an evaluation exceeds the timeout time. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionValidator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionValidator.java new file mode 100644 index 000000000..c823df306 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionValidator.java @@ -0,0 +1,70 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program 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 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +import com.google.common.collect.SetMultimap; +import com.sk89q.worldedit.antlr.ExpressionBaseListener; +import com.sk89q.worldedit.antlr.ExpressionParser; + +import java.lang.invoke.MethodHandle; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.check; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.resolveFunction; + +class ExpressionValidator extends ExpressionBaseListener { + + private final Set variableNames = new HashSet<>(); + private final SetMultimap functions; + + ExpressionValidator(Collection variableNames, + SetMultimap functions) { + this.variableNames.addAll(variableNames); + this.functions = functions; + } + + private void bindVariable(String name) { + variableNames.add(name); + } + + @Override + public void enterAssignment(ExpressionParser.AssignmentContext ctx) { + bindVariable(ctx.target.getText()); + } + + @Override + public void enterSimpleForStatement(ExpressionParser.SimpleForStatementContext ctx) { + bindVariable(ctx.counter.getText()); + } + + @Override + public void enterIdExpr(ExpressionParser.IdExprContext ctx) { + String text = ctx.source.getText(); + check(variableNames.contains(text), ctx, + "Variable '" + text + "' is not bound"); + } + + @Override + public void enterFunctionCall(ExpressionParser.FunctionCallContext ctx) { + resolveFunction(functions, ctx); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java new file mode 100644 index 000000000..e97f96ade --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java @@ -0,0 +1,337 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program 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 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.SetMultimap; +import com.google.common.primitives.Doubles; +import com.sk89q.worldedit.internal.expression.LocalSlot.Variable; +import com.sk89q.worldedit.math.Vector3; +import com.sk89q.worldedit.math.noise.PerlinNoise; +import com.sk89q.worldedit.math.noise.RidgedMultiFractalNoise; +import com.sk89q.worldedit.math.noise.VoronoiNoise; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.concurrent.ThreadLocalRandom; + +import static java.lang.invoke.MethodType.methodType; + +/** + * Contains all functions that can be used in expressions. + */ +final class Functions { + + static SetMultimap getFunctionMap() { + SetMultimap map = HashMultimap.create(); + Functions functions = new Functions(); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + + try { + addMathHandles(map, lookup); + addStaticFunctionHandles(map, lookup); + functions.addInstanceFunctionHandles(map, lookup); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + + return ImmutableSetMultimap.copyOf(map); + } + + private static void addMathHandles( + SetMultimap map, + MethodHandles.Lookup lookup + ) throws NoSuchMethodException, IllegalAccessException { + // double (double) functions + for (String name : ImmutableList.of( + "sin", "cos", "tan", "asin", "acos", "atan", + "sinh", "cosh", "tanh", "sqrt", "cbrt", "abs", + "ceil", "floor", "rint", "exp", "log", "log10" + )) { + map.put(name, lookup.findStatic(Math.class, name, + methodType(double.class, double.class))); + } + // Alias ln -> log + map.put("ln", lookup.findStatic(Math.class, "log", + methodType(double.class, double.class))); + map.put("round", lookup.findStatic(Math.class, "round", + methodType(long.class, double.class))); + + map.put("atan2", lookup.findStatic(Math.class, "atan2", + methodType(double.class, double.class, double.class))); + + // Special cases: we accept varargs for these + map.put("min", lookup.findStatic(Doubles.class, "min", + methodType(double.class, double[].class)) + .asVarargsCollector(double[].class)); + map.put("max", lookup.findStatic(Doubles.class, "max", + methodType(double.class, double[].class)) + .asVarargsCollector(double[].class)); + } + + private static void addStaticFunctionHandles( + SetMultimap map, + MethodHandles.Lookup lookup + ) throws NoSuchMethodException, IllegalAccessException { + map.put("rotate", lookup.findStatic(Functions.class, "rotate", + methodType(double.class, Variable.class, Variable.class, double.class))); + map.put("swap", lookup.findStatic(Functions.class, "swap", + methodType(double.class, Variable.class, Variable.class))); + map.put("gmegabuf", lookup.findStatic(Functions.class, "gmegabuf", + methodType(double.class, double.class))); + map.put("gmegabuf", lookup.findStatic(Functions.class, "gmegabuf", + methodType(double.class, double.class, double.class))); + map.put("gclosest", lookup.findStatic(Functions.class, "gclosest", + methodType(double.class, double.class, double.class, double.class, double.class, + double.class, double.class))); + map.put("random", lookup.findStatic(Functions.class, "random", + methodType(double.class))); + map.put("randint", lookup.findStatic(Functions.class, "randint", + methodType(double.class, double.class))); + map.put("perlin", lookup.findStatic(Functions.class, "perlin", + methodType(double.class, double.class, double.class, double.class, double.class, + double.class, double.class, double.class))); + map.put("voronoi", lookup.findStatic(Functions.class, "voronoi", + methodType(double.class, double.class, double.class, double.class, double.class, + double.class))); + map.put("ridgedmulti", lookup.findStatic(Functions.class, "ridgedmulti", + methodType(double.class, double.class, double.class, double.class, double.class, + double.class, double.class))); + map.put("query", lookup.findStatic(Functions.class, "query", + methodType(double.class, double.class, double.class, double.class, LocalSlot.class, + LocalSlot.class))); + map.put("queryAbs", lookup.findStatic(Functions.class, "queryAbs", + methodType(double.class, double.class, double.class, double.class, LocalSlot.class, + LocalSlot.class))); + map.put("queryRel", lookup.findStatic(Functions.class, "queryRel", + methodType(double.class, double.class, double.class, double.class, LocalSlot.class, + LocalSlot.class))); + } + + private void addInstanceFunctionHandles( + SetMultimap map, + MethodHandles.Lookup lookup + ) throws NoSuchMethodException, IllegalAccessException { + map.put("megabuf", lookup.findSpecial(Functions.class, "megabuf", + methodType(double.class, double.class), Functions.class) + .bindTo(this)); + map.put("megabuf", lookup.findSpecial(Functions.class, "megabuf", + methodType(double.class, double.class, double.class), Functions.class) + .bindTo(this)); + map.put("closest", lookup.findSpecial(Functions.class, "closest", + methodType(double.class, double.class, double.class, double.class, double.class, + double.class, double.class), Functions.class) + .bindTo(this)); + } + + private static double rotate(Variable x, Variable y, double angle) { + final double cosF = Math.cos(angle); + final double sinF = Math.sin(angle); + + final double xOld = x.getValue(); + final double yOld = y.getValue(); + + x.setValue(xOld * cosF - yOld * sinF); + y.setValue(xOld * sinF + yOld * cosF); + + return 0.0; + } + + private static double swap(Variable x, Variable y) { + final double tmp = x.getValue(); + + x.setValue(y.getValue()); + y.setValue(tmp); + + return 0.0; + } + + + private static final Int2ObjectMap globalMegaBuffer = new Int2ObjectOpenHashMap<>(); + private final Int2ObjectMap megaBuffer = new Int2ObjectOpenHashMap<>(); + + private static double[] getSubBuffer(Int2ObjectMap megabuf, int key) { + return megabuf.computeIfAbsent(key, k -> new double[1024]); + } + + private static double getBufferItem(final Int2ObjectMap megabuf, final int index) { + return getSubBuffer(megabuf, index & ~1023)[index & 1023]; + } + + private static double setBufferItem(final Int2ObjectMap megabuf, final int index, double value) { + return getSubBuffer(megabuf, index & ~1023)[index & 1023] = value; + } + + private static double gmegabuf(double index) { + return getBufferItem(globalMegaBuffer, (int) index); + } + + private static double gmegabuf(double index, double value) { + return setBufferItem(globalMegaBuffer, (int) index, value); + } + + private double megabuf(double index) { + return getBufferItem(megaBuffer, (int) index); + } + + private double megabuf(double index, double value) { + return setBufferItem(megaBuffer, (int) index, value); + } + + private double closest(double x, double y, double z, double index, double count, double stride) { + return findClosest( + megaBuffer, x, y, z, (int) index, (int) count, (int) stride + ); + } + + private static double gclosest(double x, double y, double z, double index, double count, double stride) { + return findClosest( + globalMegaBuffer, x, y, z, (int) index, (int) count, (int) stride + ); + } + + private static double findClosest(Int2ObjectMap megabuf, double x, double y, double z, int index, int count, int stride) { + int closestIndex = -1; + double minDistanceSquared = Double.MAX_VALUE; + + for (int i = 0; i < count; ++i) { + double currentX = getBufferItem(megabuf, index) - x; + double currentY = getBufferItem(megabuf, index+1) - y; + double currentZ = getBufferItem(megabuf, index+2) - z; + + double currentDistanceSquared = currentX*currentX + currentY*currentY + currentZ*currentZ; + + if (currentDistanceSquared < minDistanceSquared) { + minDistanceSquared = currentDistanceSquared; + closestIndex = index; + } + + index += stride; + } + + return closestIndex; + } + + private static double random() { + return ThreadLocalRandom.current().nextDouble(); + } + + private static double randint(double max) { + return ThreadLocalRandom.current().nextInt((int) Math.floor(max)); + } + + private static final ThreadLocal localPerlin = ThreadLocal.withInitial(PerlinNoise::new); + + private static double perlin(double seed, double x, double y, double z, + double frequency, double octaves, double persistence) { + PerlinNoise perlin = localPerlin.get(); + try { + perlin.setSeed((int) seed); + perlin.setFrequency(frequency); + perlin.setOctaveCount((int) octaves); + perlin.setPersistence(persistence); + } catch (IllegalArgumentException e) { + throw new EvaluationException(0, "Perlin noise error: " + e.getMessage()); + } + return perlin.noise(Vector3.at(x, y, z)); + } + + private static final ThreadLocal localVoronoi = ThreadLocal.withInitial(VoronoiNoise::new); + + private static double voronoi(double seed, double x, double y, double z, double frequency) { + VoronoiNoise voronoi = localVoronoi.get(); + try { + voronoi.setSeed((int) seed); + voronoi.setFrequency(frequency); + } catch (IllegalArgumentException e) { + throw new EvaluationException(0, "Voronoi error: " + e.getMessage()); + } + return voronoi.noise(Vector3.at(x, y, z)); + } + + private static final ThreadLocal localRidgedMulti = ThreadLocal.withInitial(RidgedMultiFractalNoise::new); + + private static double ridgedmulti(double seed, double x, double y, double z, + double frequency, double octaves) { + RidgedMultiFractalNoise ridgedMulti = localRidgedMulti.get(); + try { + ridgedMulti.setSeed((int) seed); + ridgedMulti.setFrequency(frequency); + ridgedMulti.setOctaveCount((int) octaves); + } catch (IllegalArgumentException e) { + throw new EvaluationException(0, "Ridged multi error: " + e.getMessage()); + } + return ridgedMulti.noise(Vector3.at(x, y, z)); + } + + private static double queryInternal(LocalSlot type, LocalSlot data, double typeId, double dataValue) { + // Compare to input values and determine return value + // -1 is a wildcard, always true + double ret = ((type.getValue() == -1 || typeId == type.getValue()) + && (data.getValue() == -1 || dataValue == data.getValue())) ? 1.0 : 0.0; + + if (type instanceof Variable) { + ((Variable) type).setValue(typeId); + } + if (data instanceof Variable) { + ((Variable) data).setValue(dataValue); + } + + return ret; + } + + private static double query(double x, double y, double z, LocalSlot type, LocalSlot data) { + final ExpressionEnvironment environment = Expression.getInstance().getEnvironment(); + + // Read values from world + final double typeId = environment.getBlockType(x, y, z); + final double dataValue = environment.getBlockData(x, y, z); + + return queryInternal(type, data, typeId, dataValue); + } + + private static double queryAbs(double x, double y, double z, LocalSlot type, LocalSlot data) { + final ExpressionEnvironment environment = Expression.getInstance().getEnvironment(); + + // Read values from world + final double typeId = environment.getBlockTypeAbs(x, y, z); + final double dataValue = environment.getBlockDataAbs(x, y, z); + + return queryInternal(type, data, typeId, dataValue); + } + + private static double queryRel(double x, double y, double z, LocalSlot type, LocalSlot data) { + final ExpressionEnvironment environment = Expression.getInstance().getEnvironment(); + + // Read values from world + final double typeId = environment.getBlockTypeRel(x, y, z); + final double dataValue = environment.getBlockDataRel(x, y, z); + + return queryInternal(type, data, typeId, dataValue); + } + + private Functions() { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/NumberToken.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LexerErrorListener.java similarity index 65% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/NumberToken.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LexerErrorListener.java index 44cc70665..cb4b41ce5 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/NumberToken.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LexerErrorListener.java @@ -17,28 +17,15 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression.lexer.tokens; +package com.sk89q.worldedit.internal.expression; -/** - * A number. - */ -public class NumberToken extends Token { - - public final double value; - - public NumberToken(int position, double value) { - super(position); - this.value = value; - } +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +class LexerErrorListener extends BaseErrorListener { @Override - public char id() { - return '0'; + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { + throw new LexerException(charPositionInLine, msg); } - - @Override - public String toString() { - return "NumberToken(" + value + ")"; - } - } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/LexerException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LexerException.java similarity index 92% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/LexerException.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LexerException.java index 3e08b2732..ede88dee8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/LexerException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LexerException.java @@ -17,9 +17,7 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression.lexer; - -import com.sk89q.worldedit.internal.expression.ExpressionException; +package com.sk89q.worldedit.internal.expression; /** * Thrown when the lexer encounters a problem. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LocalSlot.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LocalSlot.java new file mode 100644 index 000000000..7bfe5517c --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/LocalSlot.java @@ -0,0 +1,69 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program 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 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +/** + * Represents the metadata for a named local slot. + */ +public interface LocalSlot { + + final class Constant implements LocalSlot { + private final double value; + + public Constant(double value) { + this.value = value; + } + + @Override + public double getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + + final class Variable implements LocalSlot { + private double value; + + public Variable(double value) { + this.value = value; + } + + public void setValue(double value) { + this.value = value; + } + + @Override + public double getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + + double getValue(); + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/OperatorToken.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ParserErrorListener.java similarity index 65% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/OperatorToken.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ParserErrorListener.java index c4b70e475..937c00154 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/OperatorToken.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ParserErrorListener.java @@ -17,28 +17,15 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression.lexer.tokens; +package com.sk89q.worldedit.internal.expression; -/** - * A unary or binary operator. - */ -public class OperatorToken extends Token { - - public final String operator; - - public OperatorToken(int position, String operator) { - super(position); - this.operator = operator; - } +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +class ParserErrorListener extends BaseErrorListener { @Override - public char id() { - return 'o'; + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { + throw new ParserException(charPositionInLine, msg); } - - @Override - public String toString() { - return "OperatorToken(" + operator + ")"; - } - } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/ParserException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ParserException.java similarity index 92% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/ParserException.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ParserException.java index 9a04fc914..30a3eace8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/ParserException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ParserException.java @@ -17,9 +17,7 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression.parser; - -import com.sk89q.worldedit.internal.expression.ExpressionException; +package com.sk89q.worldedit.internal.expression; /** * Thrown when the parser encounters a problem. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/SlotTable.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/SlotTable.java new file mode 100644 index 000000000..e47d4200e --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/SlotTable.java @@ -0,0 +1,64 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program 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 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.Set; + +public class SlotTable { + + private final Map slots = new HashMap<>(); + + public Set keySet() { + return slots.keySet(); + } + + public void putSlot(String name, LocalSlot slot) { + slots.put(name, slot); + } + + public boolean containsSlot(String name) { + return slots.containsKey(name); + } + + public Optional initVariable(String name) { + slots.computeIfAbsent(name, n -> new LocalSlot.Variable(0)); + return getVariable(name); + } + + public Optional getSlot(String name) { + return Optional.ofNullable(slots.get(name)); + } + + public Optional getVariable(String name) { + return getSlot(name) + .filter(LocalSlot.Variable.class::isInstance) + .map(LocalSlot.Variable.class::cast); + } + + public OptionalDouble getSlotValue(String name) { + LocalSlot slot = slots.get(name); + return slot == null ? OptionalDouble.empty() : OptionalDouble.of(slot.getValue()); + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/Lexer.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/Lexer.java deleted file mode 100644 index 8e5b3bbec..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/Lexer.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.lexer; - -import com.sk89q.worldedit.internal.expression.lexer.tokens.CharacterToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.IdentifierToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.KeywordToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.NumberToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.OperatorToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.Token; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Processes a string into a list of tokens. - * - *

Tokens can be numbers, identifiers, operators and assorted other - * characters.

- */ -public class Lexer { - - private final String expression; - private int position = 0; - - private Lexer(String expression) { - this.expression = expression; - } - - public static List 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("*="), - '*', 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(">", - '>', 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 Set characterTokens = new HashSet<>(); - static { - characterTokens.add(','); - characterTokens.add('('); - characterTokens.add(')'); - characterTokens.add('{'); - characterTokens.add('}'); - characterTokens.add(';'); - characterTokens.add('?'); - characterTokens.add(':'); - } - - private static final Set keywords = - new HashSet<>(Arrays.asList("if", "else", "while", "do", "for", "break", "continue", "return", "switch", "case", "default")); - - 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 List tokenize() throws LexerException { - List tokens = new ArrayList<>(); - - do { - skipWhitespace(); - if (position >= expression.length()) { - break; - } - - Token token = operatorTree.evaluate(position); - if (token != null) { - tokens.add(token); - continue; - } - - final char ch = peek(); - - if (characterTokens.contains(ch)) { - tokens.add(new CharacterToken(position++, ch)); - continue; - } - - 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()) { - if (keywords.contains(identifierPart)) { - tokens.add(new KeywordToken(position, identifierPart)); - } else { - 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 void skipWhitespace() { - while (position < expression.length() && Character.isWhitespace(peek())) { - ++position; - } - } - - public class DecisionTree { - private final String tokenName; - private final Map subTrees = new HashMap<>(); - - 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); - } - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/CharacterToken.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/CharacterToken.java deleted file mode 100644 index 51bf757da..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/CharacterToken.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.lexer.tokens; - -/** - * A single character that doesn't fit any of the other token categories. - */ -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 + ")"; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/IdentifierToken.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/IdentifierToken.java deleted file mode 100644 index 4a840f754..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/IdentifierToken.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.lexer.tokens; - -/** - * An identifier. - */ -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 + ")"; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/KeywordToken.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/KeywordToken.java deleted file mode 100644 index 208e7280c..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/KeywordToken.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.lexer.tokens; - -/** - * A keyword. - */ -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 'k'; - } - - @Override - public String toString() { - return "KeywordToken(" + value + ")"; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/Token.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/Token.java deleted file mode 100644 index c6427f0c5..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/lexer/tokens/Token.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.lexer.tokens; - -import com.sk89q.worldedit.internal.expression.Identifiable; - -/** - * A token. The lexer generates these to make the parser's job easier. - */ -public abstract class Token implements Identifiable { - - private final int position; - - public Token(int position) { - this.position = position; - } - - @Override - public int getPosition() { - return position; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/Parser.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/Parser.java deleted file mode 100644 index 24f3dafa9..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/Parser.java +++ /dev/null @@ -1,464 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.parser; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.Identifiable; -import com.sk89q.worldedit.internal.expression.lexer.tokens.IdentifierToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.KeywordToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.NumberToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.OperatorToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.Token; -import com.sk89q.worldedit.internal.expression.runtime.Break; -import com.sk89q.worldedit.internal.expression.runtime.Conditional; -import com.sk89q.worldedit.internal.expression.runtime.Constant; -import com.sk89q.worldedit.internal.expression.runtime.For; -import com.sk89q.worldedit.internal.expression.runtime.Function; -import com.sk89q.worldedit.internal.expression.runtime.Functions; -import com.sk89q.worldedit.internal.expression.runtime.LValue; -import com.sk89q.worldedit.internal.expression.runtime.RValue; -import com.sk89q.worldedit.internal.expression.runtime.Return; -import com.sk89q.worldedit.internal.expression.runtime.Sequence; -import com.sk89q.worldedit.internal.expression.runtime.SimpleFor; -import com.sk89q.worldedit.internal.expression.runtime.Switch; -import com.sk89q.worldedit.internal.expression.runtime.While; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -/** - * Processes a list of tokens into an executable tree. - * - *

Tokens can be numbers, identifiers, operators and assorted other characters.

- */ -public class Parser { - private final class NullToken extends Token { - private NullToken(int position) { - super(position); - } - - @Override - public char id() { - return '\0'; - } - - @Override - public String toString() { - return "NullToken"; - } - } - - private final List tokens; - private int position = 0; - private Expression expression; - - private Parser(List tokens, Expression expression) { - this.tokens = tokens; - this.expression = expression; - } - - public static RValue parse(List tokens, Expression expression) throws ParserException { - return new Parser(tokens, expression).parse(); - } - - private RValue parse() throws ParserException { - final RValue ret = parseStatements(false); - if (position < tokens.size()) { - final Token token = peek(); - throw new ParserException(token.getPosition(), "Extra token at the end of the input: " + token); - } - - ret.bindVariables(expression, false); - - return ret; - } - - private RValue parseStatements(boolean singleStatement) throws ParserException { - List statements = new ArrayList<>(); - loop: while (position < tokens.size()) { - boolean expectSemicolon = false; - - final Token current = peek(); - switch (current.id()) { - case '{': - consumeCharacter('{'); - - statements.add(parseStatements(false)); - - consumeCharacter('}'); - - break; - - case '}': - break loop; - - case 'k': - final String keyword = ((KeywordToken) current).value; - switch (keyword.charAt(0)) { - case 'i': { // if - ++position; - final RValue condition = parseBracket(); - final RValue truePart = parseStatements(true); - final RValue falsePart; - - if (hasKeyword("else")) { - ++position; - falsePart = parseStatements(true); - } else { - falsePart = null; - } - - statements.add(new Conditional(current.getPosition(), condition, truePart, falsePart)); - break; - } - - case 'w': { // while - ++position; - final RValue condition = parseBracket(); - final RValue body = parseStatements(true); - - statements.add(new While(current.getPosition(), condition, body, false)); - break; - } - - case 'd': { // do/default - if (hasKeyword("default")) { - break loop; - } - - ++position; - final RValue body = parseStatements(true); - - consumeKeyword("while"); - - final RValue condition = parseBracket(); - - statements.add(new While(current.getPosition(), condition, body, true)); - - expectSemicolon = true; - break; - } - - case 'f': { // for - ++position; - consumeCharacter('('); - int oldPosition = position; - final RValue init = parseExpression(true); - //if ((init instanceof LValue) && ) - if (peek().id() == ';') { - ++position; - final RValue condition = parseExpression(true); - consumeCharacter(';'); - final RValue increment = parseExpression(true); - consumeCharacter(')'); - final RValue body = parseStatements(true); - - statements.add(new For(current.getPosition(), init, condition, increment, body)); - } else { - position = oldPosition; - - final Token variableToken = peek(); - if (!(variableToken instanceof IdentifierToken)) { - throw new ParserException(variableToken.getPosition(), "Expected identifier"); - } - - RValue variable = expression.getVariable(((IdentifierToken) variableToken).value, true); - if (!(variable instanceof LValue)) { - throw new ParserException(variableToken.getPosition(), "Expected variable"); - } - ++position; - - final Token equalsToken = peek(); - if (!(equalsToken instanceof OperatorToken) || !((OperatorToken) equalsToken).operator.equals("=")) { - throw new ParserException(variableToken.getPosition(), "Expected '=' or a term and ';'"); - } - ++position; - - final RValue first = parseExpression(true); - consumeCharacter(','); - final RValue last = parseExpression(true); - consumeCharacter(')'); - final RValue body = parseStatements(true); - - statements.add(new SimpleFor(current.getPosition(), (LValue) variable, first, last, body)); - } // switch (keyword.charAt(0)) - break; - } - - case 'b': // break - ++position; - statements.add(new Break(current.getPosition(), false)); - break; - - case 'c': // continue/case - if (hasKeyword("case")) { - break loop; - } - - ++position; - statements.add(new Break(current.getPosition(), true)); - break; - - case 'r': // return - ++position; - statements.add(new Return(current.getPosition(), parseExpression(true))); - - expectSemicolon = true; - break; - - case 's': // switch - ++position; - final RValue parameter = parseBracket(); - final List values = new ArrayList<>(); - final List caseStatements = new ArrayList<>(); - RValue defaultCase = null; - - consumeCharacter('{'); - while (peek().id() != '}') { - if (position >= tokens.size()) { - throw new ParserException(current.getPosition(), "Expected '}' instead of EOF"); - } - if (defaultCase != null) { - throw new ParserException(current.getPosition(), "Expected '}' instead of " + peek()); - } - - if (hasKeyword("case")) { - ++position; - - final Token valueToken = peek(); - if (!(valueToken instanceof NumberToken)) { - throw new ParserException(current.getPosition(), "Expected number instead of " + peek()); - } - - ++position; - - values.add(((NumberToken) valueToken).value); - - consumeCharacter(':'); - caseStatements.add(parseStatements(false)); - } else if (hasKeyword("default")) { - ++position; - - consumeCharacter(':'); - defaultCase = parseStatements(false); - } else { - throw new ParserException(current.getPosition(), "Expected 'case' or 'default' instead of " + peek()); - } - } - consumeCharacter('}'); - - statements.add(new Switch(current.getPosition(), parameter, values, caseStatements, defaultCase)); - break; - - default: - throw new ParserException(current.getPosition(), "Unexpected keyword '" + keyword + "'"); - } - - break; - - default: - statements.add(parseExpression(true)); - - expectSemicolon = true; - } // switch (current.id()) - - if (expectSemicolon) { - if (peek().id() == ';') { - ++position; - } else { - break; - } - } - - if (singleStatement) { - break; - } - } // while (position < tokens.size()) - - switch (statements.size()) { - case 0: - if (singleStatement) { - throw new ParserException(peek().getPosition(), "Statement expected."); - } - - return new Sequence(peek().getPosition()); - - case 1: - return statements.get(0); - - default: - return new Sequence(peek().getPosition(), statements.toArray(new RValue[statements.size()])); - } - } - - private RValue parseExpression(boolean canBeEmpty) throws ParserException { - LinkedList halfProcessed = new LinkedList<>(); - - // 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(parseFunctionCall(identifierToken)); - } else { - final RValue variable = expression.getVariable(identifierToken.value, false); - if (variable == null) { - halfProcessed.add(new UnboundVariable(identifierToken.getPosition(), identifierToken.value)); - } else { - halfProcessed.add(variable); - } - } - expressionStart = false; - break; - - case '(': - halfProcessed.add(parseBracket()); - expressionStart = false; - break; - - case ',': - case ')': - case '}': - case ';': - break loop; - - case 'o': - if (expressionStart) { - // Preprocess prefix operators into unary operators - halfProcessed.add(new UnaryOperator((OperatorToken) current)); - } else { - halfProcessed.add(current); - } - ++position; - expressionStart = true; - break; - - default: - halfProcessed.add(current); - ++position; - expressionStart = false; - break; - } - } - - if (halfProcessed.isEmpty() && canBeEmpty) { - return new Sequence(peek().getPosition()); - } - - return ParserProcessors.processExpression(halfProcessed); - } - - - private Token peek() { - if (position >= tokens.size()) { - return new NullToken(tokens.get(tokens.size() - 1).getPosition() + 1); - } - - return tokens.get(position); - } - - private Function parseFunctionCall(IdentifierToken identifierToken) throws ParserException { - consumeCharacter('('); - - try { - if (peek().id() == ')') { - ++position; - return Functions.getFunction(identifierToken.getPosition(), identifierToken.value); - } - - List args = new ArrayList<>(); - - loop: while (true) { - args.add(parseExpression(false)); - - 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 RValue[args.size()])); - } catch (NoSuchMethodException e) { - throw new ParserException(identifierToken.getPosition(), "Function '" + identifierToken.value + "' not found", e); - } - } - - private RValue parseBracket() throws ParserException { - consumeCharacter('('); - - final RValue ret = parseExpression(false); - - consumeCharacter(')'); - - return ret; - } - - private boolean hasKeyword(String keyword) { - final Token next = peek(); - - return next instanceof KeywordToken && ((KeywordToken) next).value.equals(keyword); - } - - private void assertCharacter(char character) throws ParserException { - final Token next = peek(); - if (next.id() != character) { - throw new ParserException(next.getPosition(), "Expected '" + character + "'"); - } - } - - private void assertKeyword(String keyword) throws ParserException { - if (!hasKeyword(keyword)) { - throw new ParserException(peek().getPosition(), "Expected '" + keyword + "'"); - } - } - - private void consumeCharacter(char character) throws ParserException { - assertCharacter(character); - ++position; - } - - private void consumeKeyword(String keyword) throws ParserException { - assertKeyword(keyword); - ++position; - } -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/ParserProcessors.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/ParserProcessors.java deleted file mode 100644 index b50997cc6..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/ParserProcessors.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.parser; - -import com.sk89q.worldedit.internal.expression.Identifiable; -import com.sk89q.worldedit.internal.expression.lexer.tokens.OperatorToken; -import com.sk89q.worldedit.internal.expression.lexer.tokens.Token; -import com.sk89q.worldedit.internal.expression.runtime.Conditional; -import com.sk89q.worldedit.internal.expression.runtime.Operators; -import com.sk89q.worldedit.internal.expression.runtime.RValue; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Map; - -/** - * Helper classfor Parser. Contains processors for statements and operators. - */ -public final class ParserProcessors { - - private static final Map unaryOpMap = new HashMap<>(); - - private static final Map[] binaryOpMapsLA; - private static final Map[] binaryOpMapsRA; - - static { - unaryOpMap.put("-", "neg"); - unaryOpMap.put("!", "not"); - unaryOpMap.put("~", "inv"); - unaryOpMap.put("++", "inc"); - unaryOpMap.put("--", "dec"); - unaryOpMap.put("x++", "postinc"); - unaryOpMap.put("x--", "postdec"); - unaryOpMap.put("x!", "fac"); - - final Object[][][] binaryOpsLA = { - { - { "^", "pow" }, - { "**", "pow" }, - }, - { - { "*", "mul" }, - { "/", "div" }, - { "%", "mod" }, - }, - { - { "+", "add" }, - { "-", "sub" }, - }, - { - { "<<", "shl" }, - { ">>", "shr" }, - }, - { - { "<", "lth" }, - { ">", "gth" }, - { "<=", "leq" }, - { ">=", "geq" }, - }, - { - { "==", "equ" }, - { "!=", "neq" }, - { "~=", "near" }, - }, - { - { "&&", "and" }, - }, - { - { "||", "or" }, - }, - }; - final Object[][][] binaryOpsRA = { - { - { "=", "ass" }, - { "+=", "aadd" }, - { "-=", "asub" }, - { "*=", "amul" }, - { "/=", "adiv" }, - { "%=", "amod" }, - { "^=", "aexp" }, - }, - }; - - @SuppressWarnings("unchecked") - final Map[] lBinaryOpMapsLA = binaryOpMapsLA = new Map[binaryOpsLA.length]; - for (int i = 0; i < binaryOpsLA.length; ++i) { - final Object[][] a = binaryOpsLA[i]; - switch (a.length) { - case 0: - lBinaryOpMapsLA[i] = Collections.emptyMap(); - break; - - case 1: - final Object[] first = a[0]; - lBinaryOpMapsLA[i] = Collections.singletonMap((String) first[0], (String) first[1]); - break; - - default: - Map m = lBinaryOpMapsLA[i] = new HashMap<>(); - for (final Object[] element : a) { - m.put((String) element[0], (String) element[1]); - } - } - } - - @SuppressWarnings("unchecked") - final Map[] lBinaryOpMapsRA = binaryOpMapsRA = new Map[binaryOpsRA.length]; - for (int i = 0; i < binaryOpsRA.length; ++i) { - final Object[][] a = binaryOpsRA[i]; - switch (a.length) { - case 0: - lBinaryOpMapsRA[i] = Collections.emptyMap(); - break; - - case 1: - final Object[] first = a[0]; - lBinaryOpMapsRA[i] = Collections.singletonMap((String) first[0], (String) first[1]); - break; - - default: - Map m = lBinaryOpMapsRA[i] = new HashMap<>(); - for (final Object[] element : a) { - m.put((String) element[0], (String) element[1]); - } - } - } - } - - private ParserProcessors() { - } - - static RValue processExpression(LinkedList input) throws ParserException { - return processBinaryOpsRA(input, binaryOpMapsRA.length - 1); - } - - private static RValue processBinaryOpsLA(LinkedList input, int level) throws ParserException { - if (level < 0) { - return processUnaryOps(input); - } - - LinkedList lhs = new LinkedList<>(); - LinkedList rhs = new LinkedList<>(); - String operator = null; - - for (Iterator it = input.descendingIterator(); it.hasNext();) { - Identifiable identifiable = it.next(); - if (operator == null) { - rhs.addFirst(identifiable); - - if (!(identifiable instanceof OperatorToken)) { - continue; - } - - operator = binaryOpMapsLA[level].get(((OperatorToken) identifiable).operator); - if (operator == null) { - continue; - } - - rhs.removeFirst(); - } else { - lhs.addFirst(identifiable); - } - } - - RValue rhsInvokable = processBinaryOpsLA(rhs, level - 1); - if (operator == null) return rhsInvokable; - - RValue lhsInvokable = processBinaryOpsLA(lhs, level); - - try { - return Operators.getOperator(input.get(0).getPosition(), operator, lhsInvokable, rhsInvokable); - } catch (NoSuchMethodException e) { - final Token operatorToken = (Token) input.get(lhs.size()); - throw new ParserException(operatorToken.getPosition(), "Couldn't find operator '" + operator + "'"); - } - } - - private static RValue processBinaryOpsRA(LinkedList input, int level) throws ParserException { - if (level < 0) { - return processTernaryOps(input); - } - - LinkedList lhs = new LinkedList<>(); - LinkedList rhs = new LinkedList<>(); - String operator = null; - - for (Identifiable identifiable : input) { - if (operator == null) { - lhs.addLast(identifiable); - - if (!(identifiable instanceof OperatorToken)) { - continue; - } - - operator = binaryOpMapsRA[level].get(((OperatorToken) identifiable).operator); - if (operator == null) { - continue; - } - - lhs.removeLast(); - } else { - rhs.addLast(identifiable); - } - } - - RValue lhsInvokable = processBinaryOpsRA(lhs, level - 1); - if (operator == null) return lhsInvokable; - - RValue rhsInvokable = processBinaryOpsRA(rhs, level); - - try { - return Operators.getOperator(input.get(0).getPosition(), operator, lhsInvokable, rhsInvokable); - } catch (NoSuchMethodException e) { - final Token operatorToken = (Token) input.get(lhs.size()); - throw new ParserException(operatorToken.getPosition(), "Couldn't find operator '" + operator + "'"); - } - } - - private static RValue processTernaryOps(LinkedList input) throws ParserException { - LinkedList lhs = new LinkedList<>(); - LinkedList mhs = new LinkedList<>(); - LinkedList rhs = new LinkedList<>(); - - int partsFound = 0; - int conditionalsFound = 0; - - for (Identifiable identifiable : input) { - final char character = identifiable.id(); - switch (character) { - case '?': - ++conditionalsFound; - break; - case ':': - --conditionalsFound; - break; - } - - if (conditionalsFound < 0) { - throw new ParserException(identifiable.getPosition(), "Unexpected ':'"); - } - - switch (partsFound) { - case 0: - if (character == '?') { - partsFound = 1; - } else { - lhs.addLast(identifiable); - } - break; - - case 1: - if (conditionalsFound == 0 && character == ':') { - partsFound = 2; - } else { - mhs.addLast(identifiable); - } - break; - - case 2: - rhs.addLast(identifiable); - } - } - - if (partsFound < 2) { - return processBinaryOpsLA(input, binaryOpMapsLA.length - 1); - } - - RValue lhsInvokable = processBinaryOpsLA(lhs, binaryOpMapsLA.length - 1); - RValue mhsInvokable = processTernaryOps(mhs); - RValue rhsInvokable = processTernaryOps(rhs); - - return new Conditional(input.get(lhs.size()).getPosition(), lhsInvokable, mhsInvokable, rhsInvokable); - } - - private static RValue processUnaryOps(LinkedList input) throws ParserException { - // Preprocess postfix operators into unary operators - final Identifiable center; - LinkedList postfixes = new LinkedList<>(); - do { - if (input.isEmpty()) { - throw new ParserException(-1, "Expression missing."); - } - - final Identifiable last = input.removeLast(); - if (last instanceof OperatorToken) { - postfixes.addLast(new UnaryOperator(last.getPosition(), "x" + ((OperatorToken) last).operator)); - } else if (last instanceof UnaryOperator) { - postfixes.addLast(new UnaryOperator(last.getPosition(), "x" + ((UnaryOperator) last).operator)); - } else { - center = last; - break; - } - } while (true); - - if (!(center instanceof RValue)) { - throw new ParserException(center.getPosition(), "Expected expression, found " + center); - } - - input.addAll(postfixes); - - RValue ret = (RValue) center; - while (!input.isEmpty()) { - final Identifiable last = input.removeLast(); - final int lastPosition = last.getPosition(); - if (last instanceof UnaryOperator) { - final String operator = ((UnaryOperator) last).operator; - if (operator.equals("+")) { - continue; - } - - String opName = unaryOpMap.get(operator); - if (opName != null) { - try { - ret = Operators.getOperator(lastPosition, opName, ret); - continue; - } catch (NoSuchMethodException e) { - throw new ParserException(lastPosition, "No such prefix operator: " + operator); - } - } - } - - if (last instanceof Token) { - throw new ParserException(lastPosition, "Extra token found in expression: " + last); - } else if (last instanceof RValue) { - throw new ParserException(lastPosition, "Extra expression found: " + last); - } else { - throw new ParserException(lastPosition, "Extra element found: " + last); - } - } - return ret; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/PseudoToken.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/PseudoToken.java deleted file mode 100644 index cee19e8b3..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/PseudoToken.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.parser; - -import com.sk89q.worldedit.internal.expression.Identifiable; - -/** - * A pseudo-token, inserted by the parser instead of the lexer. - */ -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; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/UnaryOperator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/UnaryOperator.java deleted file mode 100644 index d6a54b321..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/UnaryOperator.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.parser; - -import com.sk89q.worldedit.internal.expression.lexer.tokens.OperatorToken; - -/** - * The parser uses this pseudo-token to mark operators as unary operators. - */ -public class UnaryOperator extends PseudoToken { - - final String operator; - - public UnaryOperator(OperatorToken operatorToken) { - this(operatorToken.getPosition(), operatorToken.operator); - } - - public UnaryOperator(int position, String operator) { - super(position); - this.operator = operator; - } - - @Override - public char id() { - return 'p'; - } - - @Override - public String toString() { - return "UnaryOperator(" + operator + ")"; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/UnboundVariable.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/UnboundVariable.java deleted file mode 100644 index a128bc866..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/parser/UnboundVariable.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.parser; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; -import com.sk89q.worldedit.internal.expression.runtime.LValue; -import com.sk89q.worldedit.internal.expression.runtime.RValue; - -public class UnboundVariable extends PseudoToken implements LValue { - - public final String name; - - public UnboundVariable(int position, String name) { - super(position); - this.name = name; - } - - @Override - public char id() { - return 'V'; - } - - @Override - public String toString() { - return "UnboundVariable(" + name + ")"; - } - - @Override - public double getValue() throws EvaluationException { - throw new EvaluationException(getPosition(), "Tried to evaluate unbound variable!"); - } - - @Override - public LValue optimize() throws EvaluationException { - throw new EvaluationException(getPosition(), "Tried to optimize unbound variable!"); - } - - @Override - public double assign(double value) throws EvaluationException { - throw new EvaluationException(getPosition(), "Tried to assign unbound variable!"); - } - - public RValue bind(Expression expression, boolean isLValue) throws ParserException { - final RValue variable = expression.getVariable(name, isLValue); - if (variable == null) { - throw new ParserException(getPosition(), "Variable '" + name + "' not found"); - } - - return variable; - } - - @Override - public LValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - final RValue variable = expression.getVariable(name, preferLValue); - if (variable == null) { - throw new ParserException(getPosition(), "Variable '" + name + "' not found"); - } - - return (LValue) variable; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Break.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Break.java deleted file mode 100644 index 00bcf9936..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Break.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -/** - * A break or continue statement. - */ -public class Break extends Node { - - boolean doContinue; - - public Break(int position, boolean doContinue) { - super(position); - - this.doContinue = doContinue; - } - - @Override - public double getValue() throws EvaluationException { - throw new BreakException(doContinue); - } - - @Override - public char id() { - return 'b'; - } - - @Override - public String toString() { - return doContinue ? "continue" : "break"; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Conditional.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Conditional.java deleted file mode 100644 index 4579004af..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Conditional.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * An if/else statement or a ternary operator. - */ -public class Conditional extends Node { - - private RValue condition; - private RValue truePart; - private RValue falsePart; - - public Conditional(int position, RValue condition, RValue truePart, RValue falsePart) { - super(position); - - this.condition = condition; - this.truePart = truePart; - this.falsePart = falsePart; - } - - @Override - public double getValue() throws EvaluationException { - if (condition.getValue() > 0.0) { - return truePart.getValue(); - } else { - return falsePart == null ? 0.0 : falsePart.getValue(); - } - } - - @Override - public char id() { - return 'I'; - } - - @Override - public String toString() { - if (falsePart == null) { - return "if (" + condition + ") { " + truePart + " }"; - } else if (truePart instanceof Sequence || falsePart instanceof Sequence) { - return "if (" + condition + ") { " + truePart + " } else { " + falsePart + " }"; - } else { - return "(" + condition + ") ? (" + truePart + ") : (" + falsePart + ")"; - } - } - - @Override - public RValue optimize() throws EvaluationException { - final RValue newCondition = condition.optimize(); - - if (newCondition instanceof Constant) { - if (newCondition.getValue() > 0) { - return truePart.optimize(); - } else { - return falsePart == null ? new Constant(getPosition(), 0.0) : falsePart.optimize(); - } - } - - return new Conditional(getPosition(), newCondition, truePart.optimize(), falsePart == null ? null : falsePart.optimize()); - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - condition = condition.bindVariables(expression, false); - truePart = truePart.bindVariables(expression, false); - if (falsePart != null) { - falsePart = falsePart.bindVariables(expression, false); - } - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Constant.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Constant.java deleted file mode 100644 index 5bdab9e1c..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Constant.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -/** - * A constant. - */ -public final class Constant extends Node { - - private final double value; - - public Constant(int position, double value) { - super(position); - this.value = value; - } - - @Override - public double getValue() { - return value; - } - - @Override - public String toString() { - return String.valueOf(value); - } - - @Override - public char id() { - return 'c'; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/For.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/For.java deleted file mode 100644 index 6334a7350..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/For.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * A Java/C-style for loop. - */ -public class For extends Node { - - RValue init; - RValue condition; - RValue increment; - RValue body; - - public For(int position, RValue init, RValue condition, RValue increment, RValue body) { - super(position); - - this.init = init; - this.condition = condition; - this.increment = increment; - this.body = body; - } - - @Override - public double getValue() throws EvaluationException { - int iterations = 0; - double ret = 0.0; - - for (init.getValue(); condition.getValue() > 0; increment.getValue()) { - if (iterations > 256) { - throw new EvaluationException(getPosition(), "Loop exceeded 256 iterations."); - } - if (Thread.interrupted()) { - throw new EvaluationException(getPosition(), "Calculations exceeded time limit."); - } - ++iterations; - - try { - ret = body.getValue(); - } catch (BreakException e) { - if (e.doContinue) { - //noinspection UnnecessaryContinue - continue; - } else { - break; - } - } - } - - return ret; - } - - @Override - public char id() { - return 'F'; - } - - @Override - public String toString() { - return "for (" + init + "; " + condition + "; " + increment + ") { " + body + " }"; - } - - @Override - public RValue optimize() throws EvaluationException { - final RValue newCondition = condition.optimize(); - - if (newCondition instanceof Constant && newCondition.getValue() <= 0) { - // If the condition is always false, the loop can be flattened. - // So we run the init part and then return 0.0. - return new Sequence(getPosition(), init, new Constant(getPosition(), 0.0)).optimize(); - } - - //return new Sequence(getPosition(), init.optimize(), new While(getPosition(), condition, new Sequence(getPosition(), body, increment), false)).optimize(); - return new For(getPosition(), init.optimize(), newCondition, increment.optimize(), body.optimize()); - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - init = init.bindVariables(expression, false); - condition = condition.bindVariables(expression, false); - increment = increment.bindVariables(expression, false); - body = body.bindVariables(expression, false); - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Function.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Function.java deleted file mode 100644 index 1fef4a580..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Function.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Wrapper for a Java method and its arguments (other Nodes). - */ -public class Function extends Node { - - /** - * Add this annotation on functions that don't always return the same value - * for the same inputs and on functions with side-effects. - */ - @Retention(RetentionPolicy.RUNTIME) - public @interface Dynamic { } - - final Method method; - final RValue[] args; - - Function(int position, Method method, RValue... args) { - super(position); - this.method = method; - this.args = args; - } - - @Override - public final double getValue() throws EvaluationException { - return invokeMethod(method, args); - } - - protected static double invokeMethod(Method method, Object[] args) throws EvaluationException { - try { - return (Double) method.invoke(null, 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 RValue optimize() throws EvaluationException { - final RValue[] optimizedArgs = new RValue[args.length]; - boolean optimizable = !method.isAnnotationPresent(Dynamic.class); - int position = getPosition(); - for (int i = 0; i < args.length; ++i) { - final RValue optimized = optimizedArgs[i] = args[i].optimize(); - - if (!(optimized instanceof Constant)) { - optimizable = false; - } - - if (optimized.getPosition() < position) { - position = optimized.getPosition(); - } - } - - if (optimizable) { - return new Constant(position, invokeMethod(method, optimizedArgs)); - } else { - return new Function(position, method, optimizedArgs); - } - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - final Class[] parameters = method.getParameterTypes(); - for (int i = 0; i < args.length; ++i) { - final boolean argumentPrefersLValue = LValue.class.isAssignableFrom(parameters[i]); - args[i] = args[i].bindVariables(expression, argumentPrefersLValue); - } - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Functions.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Functions.java deleted file mode 100644 index d2aede416..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Functions.java +++ /dev/null @@ -1,487 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.runtime.Function.Dynamic; -import com.sk89q.worldedit.math.Vector3; -import com.sk89q.worldedit.math.noise.PerlinNoise; -import com.sk89q.worldedit.math.noise.RidgedMultiFractalNoise; -import com.sk89q.worldedit.math.noise.VoronoiNoise; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; - -/** - * Contains all functions that can be used in expressions. - */ -@SuppressWarnings("UnusedDeclaration") -public final class Functions { - - private static class Overload { - private final Method method; - private final int mask; - private final boolean isSetter; - - private Overload(Method method) throws IllegalArgumentException { - this.method = method; - - boolean isSetter = false; - int accum = 0; - Class[] parameters = method.getParameterTypes(); - for (Class parameter : parameters) { - if (isSetter) { - throw new IllegalArgumentException("Method takes arguments that can't be cast to RValue."); - } - - if (double.class.equals(parameter)) { - isSetter = true; - continue; - } - - if (!RValue.class.isAssignableFrom(parameter)) { - throw new IllegalArgumentException("Method takes arguments that can't be cast to RValue."); - } - - accum <<= 2; - - if (LValue.class.isAssignableFrom(parameter)) { - accum |= 3; - } else { - accum |= 1; - } - } - mask = accum; - this.isSetter = isSetter; - } - - public boolean matches(boolean isSetter, RValue... args) { - if (this.isSetter != isSetter) { - return false; - } - - if (this.method.getParameterTypes().length != args.length) { // TODO: optimize - return false; - } - - int accum = 0; - for (RValue argument : args) { - accum <<= 2; - - if (argument instanceof LValue) { - accum |= 3; - } else { - accum |= 1; - } - } - - return (accum & mask) == mask; - } - } - - public static Function getFunction(int position, String name, RValue... args) throws NoSuchMethodException { - final Method getter = getMethod(name, false, args); - try { - Method setter = getMethod(name, true, args); - return new LValueFunction(position, getter, setter, args); - } catch (NoSuchMethodException e) { - return new Function(position, getter, args); - } - } - - private static Method getMethod(String name, boolean isSetter, RValue... args) throws NoSuchMethodException { - final List overloads = functions.get(name); - if (overloads != null) { - for (Overload overload : overloads) { - if (overload.matches(isSetter, args)) { - return overload.method; - } - } - } - - throw new NoSuchMethodException(); // TODO: return null (check for side-effects first) - } - - private static final Map> functions = new HashMap<>(); - static { - for (Method method : Functions.class.getMethods()) { - try { - addFunction(method); - } catch (IllegalArgumentException ignored) { } - } - } - - - public static void addFunction(Method method) throws IllegalArgumentException { - final String methodName = method.getName(); - - Overload overload = new Overload(method); - - List overloads = functions.computeIfAbsent(methodName, k -> new ArrayList<>()); - - overloads.add(overload); - } - - - public static double sin(RValue x) throws EvaluationException { - return Math.sin(x.getValue()); - } - - public static double cos(RValue x) throws EvaluationException { - return Math.cos(x.getValue()); - } - - public static double tan(RValue x) throws EvaluationException { - return Math.tan(x.getValue()); - } - - - public static double asin(RValue x) throws EvaluationException { - return Math.asin(x.getValue()); - } - - public static double acos(RValue x) throws EvaluationException { - return Math.acos(x.getValue()); - } - - public static double atan(RValue x) throws EvaluationException { - return Math.atan(x.getValue()); - } - - public static double atan2(RValue y, RValue x) throws EvaluationException { - return Math.atan2(y.getValue(), x.getValue()); - } - - - public static double sinh(RValue x) throws EvaluationException { - return Math.sinh(x.getValue()); - } - - public static double cosh(RValue x) throws EvaluationException { - return Math.cosh(x.getValue()); - } - - public static double tanh(RValue x) throws EvaluationException { - return Math.tanh(x.getValue()); - } - - - public static double sqrt(RValue x) throws EvaluationException { - return Math.sqrt(x.getValue()); - } - - public static double cbrt(RValue x) throws EvaluationException { - return Math.cbrt(x.getValue()); - } - - - public static double abs(RValue x) throws EvaluationException { - return Math.abs(x.getValue()); - } - - public static double min(RValue a, RValue b) throws EvaluationException { - return Math.min(a.getValue(), b.getValue()); - } - - public static double min(RValue a, RValue b, RValue c) throws EvaluationException { - return Math.min(a.getValue(), Math.min(b.getValue(), c.getValue())); - } - - public static double max(RValue a, RValue b) throws EvaluationException { - return Math.max(a.getValue(), b.getValue()); - } - - public static double max(RValue a, RValue b, RValue c) throws EvaluationException { - return Math.max(a.getValue(), Math.max(b.getValue(), c.getValue())); - } - - - public static double ceil(RValue x) throws EvaluationException { - return Math.ceil(x.getValue()); - } - - public static double floor(RValue x) throws EvaluationException { - return Math.floor(x.getValue()); - } - - public static double rint(RValue x) throws EvaluationException { - return Math.rint(x.getValue()); - } - - public static double round(RValue x) throws EvaluationException { - return Math.round(x.getValue()); - } - - - public static double exp(RValue x) throws EvaluationException { - return Math.exp(x.getValue()); - } - - public static double ln(RValue x) throws EvaluationException { - return Math.log(x.getValue()); - } - - public static double log(RValue x) throws EvaluationException { - return Math.log(x.getValue()); - } - - public static double log10(RValue x) throws EvaluationException { - return Math.log10(x.getValue()); - } - - - public static double rotate(LValue x, LValue y, RValue angle) throws EvaluationException { - final double f = angle.getValue(); - - final double cosF = Math.cos(f); - final double sinF = Math.sin(f); - - final double xOld = x.getValue(); - final double yOld = y.getValue(); - - x.assign(xOld * cosF - yOld * sinF); - y.assign(xOld * sinF + yOld * cosF); - - return 0.0; - } - - public static double swap(LValue x, LValue y) throws EvaluationException { - final double tmp = x.getValue(); - - x.assign(y.getValue()); - y.assign(tmp); - - return 0.0; - } - - - private static final Map gmegabuf = new HashMap<>(); - private final Map megabuf = new HashMap<>(); - - public Map getMegabuf() { - return megabuf; - } - - private static double[] getSubBuffer(Map megabuf, Integer key) { - double[] ret = megabuf.get(key); - if (ret == null) { - megabuf.put(key, ret = new double[1024]); - } - return ret; - } - - private static double getBufferItem(final Map megabuf, final int index) { - return getSubBuffer(megabuf, index & ~1023)[index & 1023]; - } - - private static double setBufferItem(final Map megabuf, final int index, double value) { - return getSubBuffer(megabuf, index & ~1023)[index & 1023] = value; - } - - @Dynamic - public static double gmegabuf(RValue index) throws EvaluationException { - return getBufferItem(gmegabuf, (int) index.getValue()); - } - - @Dynamic - public static double gmegabuf(RValue index, double value) throws EvaluationException { - return setBufferItem(gmegabuf, (int) index.getValue(), value); - } - - @Dynamic - public static double megabuf(RValue index) throws EvaluationException { - return getBufferItem(Expression.getInstance().getFunctions().megabuf, (int) index.getValue()); - } - - @Dynamic - public static double megabuf(RValue index, double value) throws EvaluationException { - return setBufferItem(Expression.getInstance().getFunctions().megabuf, (int) index.getValue(), value); - } - - @Dynamic - public static double closest(RValue x, RValue y, RValue z, RValue index, RValue count, RValue stride) throws EvaluationException { - return findClosest( - Expression.getInstance().getFunctions().megabuf, - x.getValue(), - y.getValue(), - z.getValue(), - (int) index.getValue(), - (int) count.getValue(), - (int) stride.getValue() - ); - } - - @Dynamic - public static double gclosest(RValue x, RValue y, RValue z, RValue index, RValue count, RValue stride) throws EvaluationException { - return findClosest( - gmegabuf, - x.getValue(), - y.getValue(), - z.getValue(), - (int) index.getValue(), - (int) count.getValue(), - (int) stride.getValue() - ); - } - - private static double findClosest(Map megabuf, double x, double y, double z, int index, int count, int stride) { - int closestIndex = -1; - double minDistanceSquared = Double.MAX_VALUE; - - for (int i = 0; i < count; ++i) { - double currentX = getBufferItem(megabuf, index+0) - x; - double currentY = getBufferItem(megabuf, index+1) - y; - double currentZ = getBufferItem(megabuf, index+2) - z; - - double currentDistanceSquared = currentX*currentX + currentY*currentY + currentZ*currentZ; - - if (currentDistanceSquared < minDistanceSquared) { - minDistanceSquared = currentDistanceSquared; - closestIndex = index; - } - - index += stride; - } - - return closestIndex; - } - - - private static final Random random = new Random(); - - @Dynamic - public static double random() { - return random.nextDouble(); - } - - @Dynamic - public static double randint(RValue max) throws EvaluationException { - return random.nextInt((int) Math.floor(max.getValue())); - } - - private static final ThreadLocal localPerlin = ThreadLocal.withInitial(PerlinNoise::new); - - public static double perlin(RValue seed, RValue x, RValue y, RValue z, RValue frequency, RValue octaves, RValue persistence) throws EvaluationException { - PerlinNoise perlin = localPerlin.get(); - try { - perlin.setSeed((int) seed.getValue()); - perlin.setFrequency(frequency.getValue()); - perlin.setOctaveCount((int) octaves.getValue()); - perlin.setPersistence(persistence.getValue()); - } catch (IllegalArgumentException e) { - throw new EvaluationException(0, "Perlin noise error: " + e.getMessage()); - } - return perlin.noise(Vector3.at(x.getValue(), y.getValue(), z.getValue())); - } - - private static final ThreadLocal localVoronoi = ThreadLocal.withInitial(VoronoiNoise::new); - - public static double voronoi(RValue seed, RValue x, RValue y, RValue z, RValue frequency) throws EvaluationException { - VoronoiNoise voronoi = localVoronoi.get(); - try { - voronoi.setSeed((int) seed.getValue()); - voronoi.setFrequency(frequency.getValue()); - } catch (IllegalArgumentException e) { - throw new EvaluationException(0, "Voronoi error: " + e.getMessage()); - } - return voronoi.noise(Vector3.at(x.getValue(), y.getValue(), z.getValue())); - } - - private static final ThreadLocal localRidgedMulti = ThreadLocal.withInitial(RidgedMultiFractalNoise::new); - - public static double ridgedmulti(RValue seed, RValue x, RValue y, RValue z, RValue frequency, RValue octaves) throws EvaluationException { - RidgedMultiFractalNoise ridgedMulti = localRidgedMulti.get(); - try { - ridgedMulti.setSeed((int) seed.getValue()); - ridgedMulti.setFrequency(frequency.getValue()); - ridgedMulti.setOctaveCount((int) octaves.getValue()); - } catch (IllegalArgumentException e) { - throw new EvaluationException(0, "Ridged multi error: " + e.getMessage()); - } - return ridgedMulti.noise(Vector3.at(x.getValue(), y.getValue(), z.getValue())); - } - - private static double queryInternal(RValue type, RValue data, double typeId, double dataValue) throws EvaluationException { - // Compare to input values and determine return value - // -1 is a wildcard, always true - final double ret = ((type.getValue() == -1 || typeId == type.getValue()) - && (data.getValue() == -1 || dataValue == data.getValue())) ? 1.0 : 0.0; - - if (type instanceof LValue) { - ((LValue) type).assign(typeId); - } - - if (data instanceof LValue) { - ((LValue) data).assign(dataValue); - } - - return ret; - } - - @Dynamic - public static double query(RValue x, RValue y, RValue z, RValue type, RValue data) throws EvaluationException { - final double xp = x.getValue(); - final double yp = y.getValue(); - final double zp = z.getValue(); - - final ExpressionEnvironment environment = Expression.getInstance().getEnvironment(); - - // Read values from world - final double typeId = environment.getBlockType(xp, yp, zp); - final double dataValue = environment.getBlockData(xp, yp, zp); - - return queryInternal(type, data, typeId, dataValue); - } - - @Dynamic - public static double queryAbs(RValue x, RValue y, RValue z, RValue type, RValue data) throws EvaluationException { - final double xp = x.getValue(); - final double yp = y.getValue(); - final double zp = z.getValue(); - - final ExpressionEnvironment environment = Expression.getInstance().getEnvironment(); - - // Read values from world - final double typeId = environment.getBlockTypeAbs(xp, yp, zp); - final double dataValue = environment.getBlockDataAbs(xp, yp, zp); - - return queryInternal(type, data, typeId, dataValue); - } - - @Dynamic - public static double queryRel(RValue x, RValue y, RValue z, RValue type, RValue data) throws EvaluationException { - final double xp = x.getValue(); - final double yp = y.getValue(); - final double zp = z.getValue(); - - final ExpressionEnvironment environment = Expression.getInstance().getEnvironment(); - - // Read values from world - final double typeId = environment.getBlockTypeRel(xp, yp, zp); - final double dataValue = environment.getBlockDataRel(xp, yp, zp); - - return queryInternal(type, data, typeId, dataValue); - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/LValue.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/LValue.java deleted file mode 100644 index 338cc74b2..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/LValue.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * A value that can be used on the left side of an assignment. - */ -public interface LValue extends RValue { - - double assign(double value) throws EvaluationException; - - @Override - LValue optimize() throws EvaluationException; - - @Override - LValue bindVariables(Expression expression, boolean preferLValue) throws ParserException; - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/LValueFunction.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/LValueFunction.java deleted file mode 100644 index de145514a..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/LValueFunction.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -import java.lang.reflect.Method; - -/** - * Wrapper for a pair of Java methods and their arguments (other Nodes), - * forming an LValue. - */ -public class LValueFunction extends Function implements LValue { - - private final Object[] setterArgs; - private final Method setter; - - LValueFunction(int position, Method getter, Method setter, RValue... args) { - super(position, getter, args); - assert (getter.isAnnotationPresent(Dynamic.class)); - - setterArgs = new Object[args.length + 1]; - System.arraycopy(args, 0, setterArgs, 0, args.length); - this.setter = setter; - } - - @Override - public char id() { - return 'l'; - } - - @Override - public double assign(double value) throws EvaluationException { - setterArgs[setterArgs.length - 1] = value; - return invokeMethod(setter, setterArgs); - } - - @Override - public LValue optimize() throws EvaluationException { - final RValue optimized = super.optimize(); - if (optimized == this) { - return this; - } - - if (optimized instanceof Function) { - return new LValueFunction(optimized.getPosition(), method, setter, ((Function) optimized).args); - } - - return (LValue) optimized; - } - - @Override - public LValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - super.bindVariables(expression, preferLValue); - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Node.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Node.java deleted file mode 100644 index 1c1836c4d..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Node.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * A node in the execution tree of an expression. - */ -public abstract class Node implements RValue { - - private final int position; - - public Node(int position) { - this.position = position; - } - - @Override - public abstract String toString(); - - @Override - public RValue optimize() throws EvaluationException { - return this; - } - - @Override - public final int getPosition() { - return position; - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Operators.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Operators.java deleted file mode 100644 index cb4f9d78b..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Operators.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -/** - * Contains all unary and binary operators. - */ -@SuppressWarnings("UnusedDeclaration") -public final class Operators { - - private Operators() { - } - - public static Function getOperator(int position, String name, RValue lhs, RValue rhs) throws NoSuchMethodException { - if (lhs instanceof LValue) { - try { - return new Function(position, Operators.class.getMethod(name, LValue.class, RValue.class), lhs, rhs); - } catch (NoSuchMethodException ignored) { } - } - return new Function(position, Operators.class.getMethod(name, RValue.class, RValue.class), lhs, rhs); - } - - public static Function getOperator(int position, String name, RValue argument) throws NoSuchMethodException { - if (argument instanceof LValue) { - try { - return new Function(position, Operators.class.getMethod(name, LValue.class), argument); - } catch (NoSuchMethodException ignored) { } - } - return new Function(position, Operators.class.getMethod(name, RValue.class), argument); - } - - - public static double add(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() + rhs.getValue(); - } - - public static double sub(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() - rhs.getValue(); - } - - public static double mul(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() * rhs.getValue(); - } - - public static double div(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() / rhs.getValue(); - } - - public static double mod(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() % rhs.getValue(); - } - - public static double pow(RValue lhs, RValue rhs) throws EvaluationException { - return Math.pow(lhs.getValue(), rhs.getValue()); - } - - - public static double neg(RValue x) throws EvaluationException { - return -x.getValue(); - } - - public static double not(RValue x) throws EvaluationException { - return x.getValue() > 0.0 ? 0.0 : 1.0; - } - - public static double inv(RValue x) throws EvaluationException { - return ~(long) x.getValue(); - } - - - public static double lth(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() < rhs.getValue() ? 1.0 : 0.0; - } - - public static double gth(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() > rhs.getValue() ? 1.0 : 0.0; - } - - public static double leq(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() <= rhs.getValue() ? 1.0 : 0.0; - } - - public static double geq(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() >= rhs.getValue() ? 1.0 : 0.0; - } - - - public static double equ(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() == rhs.getValue() ? 1.0 : 0.0; - } - - public static double neq(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() != rhs.getValue() ? 1.0 : 0.0; - } - - public static double near(RValue lhs, RValue rhs) throws EvaluationException { - return almostEqual2sComplement(lhs.getValue(), rhs.getValue(), 450359963L) ? 1.0 : 0.0; - //return Math.abs(lhs.invoke() - rhs.invoke()) < 1e-7 ? 1.0 : 0.0; - } - - - public static double or(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() > 0.0 || rhs.getValue() > 0.0 ? 1.0 : 0.0; - } - - public static double and(RValue lhs, RValue rhs) throws EvaluationException { - return lhs.getValue() > 0.0 && rhs.getValue() > 0.0 ? 1.0 : 0.0; - } - - - public static double shl(RValue lhs, RValue rhs) throws EvaluationException { - return (long) lhs.getValue() << (long) rhs.getValue(); - } - - public static double shr(RValue lhs, RValue rhs) throws EvaluationException { - return (long) lhs.getValue() >> (long) rhs.getValue(); - } - - - public static double ass(LValue lhs, RValue rhs) throws EvaluationException { - return lhs.assign(rhs.getValue()); - } - - public static double aadd(LValue lhs, RValue rhs) throws EvaluationException { - return lhs.assign(lhs.getValue() + rhs.getValue()); - } - - public static double asub(LValue lhs, RValue rhs) throws EvaluationException { - return lhs.assign(lhs.getValue() - rhs.getValue()); - } - - public static double amul(LValue lhs, RValue rhs) throws EvaluationException { - return lhs.assign(lhs.getValue() * rhs.getValue()); - } - - public static double adiv(LValue lhs, RValue rhs) throws EvaluationException { - return lhs.assign(lhs.getValue() / rhs.getValue()); - } - - public static double amod(LValue lhs, RValue rhs) throws EvaluationException { - return lhs.assign(lhs.getValue() % rhs.getValue()); - } - - public static double aexp(LValue lhs, RValue rhs) throws EvaluationException { - return lhs.assign(Math.pow(lhs.getValue(), rhs.getValue())); - } - - - public static double inc(LValue x) throws EvaluationException { - return x.assign(x.getValue() + 1); - } - - public static double dec(LValue x) throws EvaluationException { - return x.assign(x.getValue() - 1); - } - - public static double postinc(LValue x) throws EvaluationException { - final double oldValue = x.getValue(); - x.assign(oldValue + 1); - return oldValue; - } - - public static double postdec(LValue x) throws EvaluationException { - final double oldValue = x.getValue(); - x.assign(oldValue - 1); - return oldValue; - } - - - private static final double[] factorials = new double[171]; - static { - double accum = 1; - factorials[0] = 1; - for (int i = 1; i < factorials.length; ++i) { - factorials[i] = accum *= i; - } - } - - public static double fac(RValue x) throws EvaluationException { - final int n = (int) x.getValue(); - - if (n < 0) { - return 0; - } - - if (n >= factorials.length) { - return Double.POSITIVE_INFINITY; - } - - return factorials[n]; - } - - // 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; - - final long longDiff = Math.abs(aLong - bLong); - return longDiff <= maxUlps; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/RValue.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/RValue.java deleted file mode 100644 index 45a654bcd..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/RValue.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.Identifiable; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * A value that can be used on the right side of an assignment. - */ -public interface RValue extends Identifiable { - - double getValue() throws EvaluationException; - - RValue optimize() throws EvaluationException; - - RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException; - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Return.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Return.java deleted file mode 100644 index 455a09a3b..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Return.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * A return statement. - */ -public class Return extends Node { - - RValue value; - - public Return(int position, RValue value) { - super(position); - - this.value = value; - } - - @Override - public double getValue() throws EvaluationException { - throw new ReturnException(value.getValue()); - } - - @Override - public char id() { - return 'r'; - } - - @Override - public String toString() { - return "return " + value; - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - value = value.bindVariables(expression, false); - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ReturnException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ReturnException.java deleted file mode 100644 index 84b60021b..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ReturnException.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -/** - * Thrown when a return statement is encountered. - * {@link com.sk89q.worldedit.internal.expression.Expression#evaluate} - * catches this exception and returns the enclosed value. - */ -public class ReturnException extends EvaluationException { - - final double value; - - public ReturnException(double value) { - super(-1); - - this.value = value; - } - - public double getValue() { - return value; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Sequence.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Sequence.java deleted file mode 100644 index f6458acce..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Sequence.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * A sequence of operations, usually separated by semicolons in the - * input stream. - */ -public class Sequence extends Node { - - final RValue[] sequence; - - public Sequence(int position, RValue... sequence) { - super(position); - - this.sequence = sequence; - } - - @Override - public char id() { - return 's'; - } - - @Override - public double getValue() throws EvaluationException { - double ret = 0; - for (RValue invokable : sequence) { - ret = invokable.getValue(); - } - return ret; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("seq("); - boolean first = true; - for (RValue invokable : sequence) { - if (!first) { - sb.append(", "); - } - sb.append(invokable); - first = false; - } - - return sb.append(')').toString(); - } - - @Override - public RValue optimize() throws EvaluationException { - final List newSequence = new ArrayList<>(); - - RValue droppedLast = null; - for (RValue invokable : sequence) { - droppedLast = null; - invokable = invokable.optimize(); - if (invokable instanceof Sequence) { - Collections.addAll(newSequence, ((Sequence) invokable).sequence); - } else if (invokable instanceof Constant) { - droppedLast = invokable; - } else { - newSequence.add(invokable); - } - } - - if (droppedLast != null) { - newSequence.add(droppedLast); - } - - if (newSequence.size() == 1) { - return newSequence.get(0); - } - - return new Sequence(getPosition(), newSequence.toArray(new RValue[newSequence.size()])); - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - for (int i = 0; i < sequence.length; ++i) { - sequence[i] = sequence[i].bindVariables(expression, false); - } - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/SimpleFor.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/SimpleFor.java deleted file mode 100644 index 1576c3484..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/SimpleFor.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * A simple-style for loop. - */ -public class SimpleFor extends Node { - - LValue counter; - RValue first; - RValue last; - RValue body; - - public SimpleFor(int position, LValue counter, RValue first, RValue last, RValue body) { - super(position); - - this.counter = counter; - this.first = first; - this.last = last; - this.body = body; - } - - @Override - public double getValue() throws EvaluationException { - int iterations = 0; - double ret = 0.0; - - double firstValue = first.getValue(); - double lastValue = last.getValue(); - - for (double i = firstValue; i <= lastValue; ++i) { - if (iterations > 256) { - throw new EvaluationException(getPosition(), "Loop exceeded 256 iterations."); - } - if (Thread.interrupted()) { - throw new EvaluationException(getPosition(), "Calculations exceeded time limit."); - } - ++iterations; - - try { - counter.assign(i); - ret = body.getValue(); - } catch (BreakException e) { - if (e.doContinue) { - //noinspection UnnecessaryContinue - continue; - } else { - break; - } - } - } - - return ret; - } - - @Override - public char id() { - return 'S'; - } - - @Override - public String toString() { - return "for (" + counter + " = " + first + ", " + last + ") { " + body + " }"; - } - - @Override - public RValue optimize() throws EvaluationException { - // TODO: unroll small loops into Sequences - - return new SimpleFor(getPosition(), counter.optimize(), first.optimize(), last.optimize(), body.optimize()); - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - counter = counter.bindVariables(expression, true); - first = first.bindVariables(expression, false); - last = last.bindVariables(expression, false); - body = body.bindVariables(expression, false); - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Switch.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Switch.java deleted file mode 100644 index 9f2f35764..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Switch.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -/** - * A switch/case construct. - */ -public class Switch extends Node implements RValue { - - private RValue parameter; - private final Map valueMap; - private final RValue[] caseStatements; - private RValue defaultCase; - - public Switch(int position, RValue parameter, List values, List caseStatements, RValue defaultCase) { - this(position, parameter, invertList(values), caseStatements, defaultCase); - - } - - private static Map invertList(List values) { - Map valueMap = new HashMap<>(); - for (int i = 0; i < values.size(); ++i) { - valueMap.put(values.get(i), i); - } - return valueMap; - } - - private Switch(int position, RValue parameter, Map valueMap, List caseStatements, RValue defaultCase) { - super(position); - - this.parameter = parameter; - this.valueMap = valueMap; - this.caseStatements = caseStatements.toArray(new RValue[caseStatements.size()]); - this.defaultCase = defaultCase; - } - - @Override - public char id() { - return 'W'; - } - - @Override - public double getValue() throws EvaluationException { - final double parameter = this.parameter.getValue(); - - try { - double ret = 0.0; - - final Integer index = valueMap.get(parameter); - if (index != null) { - for (int i = index; i < caseStatements.length; ++i) { - ret = caseStatements[i].getValue(); - } - } - - return defaultCase == null ? ret : defaultCase.getValue(); - } catch (BreakException e) { - if (e.doContinue) throw e; - - return 0.0; - } - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder(); - - sb.append("switch ("); - sb.append(parameter); - sb.append(") { "); - - for (int i = 0; i < caseStatements.length; ++i) { - RValue caseStatement = caseStatements[i]; - sb.append("case "); - for (Entry entry : valueMap.entrySet()) { - if (entry.getValue() == i) { - sb.append(entry.getKey()); - break; - } - } - sb.append(": "); - sb.append(caseStatement); - sb.append(' '); - } - - if (defaultCase != null) { - sb.append("default: "); - sb.append(defaultCase); - sb.append(' '); - } - - sb.append("}"); - - return sb.toString(); - } - - @Override - public RValue optimize() throws EvaluationException { - final RValue optimizedParameter = parameter.optimize(); - final List newSequence = new ArrayList<>(); - - if (optimizedParameter instanceof Constant) { - final double parameter = optimizedParameter.getValue(); - - final Integer index = valueMap.get(parameter); - if (index == null) { - return defaultCase == null ? new Constant(getPosition(), 0.0) : defaultCase.optimize(); - } - - boolean breakDetected = false; - for (int i = index; i < caseStatements.length && !breakDetected; ++i) { - final RValue invokable = caseStatements[i].optimize(); - - if (invokable instanceof Sequence) { - for (RValue subInvokable : ((Sequence) invokable).sequence) { - if (subInvokable instanceof Break) { - breakDetected = true; - break; - } - - newSequence.add(subInvokable); - } - } else { - newSequence.add(invokable); - } - } - - if (defaultCase != null && !breakDetected) { - final RValue invokable = defaultCase.optimize(); - - if (invokable instanceof Sequence) { - Collections.addAll(newSequence, ((Sequence) invokable).sequence); - } else { - newSequence.add(invokable); - } - } - - return new Switch(getPosition(), optimizedParameter, Collections.singletonMap(parameter, 0), newSequence, null); - } - - final Map newValueMap = new HashMap<>(); - - Map backMap = new HashMap<>(); - for (Entry entry : valueMap.entrySet()) { - backMap.put(entry.getValue(), entry.getKey()); - } - - for (int i = 0; i < caseStatements.length; ++i) { - final RValue invokable = caseStatements[i].optimize(); - - final Double caseValue = backMap.get(i); - if (caseValue != null) { - newValueMap.put(caseValue, newSequence.size()); - } - - if (invokable instanceof Sequence) { - Collections.addAll(newSequence, ((Sequence) invokable).sequence); - } else { - newSequence.add(invokable); - } - } - - return new Switch(getPosition(), optimizedParameter, newValueMap, newSequence, defaultCase.optimize()); - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - parameter = parameter.bindVariables(expression, false); - - for (int i = 0; i < caseStatements.length; ++i) { - caseStatements[i] = caseStatements[i].bindVariables(expression, false); - } - - defaultCase = defaultCase.bindVariables(expression, false); - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Variable.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Variable.java deleted file mode 100644 index 01fe6764f..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/Variable.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * A variable. - */ -public final class Variable extends Node implements LValue { - - public double value; - - public Variable(double value) { - super(-1); - this.value = value; - } - - @Override - public double getValue() { - return value; - } - - @Override - public String toString() { - return "var"; - } - - @Override - public char id() { - return 'v'; - } - - @Override - public double assign(double value) { - return this.value = value; - } - - @Override - public LValue optimize() throws EvaluationException { - return this; - } - - @Override - public LValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/While.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/While.java deleted file mode 100644 index 5da3dae01..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/While.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program 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 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 Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression.runtime; - -import com.sk89q.worldedit.internal.expression.Expression; -import com.sk89q.worldedit.internal.expression.parser.ParserException; - -/** - * A while loop. - */ -public class While extends Node { - - RValue condition; - RValue body; - boolean footChecked; - - public While(int position, RValue condition, RValue body, boolean footChecked) { - super(position); - - this.condition = condition; - this.body = body; - this.footChecked = footChecked; - } - - @Override - public double getValue() throws EvaluationException { - int iterations = 0; - double ret = 0.0; - - if (footChecked) { - do { - if (iterations > 256) { - throw new EvaluationException(getPosition(), "Loop exceeded 256 iterations."); - } - if (Thread.interrupted()) { - throw new EvaluationException(getPosition(), "Calculations exceeded time limit."); - } - ++iterations; - - try { - ret = body.getValue(); - } catch (BreakException e) { - if (e.doContinue) { - continue; - } else { - break; - } - } - } while (condition.getValue() > 0.0); - } else { - while (condition.getValue() > 0.0) { - if (iterations > 256) { - throw new EvaluationException(getPosition(), "Loop exceeded 256 iterations."); - } - if (Thread.interrupted()) { - throw new EvaluationException(getPosition(), "Calculations exceeded time limit."); - } - ++iterations; - - try { - ret = body.getValue(); - } catch (BreakException e) { - if (e.doContinue) { - //noinspection UnnecessaryContinue - continue; - } else { - break; - } - } - } - } - - return ret; - } - - @Override - public char id() { - return 'w'; - } - - @Override - public String toString() { - if (footChecked) { - return "do { " + body + " } while (" + condition + ")"; - } else { - return "while (" + condition + ") { " + body + " }"; - } - } - - @Override - public RValue optimize() throws EvaluationException { - final RValue newCondition = condition.optimize(); - - if (newCondition instanceof Constant && newCondition.getValue() <= 0) { - // If the condition is always false, the loop can be flattened. - if (footChecked) { - // Foot-checked loops run at least once. - return body.optimize(); - } else { - // Loops that never run always return 0.0. - return new Constant(getPosition(), 0.0); - } - } - - return new While(getPosition(), newCondition, body.optimize(), footChecked); - } - - @Override - public RValue bindVariables(Expression expression, boolean preferLValue) throws ParserException { - condition = condition.bindVariables(expression, false); - body = body.bindVariables(expression, false); - - return this; - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/shape/WorldEditExpressionEnvironment.java b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/shape/WorldEditExpressionEnvironment.java index 2304e4876..d4719fc52 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/regions/shape/WorldEditExpressionEnvironment.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/regions/shape/WorldEditExpressionEnvironment.java @@ -20,7 +20,7 @@ package com.sk89q.worldedit.regions.shape; import com.sk89q.worldedit.extent.Extent; -import com.sk89q.worldedit.internal.expression.runtime.ExpressionEnvironment; +import com.sk89q.worldedit.internal.expression.ExpressionEnvironment; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java index de9cce3cc..c33112f05 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java @@ -22,20 +22,17 @@ package com.sk89q.worldedit.internal.expression; import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.extension.platform.Platform; -import com.sk89q.worldedit.internal.expression.lexer.LexerException; -import com.sk89q.worldedit.internal.expression.parser.ParserException; -import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; -import com.sk89q.worldedit.internal.expression.runtime.ExpressionEnvironment; -import com.sk89q.worldedit.internal.expression.runtime.ExpressionTimeoutException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.Duration; + import static java.lang.Math.atan2; import static java.lang.Math.sin; -import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -79,71 +76,71 @@ public class ExpressionTest { @Test public void testErrors() { - assertAll( - // test lexer errors - () -> { - LexerException e = assertThrows(LexerException.class, - () -> compile("#")); - assertEquals(0, e.getPosition(), "Error position"); - }, - // test parser errors - () -> { - ParserException e = assertThrows(ParserException.class, - () -> compile("x")); - assertEquals(0, e.getPosition(), "Error position"); - }, - () -> { - ParserException e = assertThrows(ParserException.class, - () -> compile("x()")); - assertEquals(0, e.getPosition(), "Error position"); - }, - () -> assertThrows(ParserException.class, - () -> compile("(")), - () -> assertThrows(ParserException.class, - () -> compile("x(")), - // test overloader errors - () -> { - ParserException e = assertThrows(ParserException.class, - () -> compile("atan2(1)")); - assertEquals(0, e.getPosition(), "Error position"); - }, - () -> { - ParserException e = assertThrows(ParserException.class, - () -> compile("atan2(1, 2, 3)")); - assertEquals(0, e.getPosition(), "Error position"); - }, - () -> { - ParserException e = assertThrows(ParserException.class, - () -> compile("rotate(1, 2, 3)")); - assertEquals(0, e.getPosition(), "Error position"); - } - ); + // test lexer errors + { + ExpressionException e = assertThrows(ExpressionException.class, + () -> compile("#")); + assertEquals(0, e.getPosition(), "Error position"); + } + // test parser errors + { + ExpressionException e = assertThrows(ExpressionException.class, + () -> compile("x")); + assertEquals(0, e.getPosition(), "Error position"); + } + { + ExpressionException e = assertThrows(ExpressionException.class, + () -> compile("x()")); + assertEquals(0, e.getPosition(), "Error position"); + } + assertThrows(ExpressionException.class, + () -> compile("(")); + assertThrows(ExpressionException.class, + () -> compile("x(")); + // test overloader errors + { + ExpressionException e = assertThrows(ExpressionException.class, + () -> compile("atan2(1)")); + assertEquals(0, e.getPosition(), "Error position"); + } + { + ExpressionException e = assertThrows(ExpressionException.class, + () -> compile("atan2(1, 2, 3)")); + assertEquals(0, e.getPosition(), "Error position"); + } + { + ExpressionException e = assertThrows(ExpressionException.class, + () -> compile("rotate(1, 2, 3)")); + e.printStackTrace(); + assertEquals(7, e.getPosition(), "Error position"); + } + } @Test public void testAssign() throws ExpressionException { Expression foo = compile("{a=x} b=y; c=z", "x", "y", "z", "a", "b", "c"); foo.evaluate(2D, 3D, 5D); - assertEquals(2, foo.getVariable("a", false).getValue(), 0); - assertEquals(3, foo.getVariable("b", false).getValue(), 0); - assertEquals(5, foo.getVariable("c", false).getValue(), 0); + assertEquals(2, foo.getSlots().getSlotValue("a").orElse(-1), 0); + assertEquals(3, foo.getSlots().getSlotValue("b").orElse(-1), 0); + assertEquals(5, foo.getSlots().getSlotValue("c").orElse(-1), 0); } @Test public void testIf() throws ExpressionException { - assertEquals(40, simpleEval("if (1) x=4; else y=5; x*10+y;"), 0); - assertEquals(5, simpleEval("if (0) x=4; else y=5; x*10+y;"), 0); + assertEquals(40, simpleEval("y=0; if (1) x=4; else y=5; x*10+y;"), 0); + assertEquals(5, simpleEval("x=0; if (0) x=4; else y=5; x*10+y;"), 0); // test 'dangling else' final Expression expression1 = compile("if (1) if (0) x=4; else y=5;", "x", "y"); expression1.evaluate(1D, 2D); - assertEquals(1, expression1.getVariable("x", false).getValue(), 0); - assertEquals(5, expression1.getVariable("y", false).getValue(), 0); + assertEquals(1, expression1.getSlots().getSlotValue("x").orElse(-1), 0); + assertEquals(5, expression1.getSlots().getSlotValue("y").orElse(-1), 0); // test if the if construct is correctly recognized as a statement final Expression expression2 = compile("if (0) if (1) x=5; y=4;", "x", "y"); expression2.evaluate(1D, 2D); - assertEquals(4, expression2.getVariable("y", false).getValue(), 0); + assertEquals(4, expression2.getSlots().getSlotValue("y").orElse(-1), 0); } @Test @@ -182,9 +179,11 @@ public class ExpressionTest { @Test public void testTimeout() { - ExpressionTimeoutException e = assertThrows(ExpressionTimeoutException.class, - () -> simpleEval("for(i=0;i<256;i++){for(j=0;j<256;j++){for(k=0;k<256;k++){for(l=0;l<256;l++){ln(pi)}}}}"), - "Loop was not stopped."); + ExpressionTimeoutException e = assertTimeoutPreemptively(Duration.ofSeconds(10), () -> + assertThrows(ExpressionTimeoutException.class, + () -> simpleEval("for(i=0;i<256;i++){for(j=0;j<256;j++){for(k=0;k<256;k++){for(l=0;l<256;l++){ln(pi)}}}}"), + "Loop was not stopped.") + ); assertTrue(e.getMessage().contains("Calculations exceeded time limit")); } @@ -226,7 +225,7 @@ public class ExpressionTest { return expression.evaluate(); } - private Expression compile(String expressionString, String... variableNames) throws ExpressionException, EvaluationException { + private Expression compile(String expressionString, String... variableNames) throws ExpressionException { final Expression expression = Expression.compile(expressionString, variableNames); expression.optimize(); return expression;