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 43f94f47ef [CALCITE-6753] DeterministicCodeOptimizer may lift method 
calls out of try-catch blocks
43f94f47ef is described below

commit 43f94f47efbdba7ac4176a842868094d3741d93a
Author: Mihai Budiu <[email protected]>
AuthorDate: Tue Aug 4 21:06:27 2026 -0700

    [CALCITE-6753] DeterministicCodeOptimizer may lift method calls out of 
try-catch blocks
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../org/apache/calcite/runtime/SqlFunctions.java   |   4 -
 .../linq4j/tree/ClassDeclarationFinder.java        |  26 +++
 .../calcite/linq4j/test/DeterministicTest.java     | 183 +++++++++++++++++++++
 3 files changed, 209 insertions(+), 4 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index cf80e7fc0e..e3ad7c502d 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -5305,10 +5305,6 @@ public static int toInt(java.sql.Time v) {
     return v == null ? castNonNull(null) : toInt(v);
   }
 
-  // Method tagged as non-deterministic because it can throw.
-  // The DeterministicCodeOptimizer may otherwise try to lift it out of 
try-catch blocks.
-  // See https://issues.apache.org/jira/browse/CALCITE-6753
-  @NonDeterministic
   public static int toInt(String s) {
     return parseInt(s.trim());
   }
diff --git 
a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/ClassDeclarationFinder.java
 
b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/ClassDeclarationFinder.java
index bb0d93b013..b952534772 100644
--- 
a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/ClassDeclarationFinder.java
+++ 
b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/ClassDeclarationFinder.java
@@ -34,6 +34,10 @@
 public class ClassDeclarationFinder extends Shuttle {
   protected final @Nullable ClassDeclarationFinder parent;
 
+  /** Visits a subtree without changing it. Used for the subtrees
+   * which must not be optimized. */
+  private static final Shuttle PASS_THROUGH = new Shuttle();
+
   /**
    * The list of new final static fields to be added to the current class.
    */
@@ -153,6 +157,28 @@ protected ClassDeclarationFinder(ClassDeclarationFinder 
parent) {
     return visitor;
   }
 
+  /**
+   * Skips optimization of the entire {@code try} statement.
+   *
+   * <p>An expression must not be factored out of a {@code try} statement:
+   * the initializer of the resulting static field runs during class
+   * initialization, outside the reach of the {@code catch} and
+   * {@code finally} handlers. For example, factoring a method call out of
+   * {@code try { return f(x); } catch (Exception e) { return null; }}
+   * (the shape generated for a safe cast) would make the exception escape
+   * as an {@code ExceptionInInitializerError} instead of yielding
+   * {@code null}. See
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6753";>[CALCITE-6753]
+   * DeterministicCodeOptimizer may lift method calls out of try-catch
+   * blocks</a>.
+   *
+   * @param tryStatement statement to leave unchanged
+   * @return pass-through visitor
+   */
+  @Override public Shuttle preVisit(TryStatement tryStatement) {
+    return PASS_THROUGH;
+  }
+
   @Override public Expression visit(NewExpression newExpression,
       List<Expression> arguments, @Nullable List<MemberDeclaration> 
memberDeclarations) {
     if (parent == null) {
diff --git 
a/linq4j/src/test/java/org/apache/calcite/linq4j/test/DeterministicTest.java 
b/linq4j/src/test/java/org/apache/calcite/linq4j/test/DeterministicTest.java
index 1ec9de8a60..55b50d61fd 100644
--- a/linq4j/src/test/java/org/apache/calcite/linq4j/test/DeterministicTest.java
+++ b/linq4j/src/test/java/org/apache/calcite/linq4j/test/DeterministicTest.java
@@ -511,6 +511,189 @@ private boolean isConstant(Expression e) {
             + "}\n"));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6753";>[CALCITE-6753]
+   * DeterministicCodeOptimizer may lift method calls out of try-catch
+   * blocks</a>. A deterministic method call must stay inside the try
+   * statement; a static field initializer would run outside the reach of
+   * the catch handler. */
+  @Test void testMethodCallWithinTryCatchNotFactored() {
+    assertThat(
+        optimize(
+            Expressions.new_(
+                Runnable.class,
+                Collections.emptyList(),
+                Expressions.methodDecl(
+                    0,
+                    int.class,
+                    "test",
+                    Collections.emptyList(),
+                    Expressions.block(
+                        Expressions.tryCatch(
+                            Expressions.return_(null,
+                                Expressions.call(
+                                    getMethod(Integer.class, "valueOf",
+                                        int.class),
+                                    Expressions.constant(0))),
+                            Expressions.catch_(
+                                Expressions.parameter(Exception.class, "e"),
+                                Expressions.return_(null,
+                                    Expressions.constant(-1)))))))),
+        equalTo("{\n"
+            + "  return new Runnable(){\n"
+            + "      int test() {\n"
+            + "        try {\n"
+            + "          return Integer.valueOf(0);\n"
+            + "        } catch (Exception e) {\n"
+            + "          return -1;\n"
+            + "        }\n"
+            + "      }\n"
+            + "\n"
+            + "    };\n"
+            + "}\n"));
+  }
+
+  /** Expressions in a catch block are not factored out either, and factoring
+   * resumes for statements that follow the try statement. */
+  @Test void testFactoringResumesAfterTryCatch() {
+    assertThat(
+        optimize(
+            Expressions.new_(
+                Runnable.class,
+                Collections.emptyList(),
+                Expressions.methodDecl(
+                    0,
+                    int.class,
+                    "test",
+                    Collections.emptyList(),
+                    Expressions.block(
+                        Expressions.tryCatch(
+                            Expressions.statement(
+                                Expressions.call(
+                                    getMethod(Integer.class, "valueOf",
+                                        int.class),
+                                    Expressions.constant(0))),
+                            Expressions.catch_(
+                                Expressions.parameter(Exception.class, "e"),
+                                Expressions.statement(
+                                    Expressions.call(
+                                        getMethod(Integer.class, "valueOf",
+                                            int.class),
+                                        Expressions.constant(1))))),
+                        Expressions.return_(null,
+                            Expressions.add(ONE, TWO)))))),
+        equalTo("{\n"
+            + "  return new Runnable(){\n"
+            + "      int test() {\n"
+            + "        try {\n"
+            + "          Integer.valueOf(0);\n"
+            + "        } catch (Exception e) {\n"
+            + "          Integer.valueOf(1);\n"
+            + "        }\n"
+            + "        return $L4J$C$1_2;\n"
+            + "      }\n"
+            + "\n"
+            + "      static final int $L4J$C$1_2 = 1 + 2;\n"
+            + "    };\n"
+            + "}\n"));
+  }
+
+  /** A try statement with only a finally block is not optimized either;
+   * moving an expression to a static field initializer would bypass the
+   * finally handler. */
+  @Test void testExpressionWithinTryFinallyNotFactored() {
+    assertThat(
+        optimize(
+            Expressions.new_(
+                Runnable.class,
+                Collections.emptyList(),
+                Expressions.methodDecl(
+                    0,
+                    int.class,
+                    "test",
+                    Collections.emptyList(),
+                    Expressions.block(
+                        Expressions.tryFinally(
+                            Expressions.return_(null,
+                                Expressions.call(
+                                    getMethod(Integer.class, "valueOf",
+                                        int.class),
+                                    Expressions.constant(0))),
+                            Expressions.statement(
+                                Expressions.add(ONE, TWO))))))),
+        equalTo("{\n"
+            + "  return new Runnable(){\n"
+            + "      int test() {\n"
+            + "        try {\n"
+            + "          return Integer.valueOf(0);\n"
+            + "        } finally {\n"
+            + "          1 + 2;\n"
+            + "        }\n"
+            + "      }\n"
+            + "\n"
+            + "    };\n"
+            + "}\n"));
+  }
+
+  /** The optimizer must not add static fields to a class declared within a
+   * try statement.
+   *
+   * <p>Factoring {@code 1 + 2} out to a static field of the {@code Callable}
+   * would move the evaluation into that field's initializer. The initializer
+   * still runs inside the try, when {@code new} first instantiates the
+   * class, but the JVM wraps anything a static initializer throws in an
+   * {@code ExceptionInInitializerError} (an {@code Error}, per JLS 12.4.2),
+   * which {@code catch (Exception e)} does not match. */
+  @Test void testNestedClassWithinTryCatchNotFactored() {
+    assertThat(
+        optimize(
+            Expressions.new_(
+                Runnable.class,
+                Collections.emptyList(),
+                Expressions.methodDecl(
+                    0,
+                    int.class,
+                    "test",
+                    Collections.emptyList(),
+                    Expressions.block(
+                        Expressions.tryCatch(
+                            Expressions.return_(null,
+                                Expressions.call(
+                                    Expressions.new_(
+                                        Callable.class,
+                                        Collections.emptyList(),
+                                        Expressions.methodDecl(
+                                            0,
+                                            Object.class,
+                                            "call",
+                                            Collections.emptyList(),
+                                            Blocks.toFunctionBlock(
+                                                Expressions.add(ONE, TWO)))),
+                                    "call",
+                                    Collections.emptyList())),
+                            Expressions.catch_(
+                                Expressions.parameter(Exception.class, "e"),
+                                Expressions.return_(null,
+                                    Expressions.constant(-1)))))))),
+        equalTo("{\n"
+            + "  return new Runnable(){\n"
+            + "      int test() {\n"
+            + "        try {\n"
+            + "          return new java.util.concurrent.Callable(){\n"
+            + "              Object call() {\n"
+            + "                return 1 + 2;\n"
+            + "              }\n"
+            + "\n"
+            + "            }.call();\n"
+            + "        } catch (Exception e) {\n"
+            + "          return -1;\n"
+            + "        }\n"
+            + "      }\n"
+            + "\n"
+            + "    };\n"
+            + "}\n"));
+  }
+
   @Test void testDeterministicClassNonDeterministicMethod() {
     assertThat(
         optimize(

Reply via email to