diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index 36fd19f67bb..ed7eac4cde9 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -5100,14 +5100,18 @@ private static class QuantifyCollectionImplementor extends AbstractRexCallImplem final RexCall binaryImplementorRexCall = (RexCall) translator.builder.makeCall(call.getParserPosition(), binaryOperator, leftRex, translator.builder.makeDynamicParam(rightComponentType, 0)); + // The comparison is evaluated inside the lambda, and it reads the lambda + // parameter, so its statements must go into the lambda's block + final RexToLixTranslator lambdaTranslator = translator.setBlock(lambdaBuilder); final List binaryImplementorArgs = ImmutableList.of( new RexToLixTranslator.Result( - genIsNullStatement(translator, leftExpr), leftExpr), + genIsNullStatement(lambdaTranslator, leftExpr), leftExpr), new RexToLixTranslator.Result( - genIsNullStatement(translator, lambdaArg), lambdaArg)); + genIsNullStatement(lambdaTranslator, lambdaArg), lambdaArg)); final RexToLixTranslator.Result condition = - binaryImplementor.implement(translator, binaryImplementorRexCall, binaryImplementorArgs); + binaryImplementor.implement(lambdaTranslator, binaryImplementorRexCall, + binaryImplementorArgs); lambdaBuilder.add(Expressions.return_(null, condition.valueVariable)); final FunctionExpression predicate = Expressions.lambda(lambdaBuilder.toBlock(), lambdaArg); diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/BlockBuilder.java b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/BlockBuilder.java index febc454d271..28aab938a10 100644 --- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/BlockBuilder.java +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/BlockBuilder.java @@ -258,6 +258,8 @@ protected boolean isSimpleExpression(@Nullable Expression expr) { if (expr instanceof UnaryExpression) { UnaryExpression una = (UnaryExpression) expr; return una.getNodeType() == ExpressionType.Convert + // A cast may raise ClassCastException, or unbox a null + && !Expressions.mayThrow(una) && isSimpleExpression(una.expression); } return false; @@ -408,6 +410,14 @@ private boolean optimize(Shuttle optimizer, boolean performInline) { // anonymous classes. count = Integer.MAX_VALUE; } + if (count == 0 + && statement.initializer != null + && Expressions.mayThrow(statement.initializer)) { + // Never read, but computing the value may raise a runtime error that + // the program is expected to raise. Keep the declaration, and treat + // it like any other statement that cannot be inlined. + count = 100; + } Expression normalized = normalizeDeclaration(statement); expressionForReuse.remove(normalized); switch (count) { diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Expressions.java b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Expressions.java index ebebd2ca692..5ba55093b65 100644 --- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Expressions.java +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Expressions.java @@ -32,6 +32,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; @@ -515,6 +516,20 @@ public static boolean isConstantNull(Expression e) { && ((ConstantExpression) e).value == null; } + /** Returns whether evaluating a node may cause a runtime error, for example + * a division by zero or an arithmetic overflow. + * + *

An optimization must not discard a node that may throw, even when its + * value is unused: the error is part of the meaning of the program. It is + * still free to discard a node that Java would not have evaluated anyway, + * such as the untaken branch of {@code true ? x : y}. + */ + public static boolean mayThrow(Node node) { + final MayThrowVisitor visitor = new MayThrowVisitor(); + node.accept(visitor); + return visitor.mayThrow; + } + /** * Creates a ConditionalExpression that represents a conditional * statement. @@ -3290,6 +3305,235 @@ public interface FluentList extends List { FluentList appendAll(T... ts); } + /** Visitor that detects whether a node may cause a runtime error. + * + *

The analysis is conservative: a false positive is a missed + * simplification, but a false negative is a lost runtime error. + * + *

These nodes may throw: + * + *

+ * + *

Everything else - reading a variable or a static field, comparing two + * references, {@code instanceof}, string concatenation, and Java arithmetic + * that wraps around - is assumed not to throw. An unrecognized node is unsafe. + * + * @see #mayThrow(Node) */ + private static class MayThrowVisitor extends VisitorImpl<@Nullable Void> { + boolean mayThrow = false; + + @Override public @Nullable Void visit(MethodCallExpression call) { + mayThrow = true; + return super.visit(call); + } + + @Override public @Nullable Void visit(InvocationExpression invocation) { + mayThrow = true; + return super.visit(invocation); + } + + @Override public @Nullable Void visit(DynamicExpression dynamic) { + mayThrow = true; + return super.visit(dynamic); + } + + @Override public @Nullable Void visit(NewExpression newExpression) { + mayThrow = true; + return super.visit(newExpression); + } + + @Override public @Nullable Void visit(NewArrayExpression newArray) { + mayThrow = true; + return super.visit(newArray); + } + + @Override public @Nullable Void visit(ListInitExpression listInit) { + mayThrow = true; + return super.visit(listInit); + } + + @Override public @Nullable Void visit(MemberInitExpression memberInit) { + mayThrow = true; + return super.visit(memberInit); + } + + @Override public @Nullable Void visit(IndexExpression indexExpression) { + mayThrow = true; + return super.visit(indexExpression); + } + + @Override public @Nullable Void visit(MemberExpression member) { + if (!Modifier.isStatic(member.field.getModifiers())) { + mayThrow = true; + } + return super.visit(member); + } + + @Override public @Nullable Void visit(ThrowStatement throwStatement) { + mayThrow = true; + return super.visit(throwStatement); + } + + @Override public @Nullable Void visit(TryStatement tryStatement) { + mayThrow = true; + return super.visit(tryStatement); + } + + @Override public @Nullable Void visit(BinaryExpression binary) { + final Type left = binary.expression0.getType(); + final Type right = binary.expression1.getType(); + switch (binary.getNodeType()) { + case Assign: + case Coalesce: + break; + case Equal: + case NotEqual: + // Comparing a primitive with a reference unboxes the reference; + // comparing two references compares them by identity. + if (Primitive.is(left) != Primitive.is(right)) { + mayThrow = true; + } + break; + case Add: + // "+" is concatenation, not addition, if either operand is a String + if (left == String.class || right == String.class) { + break; + } + // fall through + case AddAssign: + case And: + case AndAlso: + case AndAssign: + case ExclusiveOr: + case ExclusiveOrAssign: + case GreaterThan: + case GreaterThanOrEqual: + case LeftShift: + case LeftShiftAssign: + case LessThan: + case LessThanOrEqual: + case Multiply: + case MultiplyAssign: + case Or: + case OrAssign: + case OrElse: + case Power: + case PowerAssign: + case RightShift: + case RightShiftAssign: + case Subtract: + case SubtractAssign: + // The operator itself cannot fail, but it may unbox an operand + if (!Primitive.is(left) || !Primitive.is(right)) { + mayThrow = true; + } + break; + case Divide: + case DivideAssign: + case DivideChecked: + case Mod: + case Modulo: + case ModuloAssign: + // May divide by zero + mayThrow = true; + break; + case AddAssignChecked: + case AddChecked: + case MultiplyAssignChecked: + case MultiplyChecked: + case SubtractAssignChecked: + case SubtractChecked: + // May overflow + mayThrow = true; + break; + default: + // A node type that no one has classified yet + mayThrow = true; + break; + } + return super.visit(binary); + } + + @Override public @Nullable Void visit(UnaryExpression unary) { + final Type operand = unary.expression.getType(); + switch (unary.getNodeType()) { + case Quote: + case TypeAs: + break; + case Decrement: + case Increment: + case IsFalse: + case IsTrue: + case Negate: + case Not: + case OnesComplement: + case PostDecrementAssign: + case PostIncrementAssign: + case PreDecrementAssign: + case PreIncrementAssign: + case UnaryPlus: + // The operator itself cannot fail, but it unboxes its operand. + if (!Primitive.is(operand)) { + mayThrow = true; + } + break; + case Convert: + if (!castAlwaysSucceeds(operand, unary.getType())) { + mayThrow = true; + } + break; + case ConvertChecked: + case NegateChecked: + // May overflow + mayThrow = true; + break; + case Unbox: + // Unboxing a null raises NullPointerException + mayThrow = true; + break; + case ArrayLength: + // Reads a field of an array, which may be null + mayThrow = true; + break; + default: + // A node type that no one has classified yet + mayThrow = true; + break; + } + return super.visit(unary); + } + + /** Returns whether a cast from {@code from} to {@code to} is known to + * succeed. A cast whose source is a primitive cannot fail; + * neither can a widening reference conversion, such + * as {@code (Object) s}. Any other cast may raise + * {@link ClassCastException} or, when unboxing, + * {@link NullPointerException}. */ + private static boolean castAlwaysSucceeds(Type from, Type to) { + if (Primitive.is(from)) { + return true; + } + return from instanceof Class + && to instanceof Class + && ((Class) to).isAssignableFrom((Class) from); + } + } + /** Fluent array list. * * @param element type */ diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/OptimizeShuttle.java b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/OptimizeShuttle.java index b6ea7e5faac..a5f04cdc354 100644 --- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/OptimizeShuttle.java +++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/OptimizeShuttle.java @@ -104,7 +104,8 @@ private static void addComplement(ExpressionType eq, ExpressionType ne) { ? expression1 : expression2; } - if (expression1.equals(expression2)) { + if (expression1.equals(expression2) + && !Expressions.mayThrow(expression0)) { // a ? b : b === b return expression1; } @@ -190,7 +191,10 @@ && eq(cmp.expression1, expression2)) { case Equal: case NotEqual: if (eq(expression0, expression1)) { - return binary.getNodeType() == Equal ? TRUE_EXPR : FALSE_EXPR; + // "a == a" discards the evaluation of "a", so it must not throw + if (!Expressions.mayThrow(expression0)) { + return binary.getNodeType() == Equal ? TRUE_EXPR : FALSE_EXPR; + } } else if (expression0 instanceof ConstantExpression && expression1 instanceof ConstantExpression) { ConstantExpression c0 = (ConstantExpression) expression0; @@ -225,11 +229,11 @@ && eq(cmp.expression1, expression2)) { // fall through case AndAlso: case OrElse: - result = visit0(binary, expression0, expression1); + result = visit0(binary, expression0, expression1, false); if (result != null) { return result; } - result = visit0(binary, expression1, expression0); + result = visit0(binary, expression1, expression0, true); if (result != null) { return result; } @@ -240,18 +244,30 @@ && eq(cmp.expression1, expression2)) { return super.visit(binary, expression0, expression1); } + /** Simplifies a binary expression whose {@code expression0} operand may be a + * constant. + * + *

{@code evaluated} says whether Java evaluates {@code expression1} before + * the operator produces its result. It is false when {@code expression1} is + * the right operand of {@code &&} or {@code ||}, which short-circuits; + * discarding a short-circuited operand cannot lose a runtime error. */ private @Nullable Expression visit0( BinaryExpression binary, Expression expression0, - Expression expression1) { + Expression expression1, + boolean evaluated) { Boolean always; switch (binary.getNodeType()) { case AndAlso: always = always(expression0); if (always != null) { - return always - ? expression1 - : FALSE_EXPR; + if (always) { + return expression1; + } + // "x && false" still evaluates x + if (!evaluated || !Expressions.mayThrow(expression1)) { + return FALSE_EXPR; + } } break; case OrElse: @@ -259,12 +275,19 @@ && eq(cmp.expression1, expression2)) { if (always != null) { // true or x --> true // false or x --> x - return always - ? TRUE_EXPR - : expression1; + if (!always) { + return expression1; + } + // "x || true" still evaluates x + if (!evaluated || !Expressions.mayThrow(expression1)) { + return TRUE_EXPR; + } } break; case Equal: + // Not guarded by mayThrow: "x == null" for a primitive x does not + // compile, so this simplification is not optional. Evaluation of x is + // preserved by its declaration, which BlockBuilder keeps. if (isConstantNull(expression1) && isKnownNotNull(expression0)) { return FALSE_EXPR; @@ -277,6 +300,7 @@ && isKnownNotNull(expression0)) { } break; case NotEqual: + // See the comment on Equal above if (isConstantNull(expression1) && isKnownNotNull(expression0)) { return TRUE_EXPR; diff --git a/linq4j/src/test/java/org/apache/calcite/linq4j/test/BlockBuilderTest.java b/linq4j/src/test/java/org/apache/calcite/linq4j/test/BlockBuilderTest.java index 181a067f75a..6950beae82b 100644 --- a/linq4j/src/test/java/org/apache/calcite/linq4j/test/BlockBuilderTest.java +++ b/linq4j/src/test/java/org/apache/calcite/linq4j/test/BlockBuilderTest.java @@ -66,6 +66,76 @@ public void prepareBuilder() { + "}\n")); } + /** Unit test for + * [CALCITE-7728] + * Linq4j can simplify expressions without regards for 'safety'. + * + *

A local variable that is never read is removed, unless computing its + * value may raise a runtime error that the program is expected to raise. */ + @Test void testUnusedDeclarationThatMayThrow() { + final ParameterExpression i = Expressions.parameter(int.class, "i"); + b.append("x", Expressions.divide(ONE, i)); + b.add(Expressions.return_(null, TWO)); + assertThat(b.toBlock(), + hasToString("{\n" + + " final int x = 1 / i;\n" + + " return 2;\n" + + "}\n")); + } + + /** Test case for + * [CALCITE-7728] + * Linq4j can simplify expressions without regards for 'safety'. + * + *

Indexing an array may raise {@link ArrayIndexOutOfBoundsException} or + * {@link NullPointerException}. */ + @Test void testUnusedDeclarationThatIndexesArray() { + final ParameterExpression a = Expressions.parameter(int[].class, "a"); + final ParameterExpression i = Expressions.parameter(int.class, "i"); + b.append("x", Expressions.arrayIndex(a, i)); + b.add(Expressions.return_(null, TWO)); + assertThat(b.toBlock(), + hasToString("{\n" + + " final int x = a[i];\n" + + " return 2;\n" + + "}\n")); + } + + /** Test case for + * [CALCITE-7728] + * Linq4j can simplify expressions without regards for 'safety'. + */ + @Test void testUnusedDeclarationThatCasts() { + final ParameterExpression o = Expressions.parameter(Object.class, "o"); + b.append("x", Expressions.convert_(o, String.class)); + b.add(Expressions.return_(null, TWO)); + // Cast may throw, cannot be removed + assertThat(b.toBlock(), + hasToString("{\n" + + " final String x = (String) o;\n" + + " return 2;\n" + + "}\n")); + } + + /** Test case for + * [CALCITE-7728] + * Linq4j can simplify expressions without regards for 'safety'. + */ + @Test void testUnusedDeclarationThatWidens() { + final ParameterExpression str = Expressions.parameter(String.class, "str"); + b.append("x", Expressions.convert_(str, Object.class)); + // Cast to Object cannot throw, it can be removed + b.add(Expressions.return_(null, TWO)); + assertThat(b.toBlock(), hasToString("{\n return 2;\n}\n")); + } + + @Test void testUnusedDeclarationThatCannotThrow() { + final ParameterExpression i = Expressions.parameter(int.class, "i"); + b.append("x", Expressions.add(ONE, i)); + b.add(Expressions.return_(null, TWO)); + assertThat(b.toBlock(), hasToString("{\n return 2;\n}\n")); + } + @Test void testTestCustomOptimizer() { BlockBuilder b = new BlockBuilder() { @Override protected Shuttle createOptimizeShuttle() { diff --git a/linq4j/src/test/java/org/apache/calcite/linq4j/test/ExpressionTest.java b/linq4j/src/test/java/org/apache/calcite/linq4j/test/ExpressionTest.java index d962204531b..c75c2a21399 100644 --- a/linq4j/src/test/java/org/apache/calcite/linq4j/test/ExpressionTest.java +++ b/linq4j/src/test/java/org/apache/calcite/linq4j/test/ExpressionTest.java @@ -1717,6 +1717,94 @@ public void checkBlockBuilder(boolean optimizing, String expected) { + ".add(\"1\").build()")); } + /** Test cases for + * [CALCITE-7728] + * Linq4j can simplify expressions without regards for 'safety'. + * + *

Checks {@link Expressions#mayThrow} for all possible expressions */ + @Test void testMayThrow() { + final ParameterExpression i = Expressions.parameter(int.class, "i"); + final ParameterExpression j = Expressions.parameter(int.class, "j"); + final ParameterExpression o = Expressions.parameter(Object.class, "o"); + final ParameterExpression str = Expressions.parameter(String.class, "str"); + final ParameterExpression box = Expressions.parameter(Integer.class, "box"); + final ParameterExpression a = Expressions.parameter(int[].class, "a"); + final ParameterExpression all = Expressions.parameter(AllType.class, "all"); + + // Reading a variable or a constant + assertMayThrow(i, false); + assertMayThrow(ONE, false); + + // Java arithmetic wraps around; comparison, bit manipulation + assertMayThrow(Expressions.add(i, j), false); + assertMayThrow(Expressions.multiply(i, j), false); + assertMayThrow(Expressions.negate(i), false); + assertMayThrow(Expressions.lessThan(i, j), false); + assertMayThrow(Expressions.leftShift(i, j), false); + assertMayThrow( + Expressions.andAlso(Expressions.lessThan(i, j), + Expressions.equal(i, j)), false); + assertMayThrow(Expressions.typeIs(o, String.class), false); + assertMayThrow(Expressions.add(str, str), false); + assertMayThrow(Expressions.equal(str, o), false); + + // An operator unboxes its operands, and a null box raises + // NullPointerException + assertMayThrow(Expressions.add(box, i), true); + assertMayThrow(Expressions.negate(box), true); + assertMayThrow(Expressions.lessThan(box, i), true); + assertMayThrow(Expressions.equal(box, i), true); + + // Division may divide by zero; checked arithmetic may overflow + assertMayThrow(Expressions.divide(i, j), true); + assertMayThrow(Expressions.modulo(i, j), true); + assertMayThrow(Expressions.addChecked(i, j), true); + assertMayThrow(Expressions.negateChecked(i), true); + + // An operand that may throw infects the whole expression + assertMayThrow(Expressions.add(ONE, Expressions.divide(ONE, i)), true); + assertMayThrow( + Expressions.condition(Expressions.lessThan(i, j), + Expressions.divide(ONE, i), ONE), true); + + // Reading an array element, and the length of an array + assertMayThrow(Expressions.arrayIndex(a, i), true); + assertMayThrow(Expressions.field(a, "length"), true); + + // Reading an instance field; a static field has no target to be null + assertMayThrow(Expressions.field(all, "i"), true); + assertMayThrow(Expressions.field(null, Integer.class, "MAX_VALUE"), false); + + // Calling a method or a constructor, and creating an array + assertMayThrow(Expressions.call(o, "toString"), true); + assertMayThrow(Expressions.new_(Object.class), true); + assertMayThrow(Expressions.newArrayBounds(int.class, 1, i), true); + assertMayThrow(Expressions.newArrayInit(int.class, ONE, TWO), true); + + // A cast that cannot fail: a primitive conversion, boxing, or a widening + // reference conversion + assertMayThrow(Expressions.convert_(i, long.class), false); + assertMayThrow(Expressions.convert_(i, Integer.class), false); + assertMayThrow(Expressions.convert_(str, Object.class), false); + + // Some casts may raise ClassCastException, and unboxing may throw NPE + assertMayThrow(Expressions.convert_(o, String.class), true); + assertMayThrow(Expressions.convert_(box, int.class), true); + assertMayThrow(Expressions.unbox(box, int.class), true); + + // Throwing, and a block that contains a throw + assertMayThrow(Expressions.throw_(Expressions.new_(RuntimeException.class)), + true); + assertMayThrow( + Expressions.block( + Expressions.throw_(Expressions.new_(RuntimeException.class))), + true); + } + + private static void assertMayThrow(Node node, boolean mayThrow) { + assertThat(node.toString(), Expressions.mayThrow(node), is(mayThrow)); + } + /** An enum. */ enum MyEnum { X, diff --git a/linq4j/src/test/java/org/apache/calcite/linq4j/test/OptimizerTest.java b/linq4j/src/test/java/org/apache/calcite/linq4j/test/OptimizerTest.java index 62c89c73277..8fb0191ec2a 100644 --- a/linq4j/src/test/java/org/apache/calcite/linq4j/test/OptimizerTest.java +++ b/linq4j/src/test/java/org/apache/calcite/linq4j/test/OptimizerTest.java @@ -902,4 +902,84 @@ class OptimizerTest { + " }\n" + "}\n")); } + + /** Unit test for + * [CALCITE-7728] + * Linq4j can simplify expressions without regards for 'safety'. + * + *

An expression that may throw, such as "1 / i", must survive a + * simplification that would otherwise discard it. It may be discarded when + * Java would not have evaluated it anyway. */ + @Test void testDoNotDiscardExpressionThatMayThrow() { + final ParameterExpression i = Expressions.parameter(int.class, "i"); + final Expression divide = Expressions.equal(Expressions.divide(ONE, i), ONE); + final Expression safe = Expressions.equal(i, ONE); + + // "x && false" evaluates x + assertThat(optimize(Expressions.andAlso(divide, FALSE)), + is("{\n return 1 / i == 1 && false;\n}\n")); + assertThat(optimize(Expressions.andAlso(safe, FALSE)), + is("{\n return false;\n}\n")); + + // "false && x" does not evaluate x + assertThat(optimize(Expressions.andAlso(FALSE, divide)), + is("{\n return false;\n}\n")); + + // "x || true" evaluates x + assertThat(optimize(Expressions.orElse(divide, TRUE)), + is("{\n return 1 / i == 1 || true;\n}\n")); + assertThat(optimize(Expressions.orElse(safe, TRUE)), + is("{\n return true;\n}\n")); + + // "a ? b : b" evaluates a + assertThat(optimize(Expressions.condition(divide, ONE, ONE)), + is("{\n return 1 / i == 1 ? 1 : 1;\n}\n")); + assertThat(optimize(Expressions.condition(safe, ONE, ONE)), + is("{\n return 1;\n}\n")); + + // "a == a" evaluates a + assertThat( + optimize( + Expressions.equal(Expressions.divide(ONE, i), + Expressions.divide(ONE, i))), + is("{\n return 1 / i == 1 / i;\n}\n")); + assertThat(optimize(Expressions.equal(i, i)), + is("{\n return true;\n}\n")); + } + + /** Unit test for + * [CALCITE-7728] + * Linq4j can simplify expressions without regards for 'safety'. + * + *

Indexing an array may raise {@link ArrayIndexOutOfBoundsException} or + * {@link NullPointerException}. */ + @Test void testDoNotDiscardArrayIndex() { + final ParameterExpression a = Expressions.parameter(int[].class, "a"); + final ParameterExpression i = Expressions.parameter(int.class, "i"); + final Expression index = + Expressions.equal(Expressions.arrayIndex(a, i), ONE); + + // "a[i] == 1 && false" evaluates "a[i] == 1" + assertThat(optimize(Expressions.andAlso(index, FALSE)), + is("{\n return a[i] == 1 && false;\n}\n")); + + // "false && a[i] == 1" does not evaluate "a[i] == 1" + assertThat(optimize(Expressions.andAlso(FALSE, index)), + is("{\n return false;\n}\n")); + + // "a[i] == 1 || true" evaluates "a[i] == 1" + assertThat(optimize(Expressions.orElse(index, TRUE)), + is("{\n return a[i] == 1 || true;\n}\n")); + + // "a[i] == 1 ? 1 : 1" evaluates "a[i] == 1" + assertThat(optimize(Expressions.condition(index, ONE, ONE)), + is("{\n return a[i] == 1 ? 1 : 1;\n}\n")); + + // "a[i] == a[i]" evaluates "a[i]" + assertThat( + optimize( + Expressions.equal(Expressions.arrayIndex(a, i), + Expressions.arrayIndex(a, i))), + is("{\n return a[i] == a[i];\n}\n")); + } }