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 197d569217 [CALCITE-7729] Linq4j BlockBuilder.optimize can optimize 
away expressions that throw
197d569217 is described below

commit 197d569217eda062142d2e9164a8e00a1db6ab61
Author: Mihai Budiu <[email protected]>
AuthorDate: Wed Aug 19 21:13:06 2026 -0700

    [CALCITE-7729] Linq4j BlockBuilder.optimize can optimize away expressions 
that throw
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../calcite/adapter/enumerable/RexImpTable.java    |  28 ++++-
 .../java/org/apache/calcite/test/JdbcTest.java     |   3 +-
 .../apache/calcite/test/ReflectiveSchemaTest.java  |   4 +-
 .../apache/calcite/linq4j/tree/BlockBuilder.java   | 108 ++++++++++++++++---
 .../calcite/linq4j/test/BlockBuilderTest.java      | 116 +++++++++++++++++++++
 .../apache/calcite/linq4j/test/ExpressionTest.java |   4 +-
 .../apache/calcite/linq4j/test/OptimizerTest.java  |   6 +-
 7 files changed, 247 insertions(+), 22 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 ae40e33c13..6ef0182b30 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
@@ -2498,11 +2498,33 @@ protected FirstLastValueImplementor(SeekType seekType) {
         return implementResultIgnoreNulls(info, winResult);
       }
 
-      return Expressions.condition(winResult.hasRows(),
+      // Generates:
+      //   T first_last_value;
+      //   if (<hasRows>) {
+      //     <statements that read the row>
+      //     first_last_value = <value of the argument in that row>;
+      //   } else {
+      //     first_last_value = <default value>;
+      //   }
+      //
+      // Reading a row is only valid when the frame has rows: on an empty frame
+      // computeIndex returns -1.
+      final ParameterExpression res =
+          Expressions.parameter(0, info.returnType(),
+              result.currentBlock().newName("first_last_value"));
+      final BlockBuilder thenBlock = result.nestBlock();
+      final Expression value =
           winResult.rowTranslator(
               winResult.computeIndex(Expressions.constant(0), seekType))
-              .translate(winResult.rexArguments().get(0), info.returnType()),
-          getDefaultValue(info.returnType()));
+              .translate(winResult.rexArguments().get(0), info.returnType());
+      thenBlock.add(Expressions.statement(Expressions.assign(res, value)));
+      result.exitBlock();
+      result.currentBlock().add(Expressions.declare(0, res, null));
+      result.currentBlock().add(
+          Expressions.ifThenElse(winResult.hasRows(), thenBlock.toBlock(),
+              Expressions.statement(
+                  Expressions.assign(res, 
getDefaultValue(info.returnType())))));
+      return res;
     }
 
     /**
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index d235fa327c..6dc4a48dc9 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -2772,6 +2772,7 @@ private void checkNullableTimestamp(CalciteAssert.Config 
config) {
         + "              final org.apache.calcite.test.schemata.hr.Employee 
current"
         + " = (org.apache.calcite.test.schemata.hr.Employee) 
inputEnumerator.current();\n"
         + "              final String input_value = current.name;\n"
+        + "              final int input_value0 = current.deptno;\n"
         + "              Integer case_when_value;\n"
         + "              if 
($L4J$C$org_apache_calcite_runtime_SqlFunctions_ne_) {\n"
         + "                case_when_value = $L4J$C$Integer_valueOf_1_;\n"
@@ -2780,7 +2781,7 @@ private void checkNullableTimestamp(CalciteAssert.Config 
config) {
         + "              }\n"
         + "              final Integer binary_call_value0 = "
         + "case_when_value == null ? null : "
-        + "Integer.valueOf(current.deptno + case_when_value.intValue());\n"
+        + "Integer.valueOf(input_value0 + case_when_value.intValue());\n"
         + "              return input_value == null || binary_call_value0 == 
null"
         + " ? null"
         + " : org.apache.calcite.runtime.SqlFunctions.substring(input_value, "
diff --git 
a/core/src/test/java/org/apache/calcite/test/ReflectiveSchemaTest.java 
b/core/src/test/java/org/apache/calcite/test/ReflectiveSchemaTest.java
index e4979b95f3..cc0205c987 100644
--- a/core/src/test/java/org/apache/calcite/test/ReflectiveSchemaTest.java
+++ b/core/src/test/java/org/apache/calcite/test/ReflectiveSchemaTest.java
@@ -764,7 +764,9 @@ private void check(ResultSetMetaData metaData, String 
columnName,
         .planContains(
             "final Long input_value = current.wrapperLong;")
         .planContains(
-            "return input_value == null ? null : 
Long.valueOf(input_value.longValue() / current.primitiveLong);")
+            "final long input_value0 = current.primitiveLong;")
+        .planContains(
+            "return input_value == null ? null : 
Long.valueOf(input_value.longValue() / input_value0);")
         .returns("C=null\n");
   }
 
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 28aab938a1..5d811ea68c 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
@@ -360,7 +360,7 @@ private boolean optimize(Shuttle optimizer, boolean 
performInline) {
     for (Statement statement : statements) {
       if (statement instanceof DeclarationStatement && performInline) {
         DeclarationStatement decl = (DeclarationStatement) statement;
-        useCounter.map.put(decl.parameter, new Slot());
+        useCounter.map.put(decl.parameter, new ParameterUse());
       }
       // We are added only counters up to current statement.
       // It is fine to count usages as the latter declarations cannot be used
@@ -378,7 +378,7 @@ private boolean optimize(Shuttle optimizer, boolean 
performInline) {
     for (Statement oldStatement : oldStatements) {
       if (oldStatement instanceof DeclarationStatement) {
         DeclarationStatement statement = (DeclarationStatement) oldStatement;
-        final Slot slot = useCounter.map.get(statement.parameter);
+        final ParameterUse slot = useCounter.map.get(statement.parameter);
         int count = slot == null ? Integer.MAX_VALUE - 10 : slot.count;
         if (count > 1 && isSimpleExpression(statement.initializer)) {
           // Inline simple final constants
@@ -410,13 +410,15 @@ 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;
+        if (statement.initializer != null
+            && (count == 0 || slot != null && slot.conditional)) {
+          // The value is either never read, or read only conditionally.
+          // If it may raise a runtime error, keep the declaration where it is.
+          final Expression initializer =
+              subMap.isEmpty() ? statement.initializer : 
statement.initializer.accept(visitor);
+          if (Expressions.mayThrow(initializer)) {
+            count = 100;
+          }
         }
         Expression normalized = normalizeDeclaration(statement);
         expressionForReuse.remove(normalized);
@@ -603,15 +605,92 @@ private static class InlineVariableVisitor extends 
SubstituteVariableVisitor {
 
   /** Use counter. */
   private static class UseCounter extends VisitorImpl<Void> {
-    private final IdentityHashMap<ParameterExpression, Slot> map = new 
IdentityHashMap<>();
+    /** Map each parameter to information about how it is used. */
+    private final IdentityHashMap<ParameterExpression, ParameterUse> map = new 
IdentityHashMap<>();
+    /** Whether the node being visited is evaluated only if some other
+     * expression permits it, as "a" in the expression "c ? a : b". */
+    private boolean inConditional = false;
+
+    /** Visits a node that is evaluated only under a condition. */
+    private void acceptConditionally(Node node) {
+      final boolean prev = inConditional;
+      inConditional = true;
+      node.accept(this);
+      inConditional = prev;
+    }
+
+    @Override public Void visit(TernaryExpression ternary) {
+      if (ternary.getNodeType() != ExpressionType.Conditional) {
+        return super.visit(ternary);
+      }
+      ternary.expression0.accept(this);
+      acceptConditionally(ternary.expression1);
+      acceptConditionally(ternary.expression2);
+      return null;
+    }
+
+    @Override public Void visit(BinaryExpression binary) {
+      switch (binary.getNodeType()) {
+      case AndAlso:
+      case OrElse:
+        // The right operand is evaluated only if the left one has not already 
decided the result
+        binary.expression0.accept(this);
+        acceptConditionally(binary.expression1);
+        return null;
+      default:
+        return super.visit(binary);
+      }
+    }
+
+    @Override public Void visit(WhileStatement whileStatement) {
+      // The body may never run
+      whileStatement.condition.accept(this);
+      acceptConditionally(whileStatement.body);
+      return null;
+    }
+
+    @Override public Void visit(ForStatement forStatement) {
+      // The body and the "post" expression may not run
+      Expressions.acceptNodes(forStatement.declarations, this);
+      if (forStatement.condition != null) {
+        forStatement.condition.accept(this);
+      }
+      if (forStatement.post != null) {
+        acceptConditionally(forStatement.post);
+      }
+      acceptConditionally(forStatement.body);
+      return null;
+    }
+
+    @Override public Void visit(ForEachStatement forEachStatement) {
+      // The body may not run
+      forEachStatement.parameter.accept(this);
+      forEachStatement.iterable.accept(this);
+      acceptConditionally(forEachStatement.body);
+      return null;
+    }
+
+    @Override public Void visit(ConditionalStatement conditionalStatement) {
+      // In "if (c0) s0 else if (c1) s1 ... else s", only "c0" is unconditional
+      final List<Node> list = conditionalStatement.expressionList;
+      for (int i = 0; i < list.size(); i++) {
+        if (i == 0) {
+          list.get(i).accept(this);
+        } else {
+          acceptConditionally(list.get(i));
+        }
+      }
+      return null;
+    }
 
     @Override public Void visit(ParameterExpression parameter) {
-      final Slot slot = map.get(parameter);
+      final ParameterUse slot = map.get(parameter);
       if (slot != null) {
         // Count use of parameter, if it's registered. It's OK if
         // parameter is not registered. It might be beyond the control
         // of this block.
         slot.count++;
+        slot.conditional |= inConditional;
       }
       return super.visit(parameter);
     }
@@ -626,9 +705,12 @@ private static class UseCounter extends VisitorImpl<Void> {
   }
 
   /**
-   * Holds the number of times a declaration was used.
+   * Holds information about the uses of one ParameterExpression within an 
expression.
    */
-  private static class Slot {
+  private static class ParameterUse {
+    /** How many times the declaration is read. */
     private int count;
+    /** Whether at least one read is evaluated only under a condition. */
+    private boolean conditional;
   }
 }
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 6950beae82..5cc772c0b5 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
@@ -83,6 +83,122 @@ public void prepareBuilder() {
             + "}\n"));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7729";>[CALCITE-7729]
+   * Linq4j BlockBuilder.optimize can optimize away expressions that
+   * throw</a>. */
+  @Test void testDeclarationUsedOnlyInBranchThatFoldsAway() {
+    final ParameterExpression i = Expressions.parameter(int.class, "i");
+    final Expression x = b.append("x", Expressions.divide(ONE, i));
+    b.add(
+        Expressions.return_(null,
+            Expressions.condition(Expressions.constant(true), TWO, x)));
+    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-7729";>[CALCITE-7729]
+   * Linq4j BlockBuilder.optimize can optimize away expressions that
+   * throw</a>. */
+  @Test void testDeclarationUsedInBranchThatSurvives() {
+    final ParameterExpression i = Expressions.parameter(int.class, "i");
+    final ParameterExpression c = Expressions.parameter(boolean.class, "c");
+    final Expression x = b.append("x", Expressions.divide(ONE, i));
+    b.add(Expressions.return_(null, Expressions.condition(c, TWO, x)));
+    assertThat(b.toBlock(),
+        hasToString("{\n"
+            + "  final int x = 1 / i;\n"
+            + "  return c ? 2 : x;\n"
+            + "}\n"));
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7729";>[CALCITE-7729]
+   * Linq4j BlockBuilder.optimize can optimize away expressions that
+   * throw</a>.
+   *
+   * <p>Nested conditionals: "x" is read in a branch of an inner conditional,
+   * which is itself in a branch. */
+  @Test void testNestedConditionalBranch() {
+    final ParameterExpression i = Expressions.parameter(int.class, "i");
+    final ParameterExpression c = Expressions.parameter(boolean.class, "c");
+    final ParameterExpression d = Expressions.parameter(boolean.class, "d");
+    final Expression x = b.append("x", Expressions.divide(ONE, i));
+    b.add(
+        Expressions.return_(null,
+            Expressions.condition(c,
+                Expressions.condition(d, TWO, x), ONE)));
+    assertThat(b.toBlock(),
+        hasToString("{\n"
+            + "  final int x = 1 / i;\n"
+            + "  return c ? (d ? 2 : x) : 1;\n"
+            + "}\n"));
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7729";>[CALCITE-7729]
+   * Linq4j BlockBuilder.optimize can optimize away expressions that
+   * throw</a>.
+   *
+   * <p>Nested conditionals: "x" is read in the condition of an inner
+   * conditional, so it is evaluated only if the outer condition holds. */
+  @Test void testNestedConditionalCondition() {
+    final ParameterExpression i = Expressions.parameter(int.class, "i");
+    final ParameterExpression c = Expressions.parameter(boolean.class, "c");
+    final Expression x = b.append("x", Expressions.divide(ONE, i));
+    b.add(
+        Expressions.return_(null,
+            Expressions.condition(c,
+                Expressions.condition(
+                    Expressions.greaterThan(x, ONE), TWO, ONE),
+                ONE)));
+    assertThat(b.toBlock(),
+        hasToString("{\n"
+            + "  final int x = 1 / i;\n"
+            + "  return c ? (x > 1 ? 2 : 1) : 1;\n"
+            + "}\n"));
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7729";>[CALCITE-7729]
+   * Linq4j BlockBuilder.optimize can optimize away expressions that
+   * throw</a>. */
+  @Test void testDeclarationUsedOnlyInWhileBody() {
+    final ParameterExpression i = Expressions.parameter(int.class, "i");
+    final ParameterExpression c = Expressions.parameter(boolean.class, "c");
+    final ParameterExpression y = Expressions.parameter(int.class, "y");
+    final Expression x = b.append("x", Expressions.divide(ONE, i));
+    b.add(Expressions.declare(0, y, ONE));
+    b.add(
+        Expressions.while_(c,
+            Expressions.statement(Expressions.assign(y, x))));
+    assertThat(b.toBlock(),
+        hasToString("{\n"
+            + "  final int x = 1 / i;\n"
+            + "  int y = 1;\n"
+            + "  while (c) {\n"
+            + "    y = x;\n"
+            + "  }\n"
+            + "}\n"));
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7729";>[CALCITE-7729]
+   * Linq4j BlockBuilder.optimize can optimize away expressions that
+   * throw</a>. */
+  @Test void testPureDeclarationIsInlinedIntoBranch() {
+    // Test with expression that does not throw
+    final ParameterExpression i = Expressions.parameter(int.class, "i");
+    final ParameterExpression c = Expressions.parameter(boolean.class, "c");
+    final Expression x = b.append("x", Expressions.add(ONE, i));
+    b.add(Expressions.return_(null, Expressions.condition(c, TWO, x)));
+    assertThat(b.toBlock(), hasToString("{\n  return c ? 2 : 1 + i;\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>.
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 c75c2a2139..3482e2b867 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
@@ -1496,8 +1496,8 @@ public void checkBlockBuilder(boolean optimizing, String 
expected) {
     assertThat(Expressions.toString(builder.toBlock()),
         is("{\n"
             + "  final Short v = (Short) ((Object[]) p)[4];\n"
-            + "  return (Number) v == null ? null : ("
-            + "(Number) v).intValue() == 1997;\n"
+            + "  final int v5 = ((Number) v).intValue();\n"
+            + "  return (Number) v == null ? null : v5 == 1997;\n"
             + "}\n"));
   }
 
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 8fb0191ec2..ce1e429c6a 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
@@ -868,8 +868,9 @@ class OptimizerTest {
                         x_)))),
         equalTo("{\n"
             + "  long x = 0L;\n"
+            + "  final long y = System.currentTimeMillis();\n"
             + "  if (System.nanoTime() > 0L) {\n"
-            + "    x = System.currentTimeMillis();\n"
+            + "    x = y;\n"
             + "  }\n"
             + "  System.out.println(x);\n"
             + "}\n"));
@@ -897,8 +898,9 @@ class OptimizerTest {
                     Expressions.statement(Expressions.assign(x_, y_))))),
         equalTo("{\n"
             + "  long x = 0L;\n"
+            + "  final long y = System.currentTimeMillis();\n"
             + "  if (System.currentTimeMillis() > 0L) {\n"
-            + "    x = System.currentTimeMillis();\n"
+            + "    x = y;\n"
             + "  }\n"
             + "}\n"));
   }

Reply via email to