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"));
+  }
 }

Reply via email to