cloud-fan commented on code in PR #58045:
URL: https://github.com/apache/spark/pull/58045#discussion_r3854922288


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodegenFallback.scala:
##########
@@ -25,13 +25,31 @@ import 
org.apache.spark.sql.catalyst.expressions.codegen.Block._
  */
 trait CodegenFallback extends Expression {
 
-  protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
+  protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode =
+    CodegenFallback.generate(this, ctx, ev)
+}
+
+object CodegenFallback {
+
+  /**
+   * Generates code that evaluates `e` by calling its `eval`, rather than by 
generating code for it.
+   * This is what [[CodegenFallback]] gives the expressions that mix it in, 
and is also used by an
+   * expression that can generate code in general but has to fall back for a 
particular shape of
+   * its own tree -- see `With.doGenCode`.
+   *
+   * The two places that reason about which subtrees run interpretively both 
dispatch on the trait:

Review Comment:
   **Nit:**
   
   Please add `EquivalentExpressions.childrenToRecurse` to this maintenance 
list. It also dispatches on `CodegenFallback` to keep interpretive subtrees out 
of CSE, so the current `two places` claim is incomplete.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpressionSuite.scala:
##########
@@ -226,15 +228,85 @@ class RewriteWithExpressionSuite extends PlanTest {
     )
   }
 
+  test("SPARK-58902: a With left in a conditional branch of an aggregate still 
converges") {
+    val Seq(a, b) = testRelation.output
+    // Not cheap and referenced twice, so it stays a memoizing `With` rather 
than being inlined.
+    val inBranch = With(a + b) { case Seq(ref) => ref * ref }
+    val plan = testRelation.groupBy(a)(max(Coalesce(Seq(a, 
inBranch))).as("col"))
+    // The `PhysicalAggregation` arm restructures the aggregate into a 
`Project` above it, and its
+    // guard is "the expressions contain a `With`", which a surviving one 
keeps true on every
+    // iteration of this fixed-point batch. Without the eq check in the rule 
this raises
+    // `Max iterations (5) reached for batch Rewrite With expression`, one 
`Project` per iteration.
+    val rewritten = Optimizer.execute(plan)
+    // Idempotent: running the batch again changes nothing.
+    comparePlans(Optimizer.execute(rewritten), rewritten)
+    assert(rewritten.collect { case p: Project => p }.size <= 1,
+      s"the rule stacked a Project per iteration:\n$rewritten")
+  }
+
+  test("SPARK-58902: a cheap or single-reference definition in a branch is 
still inlined") {
+    val Seq(a, b) = testRelation.output
+    // A bare attribute is cheap, so inlining it costs nothing and keeps the 
branch foldable.

Review Comment:
   **Nit:**
   
   `Attribute.foldable` is false, so inlining `a` does not keep this branch 
foldable.
   
   ```suggestion
       // A bare attribute is cheap, so inlining it costs nothing.
   ```



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/WithExpressionEvalSuite.scala:
##########
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.catalyst.expressions
+
+import org.apache.spark.SparkFunSuite
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, 
CodegenFallback, ExprCode, GenerateMutableProjection}
+import org.apache.spark.sql.types.{DataType, IntegerType}
+
+/**
+ * Evaluation of [[With]] and the memoization it gives a 
[[CommonExpressionRef]]. The rewrite that
+ * decides which `With`s reach evaluation at all is covered by 
`RewriteWithExpressionSuite`.
+ */
+class WithExpressionEvalSuite extends SparkFunSuite {
+
+  /**
+   * A stand-in for a stateful generator: every evaluation returns the next 
integer, so a second
+   * evaluation within one row is directly observable. Only used on the 
interpreted path.
+   */
+  private case class Counter() extends LeafExpression with Nondeterministic {
+    @transient private var n = 0
+    override def stateful: Boolean = true
+    override def dataType: DataType = IntegerType
+    override def nullable: Boolean = false
+    override protected def initializeInternal(partitionIndex: Int): Unit = {}
+    override protected def evalInternal(input: InternalRow): Any = { n += 1; n 
}
+    override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): 
ExprCode =
+      throw new UnsupportedOperationException
+  }
+
+  private def counter(): Counter = {
+    val c = Counter()
+    c.initialize(0)
+    c
+  }
+
+  /**
+   * A node that has to be evaluated interpretively even when its parent is 
generated, so that a
+   * reference below it takes the interpreted path out of generated code.
+   */
+  private case class Fallback(child: Expression) extends UnaryExpression with 
CodegenFallback {
+    override def dataType: DataType = child.dataType
+    override def eval(input: InternalRow): Any = child.eval(input)
+    override protected def withNewChildInternal(newChild: Expression): 
Fallback =
+      copy(child = newChild)
+  }
+
+  test("a definition is evaluated once per row however many references read 
it") {
+    // `ref + ref` is the shape `BETWEEN` produces. Memoized, both references 
read one value, so the
+    // sum is 2n on the nth row; inlined it would be n + (n + 1).
+    val w = With(counter()) { case Seq(ref) => Add(ref, ref) }
+    assert((1 to 4).map(_ => w.eval(InternalRow.empty)) == Seq(2, 4, 6, 8))
+  }
+
+  test("a definition is not evaluated on a row that reaches no reference") {
+    val c = counter()
+    // The branch is inside the `With`, so the `With` is entered on every row 
and does clear its
+    // cells. What it must not do is evaluate the definition before a 
reference is reached; putting
+    // the branch outside would only test that `If` does not evaluate the arm 
it did not take.
+    val w = With(c) { case Seq(ref) => If(Literal.TrueLiteral, Literal(-1), 
Add(ref, ref)) }
+    assert((1 to 3).map(_ => w.eval(InternalRow.empty)) == Seq(-1, -1, -1))
+    // The counter is still at 0, so the first row that does reach a reference 
sees 1.
+    assert(With(c) { case Seq(ref) => Add(ref, ref) }.eval(InternalRow.empty) 
== 2)
+  }
+
+  test("a reference behind a short-circuiting operator is not read when the 
left side is false") {
+    // Neither pre-evaluating the definition into a project nor guarding that 
column by the branch
+    // can express this: both evaluate on every row the branch is reached on, 
while `And` short
+    // circuits before the reference. The `And` is inside the `With` so that 
the `With` is entered

Review Comment:
   **Nit:**
   
   `short-circuit` is a compound verb here.
   
   ```suggestion
       // can express this: both evaluate on every row the branch is reached 
on, while `And`
       // short-circuits before the reference. The `And` stays inside the 
`With` so it is entered
   ```



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/With.scala:
##########
@@ -38,11 +73,121 @@ case class With(child: Expression, defs: 
Seq[CommonExpressionDef])
   override def dataType: DataType = child.dataType
   override def nullable: Boolean = child.nullable
   override def children: Seq[Expression] = child +: defs
+
+  /**
+   * The references in `child` that name one of these definitions, paired with 
the definition each
+   * names. The list is found once, since the tree does not change between 
evaluations.
+   *
+   * Only `child` is scanned, which relies on a reference to one of these 
definitions never living
+   * inside another one of them: `With.apply` cannot build that, and 
`RewriteWithExpression`, which
+   * does rewrite inside a `With`, only ever replaces a reference with its 
definition's child or
+   * with an attribute -- it never puts a reference inside a definition. 
Scanning `children` instead
+   * would look safer and be worse -- it would also bind a reference sitting 
inside the definition
+   * it names, and since `CommonExpressionCell.get` sets `computed` only after 
the nested evaluation
+   * returns, that turns today's loud "outside its With" error into a 
StackOverflowError. A nested
+   * `With` is not affected either way: `children` is `child +: defs`, so this 
scan already descends
+   * into an inner `With`'s own definitions.
+   */
+  @transient private lazy val refsToBind: Seq[(CommonExpressionRef, 
CommonExpressionDef)] = {
+    val idToDef = defs.map(d => d.id -> d).toMap
+    child.collect { case r: CommonExpressionRef if idToDef.contains(r.id) => 
(r, idToDef(r.id)) }
+  }
+
+  /**
+   * Binds this `With`'s references to its own cells, clears them, and 
evaluates the child. A
+   * reference reached by that evaluation computes its definition once and 
every later reference
+   * reads the value back, so a definition is evaluated where the child would 
have evaluated it,
+   * once, rather than once per reference. See [[CommonExpressionCell]].
+   *
+   * The binding is redone on every evaluation rather than once, because a 
reference can be reached
+   * from two `With`s. `withNewChildrenInternal` cannot hand the new `With` 
its own references: a
+   * rebuilt reference compares equal to the one it replaces, since the 
binding it carries is not
+   * part of its equality, so `transform` keeps the original. Binding once 
would then leave the
+   * `With` that bound last deciding what both of them read. Rebinding costs 
one pass over the
+   * references, two for a `BETWEEN`, and makes the `With` currently 
evaluating always the owner.
+   */
+  override def eval(input: InternalRow): Any = {
+    refsToBind.foreach { case (ref, exprDef) => ref.bindTo(exprDef) }
+    defs.foreach(_.cell.clear())
+    child.eval(input)
+  }
+
+  // The cells are cleared on entry, so this holds state for the duration of 
one evaluation.
+  override def stateful: Boolean = true
+
+  /**
+   * Whether one of this `With`'s references sits somewhere that will be 
evaluated interpretively
+   * even though this `With` is generated. Two shapes do that: a 
[[CodegenFallback]], which is
+   * evaluated by calling `eval` on it from the generated code, and a nested 
`With` that itself
+   * takes the fallback below -- `With` does not mix in `CodegenFallback`, so 
it has to be named
+   * here rather than matched as one. A reference reached that way needs its 
cell bound and
+   * cleared, which the generated code does not do: it clears the codegen 
flags.
+   *
+   * This is the same shape `EquivalentExpressions.childrenToRecurse` already 
refuses to look past,
+   * for the same reason.
+   *
+   * Each level memoizes, but `holdsMyRef` runs again at every nested `With` 
the scan passes, so a
+   * chain of them nested in each other's `child` costs on the order of the 
square of the depth.
+   * `nullif(a, nullif(b, c))` does produce such a chain -- only the memoized 
input becomes a
+   * definition, the rest stays in `child` -- but these chains are shallow in 
practice. Reading a
+   * nested `With`'s own `lazy val` from here also takes its monitor while 
holding this one; the
+   * edges only ever run from an ancestor to a proper descendant of an 
immutable tree, so the order
+   * is a strict partial one and cannot deadlock. `canonicalizationIdMap` 
below relies on the same.
+   */
+  @transient private lazy val refUnderCodegenFallback: Boolean = {
+    val ids = defs.map(_.id).toSet
+    def holdsMyRef(e: Expression): Boolean = e.exists {
+      case r: CommonExpressionRef => ids.contains(r.id)
+      case _ => false
+    }
+    child.exists {
+      case f: CodegenFallback => holdsMyRef(f)
+      case w: With if w.refUnderCodegenFallback => holdsMyRef(w)
+      case _ => false
+    }
+  }
+
+  /**
+   * Clears each definition's flag, then generates the child. The flags are 
cleared in the same
+   * block the child is generated into, so a reference cannot run against a 
flag left set by an
+   * earlier row: on a row that does not reach the branch holding this `With`, 
neither the clearing
+   * nor any reference runs.
+   *
+   * When a reference sits under a [[CodegenFallback]], or inside a nested 
`With` that itself falls
+   * back, the whole `With` is evaluated interpretively instead. Generating 
the child would leave
+   * that reference reading a cell nobody bound and nobody clears, and 
generating part of it is
+   * worse still: a definition reached from both sides would be computed once 
through the flags and
+   * once through the cell, holding two values for one row. [[eval]] binds and 
clears both, so
+   * handing it the whole subtree keeps one mechanism in play. `ctx.INPUT_ROW` 
is available on that
+   * path because `CollapseCodegenStages.supportCodegen` turns whole-stage 
codegen off for a plan
+   * whose expressions hold the offending `CodegenFallback` -- it is visible 
there, since a `With`
+   * in a conditional branch reaches execution inside `plan.expressions` like 
any other expression.
+   */
+  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): 
ExprCode = {
+    if (refUnderCodegenFallback) {
+      return CodegenFallback.generate(this, ctx, ev)
+    }
+    ctx.withCommonExprs(defs) { slots =>
+      val clearFlags = slots.map(s => s"${s.computed} = false;").mkString("\n")
+      val childGen = child.genCode(ctx)
+      ev.copy(
+        code = code"""
+           |$clearFlags
+           |${childGen.code}
+         """.stripMargin,
+        isNull = childGen.isNull,
+        value = childGen.value)
+    }
+  }
+
   override protected def withNewChildrenInternal(
       newChildren: IndexedSeq[Expression]): Expression = {
     val newDefs = newChildren.tail.map(_.asInstanceOf[CommonExpressionDef])
     // If any `CommonExpressionDef` has been updated (data type or 
nullability), also update its
-    // `CommonExpressionRef` in the `child`.
+    // `CommonExpressionRef` in the `child`. This cannot be used to hand the 
new `With` its own
+    // reference objects: a rebuilt reference is `==` the one it replaces, 
since the binding it
+    // carries is not part of its equality, so `transform` keeps the original. 
`eval` rebinds
+    // instead of relying on the references being unshared -- see 
[[refsToBind]].

Review Comment:
   **Nit:**
   
   Scaladoc links are not parsed in `//` comments, so this remains literal 
bracket markup.
   
   ```suggestion
       // instead of relying on the references being unshared -- see 
`refsToBind`.
   ```



-- 
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