codeconsole opened a new pull request, #3026:
URL: https://github.com/apache/groovy/pull/3026
Under `@CompileStatic`, a closure literal that goes to a functional
interface is still converted at run time. That covers a literal passed to a
single-abstract-method parameter, assigned to or returned as such a type, cast
to one, or coerced with `as`.
`ClosureWriter.writeClosure` leaves the new instance typed as `Closure` on
the operand stack. `OperandStack.doConvertAndCast` therefore emits an
invokedynamic `cast`, or `asType` for `as`. The runtime then wraps the closure
in a `java.lang.reflect.Proxy` with a `ConvertedClosure` handler
(`CachedSAMClass.coerceToSAM`, `TypeTransformers.createSAMTransform`), whose
every call goes through `Closure.call(Object...)` and a reflective dispatch to
`doCall`.
```groovy
@CompileStatic
int sum(int n) { IntStream.range(0, n).reduce(0) { int a, int b -> a + b } }
```
compiles to `invokedynamic
cast:(Lgroovy/lang/Closure;)Ljava/util/function/IntBinaryOperator;`, although
the same code written as a lambda uses `LambdaMetafactory`. In a GraalVM native
image, each such interface must be registered as a proxy even though the code
is statically compiled. Without that, the conversion fails with
`GroovyCastException`. GROOVY-11998 already declares extra interfaces on
closure classes for intersection casts, but only marker interfaces.
## The option
This adds an incubating option, `-Dgroovy.target.closure.sam=true`
(`CompilerConfiguration.CLOSURE_SAM_CONVERSION`), that makes the conversion at
compile time. When the type checker recorded the interface a closure literal is
converted to (the `PARAMETER_TYPE` metadata), `StaticTypesClosureWriter` gives
the closure class an adapter factory and types the new instance as that class.
Converting it to the interface then wraps it in `<closure class>$SamAdapter`, a
class generated the first time a conversion needs it.
The adapter does what the proxy does:
- it implements the interface and `Serializable`;
- it is neither a `Closure` nor a `GroovyObject`;
- its `toString` is the closure's;
- it calls `call(Object...)` and converts the result with `castToType`, so
`Predicate` still applies Groovy truth;
- it unwraps a `GroovyRuntimeException`, and wraps a checked exception the
method does not declare in an `UndeclaredThrowableException`.
When every argument already has `doCall`'s parameter type, or is `null` for
a reference type, and no parameter is an array, it calls `doCall` directly.
That is exactly when `call(Object...)` would pass the arguments through
unchanged, so the result is the same without reflection. When the interface
binds a super-interface's type arguments, as `interface Shout extends
Function<String,String>` does, the erased declarations get bridges with the
same body.
Conversions to any other type are unaffected, so a closure whose value
actually goes to `Closure` or `Object` stays a closure. These keep the run-time
conversion:
- a trait or abstract class as the target;
- an interface with a default method of the SAM's name and arity;
- a closure written in a trait method;
- a closure chosen by a conditional or Elvis expression;
- a closure that `groovy.target.closure.pack` packs;
- `Runnable` and `Callable`, which a closure already implements.
## What changes with the option on
The class of the converted value is the visible difference, which is why the
option is off by default, like `groovy.target.closure.pack` and
`groovy.target.lambda.hoist`:
- `Proxy.isProxyClass` is false, and `Proxy.getInvocationHandler` throws, so
code that dug the closure out through `ConvertedClosure` can no longer do so;
- class names in stack traces and exception messages differ;
- `equals` and `hashCode` are identity rather than `ConversionHandler`'s;
- the serialized form names the adapter class;
- on the direct path, a meta-class customization that intercepts `doCall` on
the generated closure class is not consulted;
- each converted literal adds one small class.
## Alternatives considered
- **Making the closure class implement the interface.** I implemented this
first. `ClosureMetaClass` only dispatches `doCall` and `call`, so dynamic
callers of the interface method failed with `MissingMethodException`:
`[f]*.apply(x)`, `InvokerHelper.invokeMethod(f, 'apply', x)`, `f.&apply`,
`f.with { apply(x) }`. It also made the value a `Closure`, which changes `case
f:`, `grep(f)` and `f.andThen { }`, and SAM methods named `call`, `run` or
`andThen` collide with `Closure`'s.
- **Compiling the closure as a lambda.** The value would no longer be
`Serializable`, `toString` would change, `LambdaMetafactory`'s argument
`checkcast` would stop a dynamic caller's `GString` from being coerced to
`String`, and the body would need translating from closure semantics: implicit
`it`, arity adaptation, default parameters, list destructuring, `owner` and
`delegate`.
## Tests
`StaticCompileClosureSamTest` (19 tests; 15 fail without the change) checks
the bytecode, which has no invokedynamic `cast` and no `asType` but a `new
…$SamAdapter` before the call. It checks against today's behavior:
- generic interfaces with inferred parameters;
- `GString` coercion, `null`, and `MissingMethodException` for a wrong
argument type;
- primitives and boxing, including `long` and `double` slots;
- array and varargs parameters;
- implicit `it`, zero-argument `Supplier`, default parameters, destructuring;
- result conversion (`Predicate`, void, `Comparator`), default methods, and
bridged interfaces called through the erased method;
- `owner`, `delegate` and `thisObject`, recursion, nested closures,
captured-variable mutation;
- serialization round trip;
- unchecked, declared and undeclared checked exceptions, and
`GroovyRuntimeException` unwrapping;
- dynamic callers: `*.`, `InvokerHelper`, `.&`, `with`, `andThen { }`,
`grep`;
- SAM methods named `call`, `run` and `andThen`;
- the cases that keep the run-time conversion, and the interplay with
closure packing.
The closure, coercion, lambda, method-reference, STC/SC, classgen,
transform, runtime and bugs suites pass with the option off and on (10,648
tests each). The build forwards the option to the test JVM and to the compiler
as it does `closure.pack`, so the flag-on run also compiled Groovy's own Groovy
sources and the test scripts with it; an instrumented run loaded 203 adapter
classes. The Checkstyle and CodeNarc gates pass.
A native image (GraalVM CE 25.0.4) of a statically compiled program using
`IntStream.reduce`, an `IntBinaryOperator` variable and a `Function` variable,
with only the metadata the Groovy jar ships, fails at the conversion with
`GroovyCastException` without the option and runs with it. In a rough JVM loop,
two interface calls took about 26 ns through the proxy and 2.8 ns through the
adapter.
The native-image section of the invokedynamic guide describes the option.
## Backport
Grails 9 builds with Groovy 6.0.x. Since the option is off by default, a
backport to `GROOVY_6_0_X` would change nothing unless it is set, and would let
Grails compile its statically compiled code without these proxies.
There is no JIRA issue for this yet.
--
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]