Skip to content

[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
apache:masterfrom
LuciferYang:spark-57370-narrowing-followup
Draft

[SPARK-57370][SQL][FOLLOWUP] Route codegen referencing an unnameable class to Janino when narrowing is unsound#57768
LuciferYang wants to merge 1 commit into
apache:masterfrom
LuciferYang:spark-57370-narrowing-followup

Conversation

@LuciferYang

@LuciferYang LuciferYang commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

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.

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.

Also two comment 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

…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
LuciferYang marked this pull request as draft August 4, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant