This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 1f28e07b60d3f46b29a6b7cd133dd47c750c90ef
Author: Paul King <[email protected]>
AuthorDate: Wed May 6 18:07:10 2026 +1000

    GROOVY-11998: Better support of intersection types (part 4)
    as coercion. Closure asType overload, ProxyGenerator plumbing, dynamic 
castToType(Object, Class[]).
---
 .../groovy/classgen/AsmClassGenerator.java         |  52 ++++++-
 .../groovy/runtime/IntersectionCastSupport.java    | 103 ++++++++++++
 .../groovy/lang/IntersectionCoercionTest.groovy    | 173 +++++++++++++++++++++
 3 files changed, 325 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java 
b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
index c98b943da6..4680db0ce3 100644
--- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
+++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
@@ -1005,15 +1005,20 @@ public class AsmClassGenerator extends ClassGenerator {
 
         // GROOVY-11998: lambda / method-reference factory invocations already
         // emit an object that implements every component of an intersection
-        // target via altMetafactory FLAG_MARKERS, so the outer cast is a no-op
-        // here. Non-functional intersection casts are out of scope for PR3
-        // and fall through to the default handling below.
+        // target via altMetafactory FLAG_MARKERS, so the outer cast is a 
no-op.
         if (type instanceof IntersectionTypeClassNode
                 && (expression instanceof LambdaExpression
                     || expression instanceof MethodReferenceExpression)) {
             expression.visit(this);
             return;
         }
+        // GROOVY-11998: non-functional intersection casts route through the
+        // runtime helper IntersectionCastSupport, which strict-casts for `()`
+        // and may build a multi-interface proxy via ProxyGenerator for `as`.
+        if (type instanceof IntersectionTypeClassNode) {
+            emitIntersectionCastCall((IntersectionTypeClassNode) type, 
expression, castExpression.isCoerce());
+            return;
+        }
 
         expression.visit(this);
 
@@ -1033,6 +1038,47 @@ public class AsmClassGenerator extends ClassGenerator {
         }
     }
 
+    /**
+     * Emits a call to {@link 
org.codehaus.groovy.runtime.IntersectionCastSupport}
+     * for an intersection-target cast or coercion on a non-functional source.
+     *
+     * Bytecode layout:
+     * <pre>
+     *   visit(source)             // pushes source on the JVM stack
+     *   bipush/iconst N           // component count
+     *   anewarray Class           // create Class[N]
+     *   for each component i:
+     *     dup; bipush i; ldc Type; aastore
+     *   invokestatic IntersectionCastSupport.{castTo|asType}(Object, Class[]) 
Object
+     * </pre>
+     */
+    private void emitIntersectionCastCall(final IntersectionTypeClassNode it, 
final Expression source, final boolean coerce) {
+        source.visit(this); // leaves source on the operand / JVM stack as 
Object-ish
+        controller.getOperandStack().box();   // ensure boxed reference 
(handles primitive sources)
+
+        MethodVisitor mv = controller.getMethodVisitor();
+        ClassNode[] components = it.getComponents();
+
+        BytecodeHelper.pushConstant(mv, components.length);
+        mv.visitTypeInsn(ANEWARRAY, "java/lang/Class");
+        for (int i = 0; i < components.length; i += 1) {
+            mv.visitInsn(DUP);
+            BytecodeHelper.pushConstant(mv, i);
+            BytecodeHelper.visitClassLiteral(mv, components[i]);
+            mv.visitInsn(AASTORE);
+        }
+
+        mv.visitMethodInsn(
+            INVOKESTATIC,
+            "org/codehaus/groovy/runtime/IntersectionCastSupport",
+            coerce ? "asType" : "castTo",
+            "(Ljava/lang/Object;[Ljava/lang/Class;)Ljava/lang/Object;",
+            false
+        );
+
+        controller.getOperandStack().replace(ClassHelper.OBJECT_TYPE);
+    }
+
     @Override
     public void visitNotExpression(final NotExpression expression) {
         controller.getUnaryExpressionHelper().writeNotExpression(expression);
diff --git 
a/src/main/java/org/codehaus/groovy/runtime/IntersectionCastSupport.java 
b/src/main/java/org/codehaus/groovy/runtime/IntersectionCastSupport.java
new file mode 100644
index 0000000000..06eec2654b
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/runtime/IntersectionCastSupport.java
@@ -0,0 +1,103 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.codehaus.groovy.runtime;
+
+import groovy.lang.Closure;
+import groovy.util.ProxyGenerator;
+import org.codehaus.groovy.runtime.typehandling.GroovyCastException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Runtime support for intersection-type cast and {@code as} coercion
+ * (GROOVY-11998). Compiler-generated bytecode for {@code (A & B) value} and
+ * {@code value as (A & B)} routes through these helpers when the target is
+ * an intersection type and the source is not a native lambda or method
+ * reference (those cases are handled at compile time via
+ * {@code LambdaMetafactory.altMetafactory} markers).
+ *
+ * @since 5.0.0
+ */
+public final class IntersectionCastSupport {
+
+    private IntersectionCastSupport() {}
+
+    /**
+     * Strict cast: every component must already be assignment-compatible with
+     * the source's runtime class. Throws {@link GroovyCastException} 
otherwise.
+     */
+    public static Object castTo(final Object source, final Class<?>[] 
components) {
+        if (source == null) return null;
+        for (Class<?> c : components) {
+            if (!c.isInstance(source)) {
+                throw new GroovyCastException(source, c);
+            }
+        }
+        return source;
+    }
+
+    /**
+     * Coercion: produce an object that satisfies every component. For
+     * {@link Closure} and {@link Map} sources, a multi-interface proxy is
+     * built via {@link ProxyGenerator}. For other sources, falls back to
+     * {@link #castTo} so the behaviour is at least as strict as a cast.
+     */
+    public static Object asType(final Object source, final Class<?>[] 
components) {
+        if (source == null) return null;
+
+        if (isInstanceOfAll(source, components)) {
+            return source;
+        }
+
+        Class<?> baseClass = pickBaseClass(components);
+        @SuppressWarnings({"unchecked", "rawtypes"})
+        List<Class> ifaces = (List) filterInterfaces(components);
+
+        if (source instanceof Closure) {
+            Map<String, Closure> closureMap = new HashMap<>();
+            closureMap.put("*", (Closure<?>) source); // wildcard: all SAM 
calls go to the closure
+            return ProxyGenerator.INSTANCE.instantiateAggregate(closureMap, 
ifaces, baseClass, null);
+        }
+        if (source instanceof Map) {
+            return ProxyGenerator.INSTANCE.instantiateAggregate((Map) source, 
ifaces, baseClass, null);
+        }
+
+        // Strict fallback: throws GroovyCastException with a useful message
+        return castTo(source, components);
+    }
+
+    private static boolean isInstanceOfAll(final Object source, final 
Class<?>[] components) {
+        for (Class<?> c : components) if (!c.isInstance(source)) return false;
+        return true;
+    }
+
+    private static Class<?> pickBaseClass(final Class<?>[] components) {
+        for (Class<?> c : components) if (!c.isInterface()) return c;
+        return null;
+    }
+
+    private static List<Class<?>> filterInterfaces(final Class<?>[] 
components) {
+        List<Class<?>> out = new ArrayList<>(components.length);
+        for (Class<?> c : components) if (c.isInterface()) out.add(c);
+        return out;
+    }
+}
diff --git a/src/test/groovy/groovy/lang/IntersectionCoercionTest.groovy 
b/src/test/groovy/groovy/lang/IntersectionCoercionTest.groovy
new file mode 100644
index 0000000000..f4e31d3459
--- /dev/null
+++ b/src/test/groovy/groovy/lang/IntersectionCoercionTest.groovy
@@ -0,0 +1,173 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.lang
+
+import org.codehaus.groovy.runtime.typehandling.GroovyCastException
+import org.junit.jupiter.api.Test
+
+import static org.junit.jupiter.api.Assertions.assertThrows
+
+/**
+ * Tests for runtime intersection-type cast and {@code as} coercion on
+ * non-functional sources (GROOVY-11998 PR4).
+ *
+ * The functional-source cases (lambdas / method references) are covered
+ * by {@code IntersectionCastE2ETest}; this suite focuses on the dynamic
+ * runtime path that goes through {@code IntersectionCastSupport}.
+ */
+final class IntersectionCoercionTest {
+
+    @Test
+    void 'dynamic strict cast accepts a value that already implements all 
components'() {
+        def shell = new GroovyShell()
+        def result = shell.evaluate('''
+            class Both implements Runnable, Cloneable {
+                void run() {}
+            }
+            def b = new Both()
+            return (Runnable & Cloneable) b
+        ''')
+        assert result instanceof Runnable
+        assert result instanceof Cloneable
+    }
+
+    @Test
+    void 'dynamic strict cast throws on missing component'() {
+        def shell = new GroovyShell()
+        def thrown = assertThrows(GroovyCastException) {
+            shell.evaluate('''
+                def s = "hello"
+                return (Runnable & java.io.Serializable) s
+            ''')
+        }
+        // String is Serializable but not Runnable
+        assert thrown.message.contains('Runnable') || 
thrown.message.contains('Cannot cast')
+    }
+
+    @Test
+    void 'dynamic as on a closure builds a proxy implementing all 
interfaces'() {
+        def shell = new GroovyShell()
+        def result = shell.evaluate('''
+            interface Greeter { String greet(String name) }
+            def proxy = ({ String n -> "Hello, " + n } as (Greeter & 
java.io.Serializable))
+            return [proxy, proxy.greet("world")]
+        ''')
+        def (proxy, greeting) = result
+        assert proxy instanceof java.io.Serializable
+        // The proxy's class implements Greeter via ProxyGenerator
+        assert proxy.class.interfaces.any { it.name == 'Greeter' }
+        assert greeting == 'Hello, world'
+    }
+
+    @Test
+    void 'dynamic as on a closure that already implements all components is 
identity'() {
+        // Closure already implements Runnable AND Serializable, so `as 
(Runnable & Serializable)`
+        // should not wrap; identity coercion.
+        def shell = new GroovyShell()
+        def result = shell.evaluate('''
+            def cl = { -> 1 }
+            def coerced = cl as (Runnable & java.io.Serializable)
+            return coerced.is(cl)
+        ''')
+        assert result == true
+    }
+
+    @Test
+    void 'dynamic as on a map builds a proxy implementing all interfaces'() {
+        def shell = new GroovyShell()
+        def result = shell.evaluate('''
+            interface Action { void perform() }
+            def calls = 0
+            def proxy = ([perform: { -> calls++ }] as (Action & 
java.io.Serializable))
+            proxy.perform()
+            proxy.perform()
+            return [proxy, calls]
+        ''')
+        def (proxy, calls) = result
+        assert proxy instanceof java.io.Serializable
+        assert proxy.class.interfaces.any { it.name == 'Action' }
+        assert calls == 2
+    }
+
+    @Test
+    void 'static cast on a value that already implements all components 
passes'() {
+        def shell = new GroovyShell()
+        def result = shell.evaluate('''
+            import groovy.transform.CompileStatic
+
+            class Both implements Runnable, Cloneable {
+                void run() {}
+            }
+
+            @CompileStatic
+            class T {
+                static Object make() {
+                    Both b = new Both()
+                    return (Runnable & Cloneable) b
+                }
+            }
+            T.make()
+        ''')
+        assert result instanceof Runnable
+        assert result instanceof Cloneable
+    }
+
+    @Test
+    void 'static as coercion on a closure builds a multi-interface proxy'() {
+        def shell = new GroovyShell()
+        def result = shell.evaluate('''
+            import groovy.transform.CompileStatic
+            interface Greeter { String greet(String name) }
+            @CompileStatic
+            class T {
+                static Object make() {
+                    def cl = { String n -> "Hello, " + n }
+                    return (cl as (Greeter & java.io.Serializable))
+                }
+            }
+            def p = T.make()
+            return [p, p.greet("world")]
+        ''')
+        def (proxy, greeting) = result
+        assert proxy instanceof java.io.Serializable
+        assert proxy.class.interfaces.any { it.name == 'Greeter' }
+        assert greeting == 'Hello, world'
+    }
+
+    @Test
+    void 'three-component intersection coerces correctly'() {
+        def shell = new GroovyShell()
+        def result = shell.evaluate('''
+            interface One { String one() }
+            interface Two { String two() }
+            def proxy = ([one: { 'a' }, two: { 'b' }] as (One & Two & 
java.io.Serializable))
+            return [proxy.one(), proxy.two(), proxy instanceof 
java.io.Serializable]
+        ''')
+        assert result == ['a', 'b', true]
+    }
+
+    @Test
+    void 'null sources pass through cast and as'() {
+        def shell = new GroovyShell()
+        shell.evaluate('''
+            assert ((Runnable & java.io.Serializable) null) == null
+            assert (null as (Runnable & java.io.Serializable)) == null
+        ''')
+    }
+}

Reply via email to