[code-reflection] RFR: Add isQuotable attribute to LambdaOp [v6]

Maurizio Cimadamore mcimadamore at openjdk.org
Wed Sep 10 18:19:07 UTC 2025


On Wed, 10 Sep 2025 11:52:58 GMT, Maurizio Cimadamore <mcimadamore at openjdk.org> wrote:

>> The change in this PR looks good. But, I'd like to remind ourselves that we have probably several issues when it comes to dealing with lambda expressions. There are a bunch of reasons where javac wants to use the `altMetafactory` instead of the regular one:
>> 
>> 1. when the target of the lambda has one or more "empty markers" -- e.g. `(Runnable & Cloneable) () -> ...`
>> 2. when the target of the lambda is serializable -- e.g. `(Runnable & Serializable) () -> ...
>> 3. when the metafactory needs to also implement one or more "bridge" methods
>> 
>> An example of (3) is as follows:
>> 
>> 
>> interface A<X> {
>>     void m(X x);
>> }
>> 
>> interface B<X extends Number> {
>>     void m(X x);
>> }
>> 
>> interface C extends A<Integer>, B<Integer> { }
>> 
>> C c = (i) -> System.out.println(i);
>> 
>> 
>> In the above example javac will ask the `altMetafactory` to generate an additional bridge implementation for the `(Ljava/lang/Object;)V` signature.
>> 
>> While there's things we can do to detect (1) and (2), I think the kind of analysis required for (3) is rather difficult, and compile-dependent:
>> 
>> https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L927
>> 
>> And I'm quite skeptical we can reproduce this at runtime (as the runtime capabilities to detect overriding based on source-level types are rather limited).
>> 
>> So, more generally, I think both `Interpreter` and `BytecodeGenerator` might have some issues in this area.
>
>> So, more generally, I think both `Interpreter` and `BytecodeGenerator` might have some issues in this area.
> 
> I tried something like this:
> 
> 
> class Test {
>     public interface A<X> {
>         void m(X x);
>     }
> 
>     public interface B<X extends Number> {
>         void m(X x);
>     }
> 
>     public interface C extends A<Integer>, B<Integer> { }
> 
>     @CodeReflection
>     static C getC() {
>         C c = i -> System.out.println(i);
>         return c;
>     }
> 
>     public static void main(String[] args) throws Throwable {
>         var method = Test.class.getDeclaredMethod("getC");
>         var op = (CoreOp.FuncOp) Op.ofMethod(method).get();
>         A<?> a = (A<?>)Interpreter.invoke(MethodHandles.lookup(), op);
>         a.m(null);
>     }
> }
> 
> 
> This (surprisingly) seems to work. I believe the reason is that `Interpreter` uses `MethodHandleProxies::asInterfaceInstance` -- whose javadoc says:
> 
> 
> * Because of the possibility of {@linkplain java.lang.reflect.Method#isBridge bridge methods}
>      * and other corner cases, the interface may also have several abstract methods
>      * with the same name but having distinct descriptors (types of returns and parameters).
>      * In this case, all the methods are bound in common to the one given target.
>      * The type check and effective {@code asType} conversion is applied to each
>      * method type descriptor, and all abstract methods are bound to the target in common.
>      * Beyond this type check, no further checks are made to determine that the
>      * abstract methods are related in any way.
> 
> 
> So, this special "fallback overrides" end up saving the day in this case.
> 
> However, a similar example using `BytecodeGenerator` fails:
> 
> 
> class Test {
>     public interface A<X> {
>         void m(X x);
>     }
> 
>     public interface B<X extends Number> {
>         void m(X x);
>     }
> 
>     public interface C extends A<Integer>, B<Integer> { }
> 
>     @CodeReflection
>     static void run() {
>         C c = i -> System.out.println(i);
>         A<?> a = (A<?>)c;
>         a.m(null);
>     }
> 
>     public static void main(String[] args) throws Throwable {
>         var method = Test.class.getDeclaredMethod("run");
>         var op = (CoreOp.FuncOp) Op.ofMethod(method).get();
>         var handle = BytecodeGenerator.generate(MethodHandles.lookup(), op);
>         handle.invokeExact();
>     }
> }
> 
> 
> The error I see is:
> 
> 
> WARNING: Using incubator modules: jdk.incubator.code
> Exception in thread "main" java.lang.AbstractMethodError: Receiver class run_0x0000000097069000$$Lambda/0x00000000970...

> @mcimadamore good points. Complete reverse engineering is hard. I also suspected that the bytecode generator does not produce correct bytecode behaviour for these cases, and i don't know if there is sufficient information held by the lambda op instance in these cases to generate the bytecode. Fixing both the generator and lifter together is important.

So... one thing I was positively surprised about was that, even after some extensive poking, `MethodHandleProxies` is indeed more resilient than it seems. It basically overrides any abstract methods it finds in superclasses.

To me, this suggests that we might be able to do something similar in `BytecodeGenerator` and just pass all the abstract signatures to the alternate metafactory so that they will be bridged. One complication is that when generating bytecode we might not have a `.class`, so there might not be a way for us to answer that question. But... if we can resolve the target interface to a `j,l.r.Type` then we can do the same analysis that `MethodHandleProxies` does, and call it a day.

With respect to the other issues I enumerated, my feeling is that what's missing from `LambdaOp` is that it should provide a "list" of all target types it wants to conform to. Not just one. This would allow you quickly to detect whether the lambda is serializable, quotable, or implementing marker interfaces, etc. So effectively you can implement the full spectrum of lambda expressions via code models -- perhaps with some minor incompatibility that nobody will care about.

-------------

PR Comment: https://git.openjdk.org/babylon/pull/545#issuecomment-3276029590


More information about the babylon-dev mailing list