[SPARK-57370][SQL][FOLLOWUP] Route codegen referencing an unnameable class to Janino when narrowing is unsound - #57768
Draft
LuciferYang wants to merge 1 commit into
Draft
Conversation
…class to Janino when narrowing is unsound ### What changes were proposed in this pull request? Follow-up to apache#56430, addressing review comments from cloud-fan. 1. The JDK backend rewrote a reference to a class Java cannot name - an anonymous or local class, or a class nested inside one - into its nearest nameable supertype unconditionally. That is only sound when the supertype is itself referenceable and offers every member the generated code could access, and nothing enforced either condition. Such a unit is now routed to Janino instead, joining the two existing deterministic-routing cases (REPL contexts and Scala package-object classes). `canNarrowSafely` decides this. A member matches by its exact erased signature, with an allowance for bridges: an override of a generic method erases narrower than the supertype declaration it implements (`compare(String, String)` against `Comparator.compare(Object, Object)`) and carries a bridge with the supertype's signature, so narrowing keeps dispatching to the override. An overload has no bridge and is rejected - it must be, because `Invoke` codegen wraps every call in an explicit cast, which would hide the resulting type mismatch from javac and silently bind the call to the supertype's method. The replacement type and all its enclosing classes must also be public: same-package is not sufficient, because the generated class is loaded by `InMemoryClassLoader` and its runtime package differs from the same-named package on the application loader. `nameableSupertype` now climbs while `getCanonicalName` is null rather than while the class is anonymous or local, which additionally covers a named class nested inside one of those - neither anonymous nor local itself, yet equally unnameable. 2. Reflection in this path can raise a `LinkageError` when a class loads but a type in its signature does not (a partial or shaded jar). `NonFatal` does not cover that and an escaping `Error` would bypass the codegen fallbacks, so `canNarrowSafely` and `sourceNameOf` catch it and degrade to "cannot narrow" / the binary name. 3. Comment and documentation fixes: two wordings in the `CodeCompiler.active` scaladoc, and the `spark.sql.codegen.compiler` config doc, which enumerated only two always-Janino cases. ### Why are the changes needed? Fixes the unchecked assumption. Without it, an anonymous type whose accessed member is absent from the supertype either fails to compile or - for a same-arity overload - compiles clean and returns the supertype's answer. ### Does this PR introduce _any_ user-facing change? No. The default backend is Janino, and for `spark.sql.codegen.compiler=jdk` this only moves units that javac would have mishandled onto the working path. ### How was this patch tested? Eight cases in `CodeCompilerSuite` covering the `$`-digit gate, narrowable and unnarrowable shapes, an unnameable supertype, a class nested in a local class, the routing decision under both backend settings, an end-to-end compile asserting the JDK backend rejects the unit that narrowing breaks, and its counterpart asserting a bridge-preserved call stays on the JDK backend. Each production check was mutated in turn and the corresponding test fails without it. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: Claude Opus 5
LuciferYang
marked this pull request as draft
August 4, 2026 16:15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
Follow-up to #56430, addressing review comments from cloud-fan.
The JDK backend rewrote a reference to a class Java cannot name — an anonymous or local class, or a class nested inside one — into its nearest nameable supertype unconditionally. That is only sound when the supertype is itself referenceable and offers every member the generated code could access, and nothing enforced either condition. Such a unit is now routed to Janino instead, joining the two existing deterministic-routing cases (REPL contexts and Scala package-object classes).
canNarrowSafelydecides this. A member matches by its exact erased signature, with an allowance for bridges: an override of a generic method erases narrower than the supertype declaration it implements (compare(String, String)againstComparator.compare(Object, Object)) and carries a bridge with the supertype's signature, so narrowing keeps dispatching to the override. An overload has no bridge and is rejected — it must be, becauseInvokecodegen wraps every call in an explicit cast, which would hide the resulting type mismatch from javac and silently bind the call to the supertype's method. The replacement type and all its enclosing classes must also be public: same-package is not sufficient, because the generated class is loaded byInMemoryClassLoaderand its runtime package differs from the same-named package on the application loader.nameableSupertypenow climbs whilegetCanonicalNameis null rather than while the class is anonymous or local, which additionally covers a named class nested inside one of those — neither anonymous nor local itself, yet equally unnameable.Reflection in this path can raise a
LinkageErrorwhen a class loads but a type in its signature does not (a partial or shaded jar).NonFataldoes not cover that and an escapingErrorwould bypass the codegen fallbacks, socanNarrowSafelyandsourceNameOfcatch it and degrade to "cannot narrow" / the binary name.Also two comment wordings in the
CodeCompiler.activescaladoc, and thespark.sql.codegen.compilerconfig doc, which enumerated only two always-Janino cases.Why are the changes needed?
Fixes the unchecked assumption. Without it, an anonymous type whose accessed member is absent from the supertype either fails to compile or — for a same-arity overload — compiles clean and returns the supertype's answer.
Does this PR introduce any user-facing change?
No. The default backend is Janino, and for
spark.sql.codegen.compiler=jdkthis only moves units that javac would have mishandled onto the working path.How was this patch tested?
Eight cases in
CodeCompilerSuitecovering the$-digit gate, narrowable and unnarrowable shapes, an unnameable supertype, a class nested in a local class, the routing decision under both backend settings, an end-to-end compile asserting the JDK backend rejects the unit that narrowing breaks, and its counterpart asserting a bridge-preserved call stays on the JDK backend. Each production check was mutated in turn and the corresponding test fails without it.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 5