This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 9be85f963a [CALCITE-7728] Linq4j can simplify expressions without
regards for 'safety'
9be85f963a is described below
commit 9be85f963aa0628a58e16050258c9b2af328d547
Author: Mihai Budiu <[email protected]>
AuthorDate: Tue Aug 18 22:45:10 2026 -0700
[CALCITE-7728] Linq4j can simplify expressions without regards for 'safety'
Signed-off-by: Mihai Budiu <[email protected]>
---
.../calcite/adapter/enumerable/RexImpTable.java | 10 +-
.../apache/calcite/linq4j/tree/BlockBuilder.java | 10 +
.../apache/calcite/linq4j/tree/Expressions.java | 244 +++++++++++++++++++++
.../calcite/linq4j/tree/OptimizeShuttle.java | 46 +++-
.../calcite/linq4j/test/BlockBuilderTest.java | 70 ++++++
.../apache/calcite/linq4j/test/ExpressionTest.java | 88 ++++++++
.../apache/calcite/linq4j/test/OptimizerTest.java | 80 +++++++
7 files changed, 534 insertions(+), 14 deletions(-)
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 36fd19f67b..ed7eac4cde 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<RexToLixTranslator.Result> 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 febc454d27..28aab938a1 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 ebebd2ca69..5ba55093b6 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.
+ *
+ * <p>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<T> extends List<T> {
FluentList<T> appendAll(T... ts);
}
+ /** Visitor that detects whether a node may cause a runtime error.
+ *
+ * <p>The analysis is conservative: a false positive is a missed
+ * simplification, but a false negative is a lost runtime error.
+ *
+ * <p>These nodes may throw:
+ *
+ * <ul>
+ * <li>calling anything - a method, a constructor, or a function value - as
+ * the callee decides whether to throw, and calling on a null target raises
+ * {@link NullPointerException};
+ * <li>reading an array element may raise
+ * {@link ArrayIndexOutOfBoundsException} or {@link NullPointerException};
+ * <li>reading an instance field if the target is null;
+ * <li>creating an array may raise {@link NegativeArraySizeException};
+ * <li>a cast that is not statically known to succeed may raise
+ * {@link ClassCastException};
+ * <li>unboxing raises {@link NullPointerException} on a null box;
+ * may implied by an operator, e.g. {@code integer + 1};
+ * <li>division and remainder (divide by zero), and checked
+ * arithmetic, which may overflow;
+ * <li>a {@code throw}, and a {@code try} whose body may throw.
+ * </ul>
+ *
+ * <p>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 <T> 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 b6ea7e5faa..a5f04cdc35 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.
+ *
+ * <p>{@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 181a067f75..6950beae82 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
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7728">[CALCITE-7728]
+ * Linq4j can simplify expressions without regards for 'safety'</a>.
+ *
+ * <p>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
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7728">[CALCITE-7728]
+ * Linq4j can simplify expressions without regards for 'safety'</a>.
+ *
+ * <p>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
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7728">[CALCITE-7728]
+ * Linq4j can simplify expressions without regards for 'safety'</a>.
+ */
+ @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
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7728">[CALCITE-7728]
+ * Linq4j can simplify expressions without regards for 'safety'</a>.
+ */
+ @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 d962204531..c75c2a2139 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
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7728">[CALCITE-7728]
+ * Linq4j can simplify expressions without regards for 'safety'</a>.
+ *
+ * <p>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 62c89c7327..8fb0191ec2 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
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7728">[CALCITE-7728]
+ * Linq4j can simplify expressions without regards for 'safety'</a>.
+ *
+ * <p>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
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7728">[CALCITE-7728]
+ * Linq4j can simplify expressions without regards for 'safety'</a>.
+ *
+ * <p>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"));
+ }
}