andygrove commented on code in PR #5219:
URL: https://github.com/apache/datafusion-comet/pull/5219#discussion_r3705580983


##########
spark/src/main/scala/org/apache/comet/codegen/CometBatchKernelCodegen.scala:
##########
@@ -351,82 +350,94 @@ object CometBatchKernelCodegen extends Logging with 
CometExprTraitShim with Come
       ev: ExprCode,
       writeSnippet: String,
       subExprsCode: String): String = {
-    boundExpr match {
-      case _ if isNullIntolerant(boundExpr) && allNullIntolerant(boundExpr) =>
-        // Every node from root to leaf is `NullIntolerant` or a leaf, so "any 
BoundReference null
-        // -> whole expression null". A non-null-propagating node like 
`coalesce` or `if` would
-        // make this incorrect (`coalesce(null, x)` is `x`); 
`allNullIntolerant` rejects those.
-        val inputOrdinals =
-          boundExpr.collect { case b: BoundReference => b.ordinal }.distinct
-        // Primitive Arrow vectors are wrapped in `CometPlainVector` at 
input-cast time, which
-        // exposes `isNullAt(int)` rather than the raw Arrow `isNull(int)`. 
Pick the right method
-        // per ordinal so the short-circuit compiles for timestamp / int / 
float columns too,
-        // not just VarChar / Decimal vectors that stay as raw Arrow types.
-        def nullCheckCall(ord: Int): String = {
-          val method = 
CometBatchKernelCodegenInput.nullCheckMethod(inputSchema(ord))
-          s"this.col$ord.$method(i)"
-        }
-        val nullCheck =
-          if (inputOrdinals.isEmpty) "false"
-          else inputOrdinals.map(nullCheckCall).mkString(" || ")
-        // `NullIntolerant` only constrains "any input null -> output null"; 
it does NOT promise
-        // that non-null inputs always produce non-null output. 
`MakeTimestamp(failOnError=false)`
-        // is `NullIntolerant=true` but its `doGenCode` catches 
`DateTimeException` for invalid
-        // year/month/day/hour/min/sec components and sets `ev.isNull = true`. 
Honor `ev.isNull`
-        // post-eval whenever the expression is nullable; skip the guard only 
when the root is
-        // statically non-nullable (`ev.isNull` is then a literal `false`).
-        if (boundExpr.nullable) {
-          s"""
-             |if ($nullCheck) {
-             |  output.setNull(i);
-             |} else {
-             |  $subExprsCode
-             |  ${ev.code}
-             |  if (${ev.isNull}) {
-             |    output.setNull(i);
-             |  } else {
-             |    $writeSnippet
-             |  }
-             |}
+    val inputOrdinals = boundExpr.collect { case b: BoundReference => 
b.ordinal }.distinct
+    if (canShortCircuitNulls(boundExpr, inputOrdinals)) {
+      // Primitive Arrow vectors are wrapped in `CometPlainVector` at 
input-cast time, which
+      // exposes `isNullAt(int)` rather than the raw Arrow `isNull(int)`. Pick 
the right method
+      // for the ordinal so the short-circuit compiles for timestamp / int / 
float columns too,
+      // not just VarChar / Decimal vectors that stay as raw Arrow types.
+      val ord = inputOrdinals.head
+      val nullCheck =
+        
s"this.col$ord.${CometBatchKernelCodegenInput.nullCheckMethod(inputSchema(ord))}(i)"
+      // `NullIntolerant` only constrains "any input null -> output null"; it 
does NOT promise
+      // that non-null inputs always produce non-null output. 
`MakeTimestamp(failOnError=false)`
+      // is `NullIntolerant=true` but its `doGenCode` catches 
`DateTimeException` for invalid
+      // year/month/day/hour/min/sec components and sets `ev.isNull = true`. 
Honor `ev.isNull`
+      // post-eval whenever the expression is nullable; skip the guard only 
when the root is
+      // statically non-nullable (`ev.isNull` is then a literal `false`).
+      if (boundExpr.nullable) {
+        s"""
+           |if ($nullCheck) {
+           |  output.setNull(i);
+           |} else {
+           |  $subExprsCode
+           |  ${ev.code}
+           |  if (${ev.isNull}) {
+           |    output.setNull(i);
+           |  } else {
+           |    $writeSnippet
+           |  }
+           |}
            """.stripMargin
-        } else {
-          s"""
-             |if ($nullCheck) {
-             |  output.setNull(i);
-             |} else {
-             |  $subExprsCode
-             |  ${ev.code}
-             |  $writeSnippet
-             |}
+      } else {
+        s"""
+           |if ($nullCheck) {
+           |  output.setNull(i);
+           |} else {
+           |  $subExprsCode
+           |  ${ev.code}
+           |  $writeSnippet
+           |}
            """.stripMargin
-        }
-      case _ =>
-        // NonNullableOutputShortCircuit: when `nullable = false`, drop the 
`if (ev.isNull)`
-        // guard at source level rather than relying on JIT folding.
-        if (!boundExpr.nullable) {
-          s"""
-             |$subExprsCode
-             |${ev.code}
-             |$writeSnippet
+      }
+    } else {
+      // NonNullableOutputShortCircuit: when `nullable = false`, drop the `if 
(ev.isNull)`
+      // guard at source level rather than relying on JIT folding.
+      if (!boundExpr.nullable) {
+        s"""
+           |$subExprsCode
+           |${ev.code}
+           |$writeSnippet
            """.stripMargin
-        } else {
-          s"""
-             |$subExprsCode
-             |${ev.code}
-             |if (${ev.isNull}) {
-             |  output.setNull(i);
-             |} else {
-             |  $writeSnippet
-             |}
+      } else {
+        s"""
+           |$subExprsCode
+           |${ev.code}
+           |if (${ev.isNull}) {
+           |  output.setNull(i);
+           |} else {
+           |  $writeSnippet
+           |}
            """.stripMargin
-        }
+      }
     }
   }
 
+  /**
+   * Gates the [[defaultBody]] null short-circuit. Three conditions, all 
necessary:
+   *
+   *   - The tree reads exactly one input ordinal. Spark's null handling is 
per-node and
+   *     left-to-right (`BinaryExpression.nullSafeCodeGen` emits the left 
child's code
+   *     unconditionally, then tests the left child's null, then the right 
child's), so a
+   *     short-circuit on the union of several ordinals is not equivalent: it 
skips a subtree that
+   *     Spark would have evaluated, and with it any error that subtree 
raises. Under ANSI,
+   *     `add_months(cast(s as date), i)` on `('notadate', NULL)` raises 
`CAST_INVALID_INPUT` in
+   *     Spark, so returning null here would be wrong. With a single ordinal 
there is nothing left
+   *     for Spark to evaluate ahead of that ordinal's own null check, so the 
short-circuit is
+   *     exact. (A literal-only subtree that raises would be the one 
exception, but Catalyst's
+   *     `ConstantFolding` evaluates those at analysis time and never leaves 
one in the tree.)
+   *   - The root is `NullIntolerant`, so a null input really does mean a null 
result.
+   *   - Every node in the tree is null-propagating ([[allNullIntolerant]]); a 
`Coalesce` / `If` /
+   *     `CaseWhen` anywhere would break the chain.
+   */
+  private def canShortCircuitNulls(expr: Expression, inputOrdinals: Seq[Int]): 
Boolean =
+    inputOrdinals.size == 1 && isNullIntolerant(expr) && 
allNullIntolerant(expr)

Review Comment:
   Done in dd6ae3225 — `canShortCircuitNulls` now accepts either exactly one 
input ordinal or a leaf-only-children root, and `defaultBody` emits the 
disjunction over every ordinal the tree reads instead of just the first. That 
restores the fast path for the no-cast multi-argument shapes.
   
   One wrinkle worth recording, because it makes the rule correct for a 
slightly different reason than stated above. The argument was that with 
leaf-only children "the only place an error can occur is the root's own `eval`, 
and that only runs in the `else` branch either way." That isn't quite true for 
`pmod`: it is one of the expressions that deliberately reorders its children, 
so its error check is *not* simply gated behind all-inputs-non-null. 
`Pmod.doGenCode` (it extends `BinaryArithmetic` directly and carries its own 
copy of the `DivModLike` shape) evaluates the divisor first and throws 
`REMAINDER_BY_ZERO` under ANSI:
   
   ```java
   ${eval2.code}                              // divisor
   if (${eval2.isNull}) {
     ${ev.isNull} = true;
   } else {
     ${eval1.code}                            // dividend
     if (${eval1.isNull}) {
       ${ev.isNull} = true;                   // <-- reached before the throw
     } else {
       if ($isZero) throw ...remainderByZeroError(...);
       $result
     }
   }
   ```
   
   The rule survives, but because the *dividend's null check precedes the 
throw*, not because the throw is gated on all inputs being non-null. So 
`pmod(NULL, 0)` returns NULL in Spark, and the union short-circuit stays exact 
for it. Had the throw been hoisted above the `eval1.isNull` test, `pmod(a, b)` 
with no cast on either argument would have been a swallowed-error case under 
the leaf-only rule.
   
   Since that is load-bearing and invisible from Comet's side, I documented it 
on `canShortCircuitNulls` and pinned it from both directions in 
`CometCodegenSuite`: `(NULL, 0)` must return NULL, `(7, 0)` must raise 
`REMAINDER_BY_ZERO`. If a future Spark version reorders that throw, the second 
assertion fails rather than the behaviour silently regressing.
   
   Also added: source-shape tests for both the kept (leaf-only) and skipped 
(subtree-under-root) shapes, and a runtime test over every null combination 
across two ordinals. The #4554 test goes back to its original two-`setNull` 
assertion, since its `MakeTimestamp` over six `BoundReference`s is leaf-only 
and keeps the short-circuit; it now also asserts all six ordinals get tested. A 
new sibling test puts a `Cast` under the same root to pin the single-`setNull` 
shape, so the #4554 post-eval guard is still verified independently of the 
short-circuit.
   
   I confirmed the new assertions actually discriminate: reverting the gate to 
the single-ordinal-only rule fails exactly the two tests that pin the restored 
fast path and nothing else. 175 tests across the 5 codegen suites and 137 
across the dispatcher-backed expression suites, 0 failures.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to