[ 
https://issues.apache.org/jira/browse/GROOVY-12021?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18082175#comment-18082175
 ] 

ASF GitHub Bot commented on GROOVY-12021:
-----------------------------------------

Copilot commented on code in PR #2545:
URL: https://github.com/apache/groovy/pull/2545#discussion_r3270189092


##########
subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/MonadicChecker.groovy:
##########
@@ -0,0 +1,243 @@
+/*
+ *  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.typecheckers
+
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.GenericsType
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.expr.ClosureExpression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.apache.groovy.runtime.MonadicCarrierRegistry
+import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
+import org.codehaus.groovy.transform.stc.StaticTypesMarker
+
+import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.make
+import static 
org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafeWithGenerics
+
+/**
+ * Teaches {@code @CompileStatic}/{@code @TypeChecked} about the {@code DO} 
macro's
+ * desugared output: calls to {@code 
org.apache.groovy.macrolib.Comprehensions.bind}
+ * and {@code .map}, declared {@code (Object, Closure):Object}.
+ *
+ * Three jobs:
+ * <ul>
+ *   <li><b>Enforce receiver shape</b>: the carrier must participate 
(allow-list,
+ *       structural {@code flatMap}/{@code map}, or {@code @Monadic}); 
otherwise a
+ *       precise compile error naming the type and the missing shape.</li>
+ *   <li><b>Enforce closure-return shape</b> (trusted carriers only &mdash; 
registry
+ *       or {@code @Monadic}, not structural): {@code bind}'s closure must 
yield the
+ *       <em>same</em> carrier (catches a bare body or a cross-carrier body 
inside
+ *       {@code DO}, which the erased dispatcher signature otherwise lets 
through);
+ *       {@code map}'s closure must <em>not</em> yield the same carrier (the
+ *       {@code M<M<T>>} foot-gun for hand-written {@code 
Comprehensions.map}).</li>
+ *   <li><b>Assist inference</b>: type the generator closure's parameter as the
+ *       carrier's element type (so the body type-checks), and restore the
+ *       comprehension's result type (so {@code .get()}/nesting type-check) 
instead
+ *       of the erased {@code Object} the dispatcher signature would 
yield.</li>
+ * </ul>
+ *
+ * Closure-parameter typing works by pre-setting {@code CLOSURE_ARGUMENTS} on 
the
+ * closure node, which {@code 
StaticTypeCheckingVisitor.getTypeFromClosureArguments}
+ * consults by parameter name &mdash; independent of the {@code Closure<?>} 
parameter
+ * not being a SAM type.
+ *
+ * Activate with {@code 
@CompileStatic(extensions='groovy.typecheckers.MonadicChecker')}.
+ */
+class MonadicChecker extends 
GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
+
+    private static final String DISPATCHER = 
'org.apache.groovy.runtime.Comprehensions'
+
+    @Override
+    Object run() {
+        // Fires after method selection (carrier argument already typed) but 
before
+        // the generator closure body is visited: the window to type the 
closure param.
+        onMethodSelection { expr, MethodNode target ->
+            if (!isDispatcherCall(expr, target)) return
+            MethodCallExpression call = (MethodCallExpression) expr
+            def args = call.arguments.expressions
+            def carrierType = safeType(args[0])
+            String role = call.methodAsString
+
+            if (carrierType == null || !participates(carrierType)) {
+                addStaticTypeError(
+                    "Type ${typeName(carrierType)} does not participate in 
monadic comprehensions (DO): " +
+                    "no ${role == 'bind' ? "bind (flatMap-shaped)" : 'map'} 
method " +
+                    "(not in the standard carrier allow-list, has no 
structural " +
+                    "'${role == 'bind' ? 'flatMap' : 'map'}' method, and is 
not annotated @Monadic)",
+                    args[0])
+                return
+            }
+
+            def closure = args.find { it instanceof ClosureExpression } as 
ClosureExpression
+            if (closure != null) {
+                ClassNode elem = elementType(carrierType)
+                closure.putNodeMetaData(StaticTypesMarker.CLOSURE_ARGUMENTS, 
[elem] as ClassNode[])
+            }
+        }
+
+        // The dispatcher returns erased Object; restore the comprehension's 
real type.
+        afterMethodCall { call ->
+            if (!(call instanceof MethodCallExpression)) return
+            MethodNode target = 
call.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
+            if (!isDispatcherCall(call, target)) return
+            def args = call.arguments.expressions
+            def carrierType = safeType(args[0])
+            if (carrierType == null) return
+            def closure = args.find { it instanceof ClosureExpression } as 
ClosureExpression
+            ClassNode produced = closureReturnType(closure)
+
+            // Closure-return shape check (trusted carriers only). The 
dispatcher
+            // signature is (Object, Closure):Object — STC cannot see that the
+            // body must yield the same carrier (bind) or a non-carrier (map);
+            // restore the contract here. Skipped for structural-only carriers
+            // (intentionally permissive, like participates()).
+            //
+            // Anchor at the closure (the actual offender) when present; the DO
+            // macro propagates source positions onto its synthetic lambda, so
+            // STC's addStaticTypeError (which drops positionless nodes) will
+            // surface this. Fall through to args[0] for the hand-written
+            // Comprehensions.bind/map shape where the closure may not be a
+            // literal at this argument slot.
+            String role = call.methodAsString
+            String shape = shapeMsg(role, carrierType, produced)
+            if (shape) addStaticTypeError(shape, closure ?: args[0])
+
+            ClassNode result
+            if (role == 'bind') {
+                // closure yields M<B>; bind yields the same carrier
+                result = produced ?: carrierType
+            } else {
+                // map: closure yields B; result is M<B>
+                ClassNode b = produced ?: OBJECT_TYPE
+                result = 
makeClassSafeWithGenerics(carrierType.plainNodeReference, new GenericsType(b))
+            }
+            storeType(call, result)
+        }
+    }
+
+    /**
+     * Diagnostic for the dispatcher closure-return contract, or null if 
acceptable.
+     * Tolerates unknown returns (null/Object); only flags when the carrier 
mismatch
+     * is statically demonstrable and the receiver is a trusted (registry- or
+     * {@code @Monadic}-keyed) carrier.
+     */
+    private String shapeMsg(String role, ClassNode receiver, ClassNode 
produced) {
+        if (produced == null || produced == OBJECT_TYPE) return null
+        String recv = trustedCarrierName(receiver)
+        if (recv == null) return null
+        String ret = trustedCarrierName(produced)
+        if (role == 'bind') {
+            if (ret == null) {
+                return "Closure passed to Comprehensions.bind on ${recv} must 
yield ${recv}; " +
+                    "got ${typeName(produced)} (not a carrier). In a DO 
comprehension, the body " +
+                    "must produce the same carrier (e.g. ${recv}.of(...))."
+            }
+            if (ret != recv) {
+                return "Closure passed to Comprehensions.bind on ${recv} must 
yield ${recv}; " +
+                    "got ${ret}. Mixing carriers in a comprehension is not 
supported."
+            }
+            return null
+        }
+        // role == 'map'
+        if (ret == recv) {
+            return "Closure passed to Comprehensions.map on ${recv} returns a 
${recv}, " +
+                "producing ${recv}<${recv}<...>>; use Comprehensions.bind 
instead."
+        }
+        null
+    }
+
+    /**
+     * The canonical carrier name &mdash; the key for same-carrier comparison 
&mdash;
+     * for the given type, restricted to <em>trusted</em> participation paths
+     * (registry allow-list, {@code @Monadic}). Returns {@code null} for
+     * structural-only or non-carrier types; structural participation is
+     * intentionally permissive and not asserted against.
+     */
+    private String trustedCarrierName(ClassNode cn) {
+        if (cn == null) return null
+        ClassNode bare = cn.redirect() ?: cn
+        for (e in MonadicCarrierRegistry.entries()) {
+            if (assignableTo(bare, make(e.carrier()))) return 
make(e.carrier()).name
+        }
+        for (e in MonadicCarrierRegistry.namedEntries()) {
+            if (assignableTo(bare, make(e.carrierName()))) return 
e.carrierName()
+        }
+        for (ClassNode c = bare; c != null && c.name != 'java.lang.Object'; c 
= c.superClass) {
+            if (c.annotations?.any { it.classNode?.nameWithoutPackage == 
'Monadic' }) {
+                return c.name
+            }
+        }
+        null
+    }
+
+    private boolean isDispatcherCall(expr, MethodNode target) {
+        expr instanceof MethodCallExpression &&
+            expr.methodAsString in ['bind', 'map'] &&
+            target?.declaringClass?.name == DISPATCHER
+    }
+
+    private ClassNode safeType(expr) {
+        try { getType(expr) } catch (ignored) { null }
+    }
+
+    private static String typeName(ClassNode cn) {
+        cn == null ? '<unknown>' : cn.toString(false)
+    }
+
+    private boolean participates(ClassNode cn) {
+        ClassNode bare = cn.redirect() ?: cn
+        // 1. standard allow-list (shared with the runtime dispatcher), Class- 
and name-keyed
+        if (MonadicCarrierRegistry.entries().any { assignableTo(bare, 
make(it.carrier())) }) return true
+        if (MonadicCarrierRegistry.namedEntries().any { assignableTo(bare, 
make(it.carrierName())) }) return true
+        // 2. structural (flatMap covers bind; map covers the map role)
+        if (hasMethodNamed(bare, 'flatMap') || hasMethodNamed(bare, 'map')) 
return true
+        // 3. @Monadic opt-in (matched by simple name, like 
@Reducer/@Associative)
+        return bare.annotations.any { it.classNode?.nameWithoutPackage == 
'Monadic' }
+    }
+
+    private boolean assignableTo(ClassNode cn, ClassNode t) {
+        cn == t || cn.isDerivedFrom(t) || cn.implementsInterface(t)
+    }
+
+    private boolean hasMethodNamed(ClassNode cn, String name) {
+        for (ClassNode c = cn; c != null && c != OBJECT_TYPE; c = 
c.superClass) {
+            if (c.getMethods(name)) return true
+            if (c.interfaces.any { it.getMethods(name) }) return true

Review Comment:
   `participates` uses `hasMethodNamed` to detect structural participation, but 
`hasMethodNamed` returns true for *any* method called `flatMap`/`map` 
regardless of arity. The runtime dispatcher 
(`Comprehensions.findSingleArgMethod`) requires a single-argument method, so 
this can allow a carrier through under `@CompileStatic` that will later fail at 
runtime. Align `hasMethodNamed` with the runtime rule by requiring exactly one 
parameter (and ideally skipping bridge/synthetic methods similarly).
   



##########
src/main/java/org/apache/groovy/runtime/Comprehensions.java:
##########
@@ -0,0 +1,184 @@
+/*
+ *  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.apache.groovy.runtime;
+
+import groovy.lang.Closure;
+import org.apache.groovy.lang.annotation.Incubating;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+import org.codehaus.groovy.runtime.InvokerHelper;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.function.Function;
+
+/**
+ * Runtime bind/map dispatcher for monadic comprehensions &mdash; the emit 
target
+ * of the {@code DO} macro.
+ * <p>
+ * The macro runs at {@code SEMANTIC_ANALYSIS}, <em>before</em> type checking, 
so it
+ * cannot know the carrier's bind-method name and cannot emit it directly. 
Instead it
+ * emits {@code Comprehensions.bind(carrier) { x -> ... }} calls; this class 
resolves
+ * the carrier-specific method at runtime (dynamic Groovy), while the
+ * {@code groovy.typecheckers.MonadicChecker} type-checking extension 
specialises this
+ * one signature under {@code @CompileStatic}.
+ * <p>
+ * This is runtime support invoked from generated bytecode, hence its 
placement in
+ * core alongside the rest of the Groovy runtime; the {@code DO} macro and the 
type
+ * checker are compile-time only and remain in their optional modules.
+ * <p>
+ * Participation is resolved first-match-wins:
+ * <ol>
+ *   <li>standard allow-list ({@link MonadicCarrierRegistry});</li>
+ *   <li>structural ({@code flatMap}/{@code map} present);</li>
+ *   <li>{@code @Monadic} opt-in (matched by simple name; honours {@code 
bind}/{@code map} overrides).</li>
+ * </ol>
+ * A configured marker interface is a further opt-in mechanism that is not yet
+ * implemented.
+ * <p>
+ * Surface generosity: the carrier's bind method may accept a {@link Function} 
or a
+ * {@code Closure}; the closure is adapted to whichever the target declares. 
Monad
+ * laws are not enforced &mdash; structural participation, algebraic-law 
obligation
+ * on the implementer (the {@code @Reducer}/{@code @Associative} treatment).
+ *
+ * @since 6.0.0
+ */
+@Incubating
+public final class Comprehensions {
+
+    private Comprehensions() {}
+
+    /** Bind (flatMap-shaped): {@code carrier.<bind>(x -> fn(x))} where {@code 
fn} yields the same carrier. */
+    public static Object bind(Object carrier, Closure<?> fn) {
+        return dispatch(carrier, fn, true);
+    }
+
+    /** Map: {@code carrier.<map>(x -> fn(x))} where {@code fn} yields a plain 
value. */
+    public static Object map(Object carrier, Closure<?> fn) {
+        return dispatch(carrier, fn, false);
+    }
+
+    private static Object dispatch(Object carrier, Closure<?> fn, boolean 
bindRole) {
+        if (carrier == null) {
+            throw new IllegalArgumentException(
+                "Monadic comprehension carrier is null; null cannot 
participate as a carrier");
+        }
+        Class<?> type = carrier.getClass();
+        String method = resolveMethodName(carrier, type, bindRole);
+        if (method == null) {
+            String role = bindRole ? "bind (flatMap-shaped)" : "map";
+            String structural = bindRole ? "flatMap" : "map";
+            throw new IllegalArgumentException(
+                "Type " + type.getName() + " does not participate in monadic 
comprehensions: "
+              + "no " + role + " method found (not in the standard carrier 
allow-list, "
+              + "has no structural '" + structural + "' method, and is not 
annotated @Monadic)");
+        }
+        Object arg = adaptClosure(type, method, fn);
+        return InvokerHelper.invokeMethod(carrier, method, arg);
+    }
+
+    private static String resolveMethodName(Object carrier, Class<?> type, 
boolean bindRole) {
+        // 1. standard allow-list (Class- or name-keyed)
+        String[] bindMap = MonadicCarrierRegistry.lookupBindMap(carrier);
+        if (bindMap != null) {
+            return bindRole ? bindMap[0] : bindMap[1];
+        }
+        // 2. structural
+        String structural = bindRole ? "flatMap" : "map";
+        if (findSingleArgMethod(type, structural) != null) {
+            return structural;
+        }
+        // 3. @Monadic opt-in (matched by simple name, like 
@Reducer/@Associative)
+        String monadic = monadicMethodName(type, bindRole);
+        if (monadic != null && findSingleArgMethod(type, monadic) != null) {
+            return monadic;
+        }
+        return null;
+    }
+
+    private static String monadicMethodName(Class<?> type, boolean bindRole) {
+        for (Class<?> c = type; c != null && c != Object.class; c = 
c.getSuperclass()) {
+            String n = readMonadicMember(c.getDeclaredAnnotations(), bindRole);
+            if (n != null) return n;
+            for (Class<?> i : c.getInterfaces()) {
+                String ni = readMonadicMember(i.getDeclaredAnnotations(), 
bindRole);
+                if (ni != null) return ni;
+            }
+        }
+        return null;
+    }
+
+    private static String readMonadicMember(Annotation[] annotations, boolean 
bindRole) {
+        for (Annotation a : annotations) {
+            if (!"Monadic".equals(a.annotationType().getSimpleName())) 
continue;
+            String configured = invokeStringMember(a, bindRole ? "bind" : 
"map");
+            if (configured == null || configured.isEmpty()) {
+                return bindRole ? "flatMap" : "map"; // opted in, structural 
defaults
+            }
+            return configured;
+        }
+        return null;
+    }
+
+    private static String invokeStringMember(Annotation a, String member) {
+        try {
+            Object v = a.annotationType().getMethod(member).invoke(a);
+            return v == null ? null : v.toString();
+        } catch (ReflectiveOperationException ignored) {
+            return null;
+        }
+    }
+
+    private static Method findSingleArgMethod(Class<?> type, String name) {
+        for (Method m : type.getMethods()) {
+            if (m.getParameterCount() == 1 && m.getName().equals(name)
+                    && !m.isBridge() && !m.isSynthetic()) {
+                return m;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adapt the generator closure to whatever single-arg type the carrier's 
bind
+     * method declares:
+     * <ul>
+     *   <li>a {@code Closure}-typed parameter receives the closure 
directly;</li>
+     *   <li>a {@link Function}-typed parameter receives a thin wrapper;</li>
+     *   <li>any other functional interface (for example Functional Java's
+     *       {@code fj.F}) receives the closure coerced to that interface.</li>
+     * </ul>
+     */
+    private static Object adaptClosure(Class<?> type, String method, final 
Closure<?> fn) {
+        Method m = findSingleArgMethod(type, method);
+        Class<?> pt = (m != null) ? m.getParameterTypes()[0] : null;
+        if (pt == null || Closure.class.isAssignableFrom(pt)) {
+            return fn;
+        }
+        if (pt.isAssignableFrom(Function.class)) { // 
java.util.function.Function (or a supertype)
+            return new Function<Object, Object>() {
+                @Override
+                public Object apply(Object value) {
+                    return fn.call(value);
+                }
+            };
+        }

Review Comment:
   `adaptClosure` treats any parameter type that is a *supertype* of `Function` 
(including `Object`) as “Function-typed” because it uses 
`pt.isAssignableFrom(Function.class)`. That will wrap the closure in a 
`Function` even when a structural carrier’s `flatMap/map` is declared with an 
untyped/`Object` parameter (common in Groovy), and such implementations 
typically expect a `Closure` (e.g. invoke via `fn.call(...)`), so DO will fail 
at runtime. Consider narrowing this to `pt == Function.class` (or otherwise 
only when the selected overload actually requires a `Function`), and/or prefer 
a `Closure`-accepting overload when present.



##########
subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/MonadicChecker.groovy:
##########
@@ -0,0 +1,243 @@
+/*
+ *  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.typecheckers
+
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.GenericsType
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.expr.ClosureExpression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.apache.groovy.runtime.MonadicCarrierRegistry
+import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
+import org.codehaus.groovy.transform.stc.StaticTypesMarker
+
+import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.make
+import static 
org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafeWithGenerics
+
+/**
+ * Teaches {@code @CompileStatic}/{@code @TypeChecked} about the {@code DO} 
macro's
+ * desugared output: calls to {@code 
org.apache.groovy.macrolib.Comprehensions.bind}

Review Comment:
   Javadoc refers to `org.apache.groovy.macrolib.Comprehensions.bind/.map`, but 
the dispatcher class added/used elsewhere is 
`org.apache.groovy.runtime.Comprehensions`. Updating this reference will avoid 
confusion for users enabling the extension.
   



##########
subprojects/groovy-typecheckers/src/main/groovy/groovy/typecheckers/MonadicChecker.groovy:
##########
@@ -0,0 +1,243 @@
+/*
+ *  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.typecheckers
+
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.GenericsType
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.ast.expr.ClosureExpression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.apache.groovy.runtime.MonadicCarrierRegistry
+import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport
+import org.codehaus.groovy.transform.stc.StaticTypesMarker
+
+import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.make
+import static 
org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafeWithGenerics
+
+/**
+ * Teaches {@code @CompileStatic}/{@code @TypeChecked} about the {@code DO} 
macro's
+ * desugared output: calls to {@code 
org.apache.groovy.macrolib.Comprehensions.bind}
+ * and {@code .map}, declared {@code (Object, Closure):Object}.
+ *
+ * Three jobs:
+ * <ul>
+ *   <li><b>Enforce receiver shape</b>: the carrier must participate 
(allow-list,
+ *       structural {@code flatMap}/{@code map}, or {@code @Monadic}); 
otherwise a
+ *       precise compile error naming the type and the missing shape.</li>
+ *   <li><b>Enforce closure-return shape</b> (trusted carriers only &mdash; 
registry
+ *       or {@code @Monadic}, not structural): {@code bind}'s closure must 
yield the
+ *       <em>same</em> carrier (catches a bare body or a cross-carrier body 
inside
+ *       {@code DO}, which the erased dispatcher signature otherwise lets 
through);
+ *       {@code map}'s closure must <em>not</em> yield the same carrier (the
+ *       {@code M<M<T>>} foot-gun for hand-written {@code 
Comprehensions.map}).</li>
+ *   <li><b>Assist inference</b>: type the generator closure's parameter as the
+ *       carrier's element type (so the body type-checks), and restore the
+ *       comprehension's result type (so {@code .get()}/nesting type-check) 
instead
+ *       of the erased {@code Object} the dispatcher signature would 
yield.</li>
+ * </ul>
+ *
+ * Closure-parameter typing works by pre-setting {@code CLOSURE_ARGUMENTS} on 
the
+ * closure node, which {@code 
StaticTypeCheckingVisitor.getTypeFromClosureArguments}
+ * consults by parameter name &mdash; independent of the {@code Closure<?>} 
parameter
+ * not being a SAM type.
+ *
+ * Activate with {@code 
@CompileStatic(extensions='groovy.typecheckers.MonadicChecker')}.
+ */
+class MonadicChecker extends 
GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {
+
+    private static final String DISPATCHER = 
'org.apache.groovy.runtime.Comprehensions'
+
+    @Override
+    Object run() {
+        // Fires after method selection (carrier argument already typed) but 
before
+        // the generator closure body is visited: the window to type the 
closure param.
+        onMethodSelection { expr, MethodNode target ->
+            if (!isDispatcherCall(expr, target)) return
+            MethodCallExpression call = (MethodCallExpression) expr
+            def args = call.arguments.expressions
+            def carrierType = safeType(args[0])
+            String role = call.methodAsString
+
+            if (carrierType == null || !participates(carrierType)) {
+                addStaticTypeError(
+                    "Type ${typeName(carrierType)} does not participate in 
monadic comprehensions (DO): " +
+                    "no ${role == 'bind' ? "bind (flatMap-shaped)" : 'map'} 
method " +
+                    "(not in the standard carrier allow-list, has no 
structural " +
+                    "'${role == 'bind' ? 'flatMap' : 'map'}' method, and is 
not annotated @Monadic)",
+                    args[0])
+                return
+            }
+
+            def closure = args.find { it instanceof ClosureExpression } as 
ClosureExpression
+            if (closure != null) {
+                ClassNode elem = elementType(carrierType)
+                closure.putNodeMetaData(StaticTypesMarker.CLOSURE_ARGUMENTS, 
[elem] as ClassNode[])
+            }
+        }
+
+        // The dispatcher returns erased Object; restore the comprehension's 
real type.
+        afterMethodCall { call ->
+            if (!(call instanceof MethodCallExpression)) return
+            MethodNode target = 
call.getNodeMetaData(StaticTypesMarker.DIRECT_METHOD_CALL_TARGET)
+            if (!isDispatcherCall(call, target)) return
+            def args = call.arguments.expressions
+            def carrierType = safeType(args[0])
+            if (carrierType == null) return
+            def closure = args.find { it instanceof ClosureExpression } as 
ClosureExpression
+            ClassNode produced = closureReturnType(closure)
+
+            // Closure-return shape check (trusted carriers only). The 
dispatcher
+            // signature is (Object, Closure):Object — STC cannot see that the
+            // body must yield the same carrier (bind) or a non-carrier (map);
+            // restore the contract here. Skipped for structural-only carriers
+            // (intentionally permissive, like participates()).
+            //
+            // Anchor at the closure (the actual offender) when present; the DO
+            // macro propagates source positions onto its synthetic lambda, so
+            // STC's addStaticTypeError (which drops positionless nodes) will
+            // surface this. Fall through to args[0] for the hand-written
+            // Comprehensions.bind/map shape where the closure may not be a
+            // literal at this argument slot.
+            String role = call.methodAsString
+            String shape = shapeMsg(role, carrierType, produced)
+            if (shape) addStaticTypeError(shape, closure ?: args[0])
+
+            ClassNode result
+            if (role == 'bind') {
+                // closure yields M<B>; bind yields the same carrier
+                result = produced ?: carrierType
+            } else {
+                // map: closure yields B; result is M<B>
+                ClassNode b = produced ?: OBJECT_TYPE
+                result = 
makeClassSafeWithGenerics(carrierType.plainNodeReference, new GenericsType(b))
+            }
+            storeType(call, result)
+        }
+    }
+
+    /**
+     * Diagnostic for the dispatcher closure-return contract, or null if 
acceptable.
+     * Tolerates unknown returns (null/Object); only flags when the carrier 
mismatch
+     * is statically demonstrable and the receiver is a trusted (registry- or
+     * {@code @Monadic}-keyed) carrier.
+     */
+    private String shapeMsg(String role, ClassNode receiver, ClassNode 
produced) {
+        if (produced == null || produced == OBJECT_TYPE) return null
+        String recv = trustedCarrierName(receiver)
+        if (recv == null) return null
+        String ret = trustedCarrierName(produced)
+        if (role == 'bind') {
+            if (ret == null) {
+                return "Closure passed to Comprehensions.bind on ${recv} must 
yield ${recv}; " +
+                    "got ${typeName(produced)} (not a carrier). In a DO 
comprehension, the body " +
+                    "must produce the same carrier (e.g. ${recv}.of(...))."
+            }
+            if (ret != recv) {
+                return "Closure passed to Comprehensions.bind on ${recv} must 
yield ${recv}; " +
+                    "got ${ret}. Mixing carriers in a comprehension is not 
supported."
+            }
+            return null
+        }
+        // role == 'map'
+        if (ret == recv) {
+            return "Closure passed to Comprehensions.map on ${recv} returns a 
${recv}, " +
+                "producing ${recv}<${recv}<...>>; use Comprehensions.bind 
instead."
+        }
+        null
+    }
+
+    /**
+     * The canonical carrier name &mdash; the key for same-carrier comparison 
&mdash;
+     * for the given type, restricted to <em>trusted</em> participation paths
+     * (registry allow-list, {@code @Monadic}). Returns {@code null} for
+     * structural-only or non-carrier types; structural participation is
+     * intentionally permissive and not asserted against.
+     */
+    private String trustedCarrierName(ClassNode cn) {
+        if (cn == null) return null
+        ClassNode bare = cn.redirect() ?: cn
+        for (e in MonadicCarrierRegistry.entries()) {
+            if (assignableTo(bare, make(e.carrier()))) return 
make(e.carrier()).name
+        }
+        for (e in MonadicCarrierRegistry.namedEntries()) {
+            if (assignableTo(bare, make(e.carrierName()))) return 
e.carrierName()
+        }
+        for (ClassNode c = bare; c != null && c.name != 'java.lang.Object'; c 
= c.superClass) {
+            if (c.annotations?.any { it.classNode?.nameWithoutPackage == 
'Monadic' }) {
+                return c.name
+            }
+        }
+        null
+    }
+
+    private boolean isDispatcherCall(expr, MethodNode target) {
+        expr instanceof MethodCallExpression &&
+            expr.methodAsString in ['bind', 'map'] &&
+            target?.declaringClass?.name == DISPATCHER
+    }
+
+    private ClassNode safeType(expr) {
+        try { getType(expr) } catch (ignored) { null }
+    }
+
+    private static String typeName(ClassNode cn) {
+        cn == null ? '<unknown>' : cn.toString(false)
+    }
+
+    private boolean participates(ClassNode cn) {
+        ClassNode bare = cn.redirect() ?: cn
+        // 1. standard allow-list (shared with the runtime dispatcher), Class- 
and name-keyed
+        if (MonadicCarrierRegistry.entries().any { assignableTo(bare, 
make(it.carrier())) }) return true
+        if (MonadicCarrierRegistry.namedEntries().any { assignableTo(bare, 
make(it.carrierName())) }) return true
+        // 2. structural (flatMap covers bind; map covers the map role)
+        if (hasMethodNamed(bare, 'flatMap') || hasMethodNamed(bare, 'map')) 
return true
+        // 3. @Monadic opt-in (matched by simple name, like 
@Reducer/@Associative)
+        return bare.annotations.any { it.classNode?.nameWithoutPackage == 
'Monadic' }
+    }
+

Review Comment:
   The `@Monadic` participation check differs from the runtime dispatcher: 
`participates` only checks `bare.annotations` (no superclass/interface walk), 
and `trustedCarrierName` walks superclasses but not interfaces. 
`Comprehensions.monadicMethodName` searches superclasses *and* interfaces for a 
`Monadic` annotation, so under `@CompileStatic` this can incorrectly reject (or 
fail to “trust”) carriers that work at runtime when `@Monadic` is declared on a 
supertype or interface.
   





> Add DO macro for monadic comprehensions over Optional/Stream/Awaitable and 
> @Monadic types
> -----------------------------------------------------------------------------------------
>
>                 Key: GROOVY-12021
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12021
>             Project: Groovy
>          Issue Type: New Feature
>            Reporter: Paul King
>            Priority: Major
>




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to