diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/BreakException.java similarity index 92% rename from worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/BreakException.java index 5b2ae6b24..268f047eb 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/BreakException.java @@ -17,13 +17,13 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.internal.expression; +package com.sk89q.worldedit.internal.expression.invoke; /** * Thrown when a break or continue is encountered. * Loop constructs catch this exception. */ -public class BreakException extends RuntimeException { +class BreakException extends RuntimeException { public static final BreakException BREAK = new BreakException(false); public static final BreakException CONTINUE = new BreakException(true); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/CompilingVisitor.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/CompilingVisitor.java index 2bf2f5b33..ae2b153ba 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/CompilingVisitor.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/CompilingVisitor.java @@ -21,7 +21,6 @@ package com.sk89q.worldedit.internal.expression.invoke; import com.sk89q.worldedit.antlr.ExpressionBaseVisitor; import com.sk89q.worldedit.antlr.ExpressionParser; -import com.sk89q.worldedit.internal.expression.BreakException; import com.sk89q.worldedit.internal.expression.EvaluationException; import com.sk89q.worldedit.internal.expression.ExecutionData; import com.sk89q.worldedit.internal.expression.ExpressionHelper; @@ -217,12 +216,31 @@ class CompilingVisitor extends ExpressionBaseVisitor { return CONTINUE_STATEMENT; } + // MH = (Double)Double + // takes the double to return, conveniently has Double return type + private static final MethodHandle RETURN_STATEMENT_BASE = MethodHandles.filterReturnValue( + // take the (Double)ReturnException constructor + ExpressionHandles.NEW_RETURN_EXCEPTION, + // and map the return type to Double by throwing it + MethodHandles.throwException(Double.class, ReturnException.class) + ); + @Override public MethodHandle visitReturnStatement(ExpressionParser.ReturnStatementContext ctx) { + // MH:returnValue = (ExecutionData)Double + MethodHandle returnValue; if (ctx.value != null) { - return evaluate(ctx.value).handle; + returnValue = evaluate(ctx.value).handle; + } else { + returnValue = defaultResult(); } - return defaultResult(); + return MethodHandles.filterArguments( + // take the (Double)Double return statement + RETURN_STATEMENT_BASE, + 0, + // map the Double back to ExecutionData via the returnValue + returnValue + ); } @Override @@ -450,7 +468,7 @@ class CompilingVisitor extends ExpressionBaseVisitor { case NOT_EQUAL: return (l, r) -> ExpressionHandles.boolToDouble(l != r); case NEAR: - return (l, r) -> ExpressionHandles.boolToDouble(almostEqual2sComplement(l, r, 450359963L)); + return (l, r) -> ExpressionHandles.boolToDouble(almostEqual2sComplement(l, r)); case GREATER_THAN_OR_EQUAL: return (l, r) -> ExpressionHandles.boolToDouble(l >= r); } @@ -459,7 +477,7 @@ class CompilingVisitor extends ExpressionBaseVisitor { } // Usable AlmostEqual function, based on http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm - private static boolean almostEqual2sComplement(double a, double b, long maxUlps) { + private static boolean almostEqual2sComplement(double a, double b) { // 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 @@ -473,7 +491,7 @@ class CompilingVisitor extends ExpressionBaseVisitor { if (bLong < 0) bLong = 0x8000000000000000L - bLong; final long longDiff = Math.abs(aLong - bLong); - return longDiff <= maxUlps; + return longDiff <= 450359963L; } @Override @@ -645,11 +663,7 @@ class CompilingVisitor extends ExpressionBaseVisitor { checkHandle(childResult, (ParserRuleContext) c); } - boolean returning = c instanceof ExpressionParser.ReturnStatementContext; - result = aggregateResult(result, childResult, returning); - if (returning) { - return result; - } + result = aggregateHandleResult(result, childResult); } return result; @@ -660,25 +674,26 @@ class CompilingVisitor extends ExpressionBaseVisitor { throw new UnsupportedOperationException(); } - private MethodHandle aggregateResult(MethodHandle oldResult, MethodHandle result, - boolean keepDefault) { + private MethodHandle aggregateHandleResult(MethodHandle oldResult, MethodHandle result) { + // MH:oldResult,result = (ExecutionData)Double + // Execute `oldResult` but ignore its return value, then execute result and return that. // If `oldResult` (the old value) is `defaultResult`, it's bogus, so just skip it if (oldResult == DEFAULT_RESULT) { return result; } - if (result == DEFAULT_RESULT && !keepDefault) { - return oldResult; - } // Add a dummy Double parameter to the end + // MH:dummyDouble = (ExecutionData, Double)Double MethodHandle dummyDouble = MethodHandles.dropArguments( result, 1, Double.class ); // Have oldResult turn it from data->Double + // MH:doubledData = (ExecutionData, ExecutionData)Double MethodHandle doubledData = MethodHandles.collectArguments( dummyDouble, 1, oldResult ); // Deduplicate the `data` parameter + // MH:@return = (ExecutionData)Double return ExpressionHandles.dedupData(doubledData); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionCompiler.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionCompiler.java index 7d2fb6799..3f3ffd8fe 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionCompiler.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionCompiler.java @@ -29,8 +29,6 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import static com.sk89q.worldedit.internal.expression.invoke.ExpressionHandles.COMPILED_EXPRESSION_SIG; -import static com.sk89q.worldedit.internal.expression.invoke.ExpressionHandles.safeInvoke; import static java.lang.invoke.MethodType.methodType; /** @@ -45,7 +43,7 @@ public class ExpressionCompiler { private static final MethodHandle HANDLE_TO_CE_CONVERTER; static { - MethodHandle handleInvoker = MethodHandles.invoker(COMPILED_EXPRESSION_SIG); + MethodHandle handleInvoker = MethodHandles.invoker(ExpressionHandles.COMPILED_EXPRESSION_SIG); try { HANDLE_TO_CE_CONVERTER = LambdaMetafactory.metafactory( MethodHandles.lookup(), @@ -54,11 +52,11 @@ public class ExpressionCompiler { // Take a handle, to be converted to CompiledExpression HANDLE_TO_CE, // Raw signature for SAM type - COMPILED_EXPRESSION_SIG, + ExpressionHandles.COMPILED_EXPRESSION_SIG, // Handle to call the captured handle. handleInvoker, // Actual signature at invoke time - COMPILED_EXPRESSION_SIG + ExpressionHandles.COMPILED_EXPRESSION_SIG ).dynamicInvoker().asType(HANDLE_TO_CE); } catch (LambdaConversionException e) { throw new IllegalStateException("Failed to load ExpressionCompiler MetaFactory", e); @@ -68,8 +66,15 @@ public class ExpressionCompiler { public CompiledExpression compileExpression(ExpressionParser.AllStatementsContext root, Functions functions) { MethodHandle invokable = root.accept(new CompilingVisitor(functions)); - return (CompiledExpression) safeInvoke( - HANDLE_TO_CE_CONVERTER, h -> h.invoke(invokable) + // catch ReturnExpression and substitute its result + invokable = MethodHandles.catchException( + invokable, + ReturnException.class, + ExpressionHandles.RETURN_EXCEPTION_GET_RESULT + ); + MethodHandle finalInvokable = invokable; + return (CompiledExpression) ExpressionHandles.safeInvoke( + HANDLE_TO_CE_CONVERTER, h -> h.invoke(finalInvokable) ); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionHandles.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionHandles.java index 0d118d6be..372ef1645 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionHandles.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionHandles.java @@ -20,7 +20,6 @@ package com.sk89q.worldedit.internal.expression.invoke; import com.google.common.base.Throwables; -import com.sk89q.worldedit.internal.expression.BreakException; import com.sk89q.worldedit.internal.expression.CompiledExpression; import com.sk89q.worldedit.internal.expression.EvaluationException; import com.sk89q.worldedit.internal.expression.ExecutionData; @@ -70,6 +69,10 @@ class ExpressionHandles { // (double, double)Double; static final MethodHandle CALL_BINARY_OP; static final MethodHandle NEW_LS_CONSTANT; + // (Double)ReturnException; + static final MethodHandle NEW_RETURN_EXCEPTION; + // (ReturnException)Double; + static final MethodHandle RETURN_EXCEPTION_GET_RESULT; static final MethodHandle NULL_DOUBLE = dropData(constant(Double.class, null)); @@ -105,6 +108,10 @@ class ExpressionHandles { .asType(methodType(Double.class, DoubleBinaryOperator.class, double.class, double.class)); NEW_LS_CONSTANT = lookup.findConstructor(LocalSlot.Constant.class, methodType(void.class, double.class)); + NEW_RETURN_EXCEPTION = lookup.findConstructor(ReturnException.class, + methodType(void.class, Double.class)); + RETURN_EXCEPTION_GET_RESULT = lookup.findVirtual(ReturnException.class, + "getResult", methodType(Double.class)); } catch (NoSuchMethodException | IllegalAccessException e) { throw new IllegalStateException(e); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ReturnException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ReturnException.java new file mode 100644 index 000000000..bd4a757fb --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ReturnException.java @@ -0,0 +1,39 @@ +/* + * 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.invoke; + +/** + * Thrown when a return is encountered, to pop the stack frames and return the value easily. + * + * Should be caught by the executor. + */ +class ReturnException extends RuntimeException { + + private final Double result; + + public ReturnException(Double result) { + super("return " + result, null, true, false); + this.result = result; + } + + public Double getResult() { + return result; + } +} 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 2cea279ce..3d870a9fc 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 @@ -19,10 +19,17 @@ package com.sk89q.worldedit.internal.expression; +import com.google.common.collect.ImmutableList; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; import java.time.Duration; +import java.util.List; +import java.util.stream.Stream; +import static com.sk89q.worldedit.internal.expression.ExpressionTestCase.testCase; import static java.lang.Math.atan2; import static java.lang.Math.sin; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -31,28 +38,45 @@ import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; class ExpressionTest extends BaseExpressionTest { + + @TestFactory + public Stream testEvaluate() throws ExpressionException { + List testCases = ImmutableList.of( + // basic arithmetic + testCase("1 - 2 + 3", 2), + // unary ops + testCase("2 + +4", 6), + testCase("2 - -4", 6), + testCase("2 * -4", -8), + // check functions + testCase("sin(5)", sin(5)), + testCase("atan2(3, 4)", atan2(3, 4)), + // check conditionals + testCase("0 || 5", 5), + testCase("2 || 5", 2), + testCase("2 && 5", 5), + testCase("5 && 0", 0), + // check ternaries + testCase("false ? 1 : 2", 2), + testCase("true ? 1 : 2", 1), + // - ternary binds inside + testCase("true ? true ? 1 : 2 : 3", 1), + testCase("true ? false ? 1 : 2 : 3", 2), + testCase("false ? true ? 1 : 2 : 3", 3), + testCase("false ? false ? 1 : 2 : 3", 3), + // check return + testCase("return 1; 0", 1) + ); + return testCases.stream() + .map(testCase -> DynamicTest.dynamicTest( + testCase.getExpression(), + () -> assertEquals(testCase.getResult(), simpleEval(testCase.getExpression()), 0) + )); + } + @Test - public void testEvaluate() throws ExpressionException { - // check - assertEquals(1 - 2 + 3, simpleEval("1 - 2 + 3"), 0); - - // check unary ops - assertEquals(2 + +4, simpleEval("2 + +4"), 0); - assertEquals(2 - -4, simpleEval("2 - -4"), 0); - assertEquals(2 * -4, simpleEval("2 * -4"), 0); - - // check functions - assertEquals(sin(5), simpleEval("sin(5)"), 0); - assertEquals(atan2(3, 4), simpleEval("atan2(3, 4)"), 0); - - // check variables + void testVariables() { assertEquals(8, compile("foo+bar", "foo", "bar").evaluate(5D, 3D), 0); - - // check conditionals - assertEquals(5, simpleEval("0 || 5"), 0); - assertEquals(2, simpleEval("2 || 5"), 0); - assertEquals(5, simpleEval("2 && 5"), 0); - assertEquals(0, simpleEval("5 && 0"), 0); } @Test