dongjoon-hyun commented on code in PR #58881:
URL: https://github.com/apache/spark/pull/58881#discussion_r4449324648
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1926,6 +1935,19 @@ class CodegenContext extends Logging {
}
}
+ /**
+ * Under testing, asserts that each of `parameters`, the parameters of the
method `method` an
+ * operator moves its code into, is a Java name. The parameters are the
variables
+ * `InputPolicy.operatorMethod` collects, which passes a `VariableValue`
whatever its name.
+ */
+ private[sql] def assertParametersAreNames(parameters: Seq[String], method:
String): Unit = {
Review Comment:
**No test fails if one of the three calls to this check is removed.**
`CollectInputsSuite` calls `assertParametersAreNames` directly (L177-180,
L192), but no test reaches the calls at L2245, ExpandExec.scala:241 or
AggregateCodegenSupport.scala:354 with a value that is not a name. Every
producer in the tree hands out names, so if a later change drops one of these
calls, or moves it out of the branch that builds the method, CI stays green.
The next producer that hands out a slot as a `VariableValue` then gets a method
like `doAggregate_sum(UTF8String x_mutableStateArray_0[1])` in production
again. The check you ran, with `ExpandExec` handing out `JavaCode.variable`
again, is not kept in any test.
The `subExpr` call can be pinned in catalyst without an operator:
`subexpressionEliminationForWholeStageCodegen` with `currentVars` holding a
slot `VariableValue` and a split threshold of 1, expecting an `AssertionError`
that names `subExpr`. For example, in `CollectInputsSuite` (I traced this by
reading the code and did not run it):
```scala
test("a subexpression method that takes a slot fails the testing check") {
withSQLConf(SQLConf.CODEGEN_METHOD_SPLIT_THRESHOLD.key -> "1") {
val ctx = context()
val slot = ctx.addMutableState("UTF8String", "value")
val input = ExprCode(EmptyBlock, FalseLiteral, JavaCode.variable(slot,
StringType))
ctx.currentVars = Seq(input)
val common = Upper(BoundReference(0, StringType, nullable = false))
val e = intercept[AssertionError] {
ctx.subexpressionEliminationForWholeStageCodegen(Seq(Concat(Seq(common,
common))))
}
assert(e.getMessage.contains("subExpr") &&
e.getMessage.contains(slot), e.getMessage)
}
}
```
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1322,23 +1326,25 @@ class CodegenContext extends Logging {
val args = mutable.LinkedHashMap.empty[String, VariableValue]
val inputsToEvaluate = mutable.ArrayBuffer.empty[ExprCode]
// Whether the code can read `v` from the method: false for a value no
parameter can carry
- // where the policy asks for names. `ExpandExec` hands out a
`VariableValue` naming a slot of a
- // compacted mutable state array, and a `SimpleExprValue` is an expression
rather than a name
+ // where the policy asks for names. A `SimpleExprValue` is an expression
rather than a name
// -- `posexplode_outer` gives its position the nullness `index == -1`,
and a `Byte` or `Short`
// literal's value is `(byte)1`. A field or a literal needs no parameter
and is read as it
- // stands; so is a field handed out as a `VariableValue` where the policy
says so, passing one
- // being refused by the split methods' own check, a slot of a compacted
array included.
+ // stands; so is a `VariableValue` naming a field `addMutableState`
registered, where the
+ // policy says so, passing one being refused by the whole stage split's
own check (`splitCall`)
+ // in tests. A slot of a compacted array is not a name at all, so it has
to be handed out as a
+ // `GlobalValue`, and `INPUT_ROW` cannot be one. Testing checks for one
handed out as a
+ // variable: here, where the policy asks for names; elsewhere, in the
method taking it
+ // (`assertParametersAreNames`).
def take(v: ExprValue): Boolean = v match {
case local: VariableValue =>
val name = local.variableName
- val isName = name.nonEmpty &&
Character.isJavaIdentifierStart(name.head) &&
- name.forall(Character.isJavaIdentifierPart)
- // A slot of a compacted array is a field under every policy, since no
parameter can be
- // named `array[i]`, where the whole stage split is on; with it off,
every policy answers
- // as it did before that split existed. Another field is passed where
the policy says so.
- val isField = (isCompactedSlot(name) && wholeStageSplit !=
WholeStageSplit.Off) ||
- (policy.fieldsNeedNoArgument && mutableStateNames.contains(name))
- val taken = isName || isField || !policy.onlyNames
+ val isName = isJavaName(name)
+ if (Utils.isTesting && policy.onlyNames) {
Review Comment:
**Whether a mislabeled slot fails a test depends on the order of the
inputs.** `take` runs only while `possible` holds: the loop at L1363 stops at
the first refusal, and `take(input.value) && take(input.isNull)` at L1390 skips
the null flag once the value is refused. So under `commonExpressionMethod`, a
definition `Add(slot, deferred)`, where `deferred` is an input the operator has
not evaluated yet, pops `deferred` first, sets `possible = false`, and never
reaches the slot, so there is no `AssertionError`. `Add(deferred, slot)` does
fail the test. In the same way, `wholeStageSplitPossible` (L1680) stops at the
first piece that can be split, so a slot in a later piece is only reported when
the Split pass runs.
Production refuses the method either way, so nothing breaks, but the check
can miss a mislabeled producer depending on the operand order. Making it
complete would mean walking on after a refusal, which may not be worth it.
Could the comment above at least say that the check covers what the walk
reaches before its first refusal?
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1322,23 +1326,25 @@ class CodegenContext extends Logging {
val args = mutable.LinkedHashMap.empty[String, VariableValue]
val inputsToEvaluate = mutable.ArrayBuffer.empty[ExprCode]
// Whether the code can read `v` from the method: false for a value no
parameter can carry
- // where the policy asks for names. `ExpandExec` hands out a
`VariableValue` naming a slot of a
- // compacted mutable state array, and a `SimpleExprValue` is an expression
rather than a name
+ // where the policy asks for names. A `SimpleExprValue` is an expression
rather than a name
// -- `posexplode_outer` gives its position the nullness `index == -1`,
and a `Byte` or `Short`
// literal's value is `(byte)1`. A field or a literal needs no parameter
and is read as it
- // stands; so is a field handed out as a `VariableValue` where the policy
says so, passing one
- // being refused by the split methods' own check, a slot of a compacted
array included.
+ // stands; so is a `VariableValue` naming a field `addMutableState`
registered, where the
+ // policy says so, passing one being refused by the whole stage split's
own check (`splitCall`)
+ // in tests. A slot of a compacted array is not a name at all, so it has
to be handed out as a
+ // `GlobalValue`, and `INPUT_ROW` cannot be one. Testing checks for one
handed out as a
+ // variable: here, where the policy asks for names; elsewhere, in the
method taking it
+ // (`assertParametersAreNames`).
Review Comment:
**Nit:** "`INPUT_ROW` cannot be one. Testing checks for one handed out as a
variable: here, ...; elsewhere, in the method taking it" reads as if every such
value were checked. But `wholeStageSplitArguments` (L1700) appends `INPUT_ROW`
without going through `take` or any other check, which is the gap you filed as
SPARK-60510. So a slot `INPUT_ROW` read by a CASE WHEN block becomes a
parameter in `splitCall`, and no testing check sees it. `@param onlyNames`
(L126-128) says the same ("fails testing either way"), and its "a variable the
walk takes" is a value this policy refuses rather than takes. Could both
mention the exception until SPARK-60510 lands? For example:
```suggestion
// `GlobalValue`, and `INPUT_ROW` cannot be one. Testing checks for one
handed out as a
// variable: here, where the policy asks for names; elsewhere, in the
method taking it
// (`assertParametersAreNames`), except the row a whole stage split
appends (SPARK-60510).
```
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -1411,13 +1417,16 @@ class CodegenContext extends Logging {
}
}
- /**
- * Whether `name` is a slot of a compacted mutable state array, such as
`mutableStateArray_0[3]`,
- * which `addMutableState` hands out for a state it does not inline: a field
of the class.
- */
- private def isCompactedSlot(name: String): Boolean = name match {
- case CodeGenerator.CompactedSlot(array) =>
mutableStateNames.contains(array)
- case _ => false
+ /** Whether `name` can name a parameter: a slot of a compacted array,
`array[i]`, cannot. */
+ private def isJavaName(name: String): Boolean =
+ name.nonEmpty && Character.isJavaIdentifierStart(name.head) &&
+ name.forall(Character.isJavaIdentifierPart)
Review Comment:
**Nit:** The JDK already has this check:
`javax.lang.model.SourceVersion.isIdentifier`, which `ScalaReflection` uses for
the same purpose at ScalaReflection.scala:405-406, together with
`SourceVersion.isKeyword`. The check here accepts keywords and literals such as
`this`, `int` or `null`, which cannot name a parameter either, and it looks at
UTF-16 chars rather than code points. For example:
```suggestion
private def isJavaName(name: String): Boolean =
SourceVersion.isIdentifier(name) && !SourceVersion.isKeyword(name)
```
This needs `import javax.lang.model.SourceVersion`. No producer hands out
such a name today, so this is only a cleanup.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/aggregate/AggregateCodegenSupport.scala:
##########
@@ -351,6 +351,7 @@ trait AggregateCodegenSupport
if (inputVars.forall(_.isDefined)) {
val splitCodes = inputVars.flatten.zipWithIndex.map { case (args, i) =>
val doAggFunc = ctx.freshName(s"doAggregate_${aggNames(i)}")
+ ctx.assertParametersAreNames(args.map(_.variableName), doAggFunc)
Review Comment:
**Nit:** This line and the next are now the same pair of testing checks,
with the same parameter names and method, at all three places an operator
builds a method from `getLocalInputVariableValues`: here,
CodeGenerator.scala:2245-2246 and ExpandExec.scala:241-242. A fourth such place
has to remember both, and missing the new one loses the name check without any
test noticing (see the comment on CodeGenerator.scala L1943). Could they be one
testing helper, e.g. `ctx.assertMethodParameters(body, parameters, method)`
running both, or could `assertSplitCallsWithin` check the names as well? The
`computeCommonExpr` call at CodeGenerator.scala:428 would then check names that
`collectInputs` already checked, which is harmless.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/MergeRowsExec.scala:
##########
@@ -156,7 +156,7 @@ case class MergeRowsExec(
|}
|$rowIdBitmap.add(${currentRowId.value});
""".stripMargin
- ExprCode(code, FalseLiteral, JavaCode.variable(rowIdBitmap, bitmapClass))
+ ExprCode(code, FalseLiteral, JavaCode.global(rowIdBitmap, bitmapClass))
Review Comment:
**Nit:** The value of this `ExprCode` is never read: the only caller, L184,
takes `.code`. So this relabel changes no generated code and no input
collection, and no test can observe it. The PR description says `MergeRowsExec`
"hands out its row-id bitmap, also a slot, with `JavaCode.global` as well",
which reads as a second producer fix that has to travel with the Expand one,
e.g. in a backport. Could this line go, to keep the change to the fix, or could
the description say it is for consistency only?
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/codegen/CollectInputsSuite.scala:
##########
@@ -139,29 +139,58 @@ class CollectInputsSuite extends SparkFunSuite with
SQLHelper {
assert(!ctx.collectInputs(Seq(input(0)), operator, Map.empty).get.readsRow)
}
- test("a slot of a compacted mutable state array is a field under every
policy with the whole " +
- "stage split on, never a parameter, which no method could declare") {
+ test("a field handed out as a global is never an argument; an inlined one
handed out as a " +
+ "variable is left out only where the policy says so") {
val ctx = context()
- ctx.wholeStageSplit = WholeStageSplit.Record
- // A state of a type that is not primitive is compacted into an array, and
its name is a slot.
+ val expr = Length(BoundReference(0, StringType, nullable = false))
+ // A state of a type that is not primitive is a slot of a compacted array.
val slot = ctx.addMutableState("UTF8String", "value")
assert(slot.matches("\\w+\\[\\d+\\]"), slot)
- ctx.currentVars = Seq(ExprCode(EmptyBlock, FalseLiteral,
JavaCode.variable(slot, StringType)))
- val expr = Length(BoundReference(0, StringType, nullable = false))
+ ctx.currentVars = Seq(ExprCode(EmptyBlock, FalseLiteral,
JavaCode.global(slot, StringType)))
for (policy <- Seq(InputPolicy.wholeStageSplit, commonExpr, operator)) {
assert(names(ctx.collectInputs(Seq(expr), policy,
Map.empty).get).isEmpty, policy)
}
+ // An inlined state handed out as a variable is read as the field only by
a whole stage split
+ // method.
+ val field = ctx.addMutableState("UTF8String", "field", forceInline = true)
+ ctx.currentVars = Seq(ExprCode(EmptyBlock, FalseLiteral,
JavaCode.variable(field, StringType)))
+ assert(names(ctx.collectInputs(Seq(expr), InputPolicy.wholeStageSplit,
Map.empty).get).isEmpty)
+ for (policy <- Seq(commonExpr, operator)) {
+ assert(names(ctx.collectInputs(Seq(expr), policy, Map.empty).get) ==
Seq(field), policy)
+ }
}
- test("with the whole stage split off, a slot is answered as before that
split existed") {
- // A common expression's method refuses it, so its definition stays
inline, and an operator's
- // method lists it among its inputs.
+ test("a slot handed out as a variable fails the testing check: while
collecting where the " +
+ "policy asks for names, and in the method that takes it where it does
not") {
val ctx = context()
val slot = ctx.addMutableState("UTF8String", "value")
ctx.currentVars = Seq(ExprCode(EmptyBlock, FalseLiteral,
JavaCode.variable(slot, StringType)))
val expr = Length(BoundReference(0, StringType, nullable = false))
- assert(ctx.collectInputs(Seq(expr), commonExpr, Map.empty).isEmpty)
- assert(names(ctx.collectInputs(Seq(expr), operator, Map.empty).get) ==
Seq(slot))
+ for (policy <- Seq(InputPolicy.wholeStageSplit, commonExpr)) {
+ val e = intercept[AssertionError](ctx.collectInputs(Seq(expr), policy,
Map.empty))
+ assert(e.getMessage.contains(s"$slot cannot name a parameter") &&
+ e.getMessage.contains("JavaCode.global"), e.getMessage)
+ }
+ // An operator may keep its code inline, so collecting does not check.
+ val parameters = names(ctx.collectInputs(Seq(expr), operator,
Map.empty).get)
+ assert(parameters == Seq(slot))
+ val e = intercept[AssertionError](ctx.assertParametersAreNames(parameters,
"m"))
+ assert(e.getMessage.contains(s"m takes $slot") &&
e.getMessage.contains("JavaCode.global"),
+ e.getMessage)
+ ctx.assertParametersAreNames(Seq("value_0", "isNull_0"), "m")
+ }
+
+ test("an INPUT_ROW that is a slot fails the testing check: while collecting
for a common " +
+ "expression's method, and in an operator's method that takes it") {
+ val ctx = context()
+ ctx.INPUT_ROW = ctx.addMutableState("InternalRow", "row")
Review Comment:
**Nit:** This test relies on `addMutableState("InternalRow", "row")`
returning a slot, but unlike the test above (L147-148) it does not assert that.
If that state were ever inlined, both `intercept`s would fail with "Expected
exception java.lang.AssertionError to be thrown, but no exception was thrown",
which points at the check rather than at the setup. For example:
```suggestion
ctx.INPUT_ROW = ctx.addMutableState("InternalRow", "row")
assert(ctx.INPUT_ROW.matches("\\w+\\[\\d+\\]"), ctx.INPUT_ROW)
```
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/WithExpressionEvalSuite.scala:
##########
@@ -305,18 +305,18 @@ class WithExpressionEvalSuite extends SparkFunSuite with
SQLHelper {
s"the innermost body was emitted $count times at depth $depth")
assert(!methods.contains("computeCommonExpr"), methods)
}
- // A value whose name is not one a parameter can take: `ExpandExec`
hands out a compacted
- // mutable state array slot as if it were a local. The marker count says
the fallback pasted
- // the body at both references; that a method was attempted at all is
the case above, which
- // gets one from the same shape with a passable name.
+ // A value whose name is not one a parameter can take, such as a slot of
a compacted mutable
+ // state array handed out as a local. Its producer has to hand it out as
a field, and tests
+ // fail where a method would be built; the previous test gets a method
from the same shape
+ // with a passable name.
val slot = ExprCode(EmptyBlock, FalseLiteral,
JavaCode.variable("mutableStateArray_0[3]", IntegerType))
- val (slotBody, slotMethods) = generateWithLocalInput(nested(2), slot)
- assert(!slotMethods.contains("computeCommonExpr"), slotMethods)
- assert(markersIn(slotBody + slotMethods) == 4, slotBody + slotMethods)
+ val e = intercept[AssertionError](generateWithLocalInput(nested(2),
slot))
Review Comment:
**Nit:** This test is titled "the shapes a method is not possible for keep
the inline body", but this case now expects an `AssertionError` instead, and
its `markersIn(...) == 4` check is gone. The inline fallback for a value no
parameter can name is now covered only by the `position` case below. Could the
slot case move into a test of its own, e.g. "a slot handed out as a local fails
the testing check where a definition's method is considered", so that this
title stays true?
--
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]