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]

Reply via email to