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


##########
src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy:
##########
@@ -0,0 +1,1115 @@
+/*
+ *  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.transform
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Tests for virtual thread integration, executor configuration, exception
+ * handling consistency across async methods/closures/lambdas, and coverage
+ * improvements for the async/await feature.
+ *
+ * @since 6.0.0
+ */
+final class AsyncVirtualThreadTest {
+
+    // ---- Virtual thread detection and usage ----
+
+    @Test
+    void testVirtualThreadsAvailable() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+            // JDK 21+ should have virtual threads
+            def jdkVersion = Runtime.version().feature()
+            if (jdkVersion >= 21) {
+                assert isVirtualThreadsAvailable()
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncMethodRunsOnVirtualThread() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+
+            class VTService {
+                async checkThread() {
+                    return Thread.currentThread().isVirtual()
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def service = new VTService()
+                def result = service.checkThread().get()
+                assert result == true
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncClosureRunsOnVirtualThread() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+
+            if (isVirtualThreadsAvailable()) {
+                def awaitable = async { Thread.currentThread().isVirtual() }
+                assert await(awaitable) == true
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncLambdaRunsOnVirtualThread() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+
+            if (isVirtualThreadsAvailable()) {
+                def asyncFn = async { x -> Thread.currentThread().isVirtual() }
+                def result = await(asyncFn(42))
+                assert result == true
+            }
+        '''
+    }
+
+    @Test
+    void testForAwaitRunsOnVirtualThread() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+            import groovy.concurrent.AsyncStream
+
+            class VTGenerator {
+                async generate() {
+                    yield return Thread.currentThread().isVirtual()
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def gen = new VTGenerator()
+                def stream = gen.generate()
+                def results = []
+                for await (item in stream) {
+                    results << item
+                }
+                assert results == [true]
+            }
+        '''
+    }
+
+    @Test
+    void testHighConcurrencyWithVirtualThreads() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+
+            class HighConcurrency {
+                async compute(int n) {
+                    Thread.sleep(10)
+                    return n * 2
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def svc = new HighConcurrency()
+                // Launch 1000 concurrent tasks — trivial with virtual threads
+                def awaitables = (1..1000).collect { svc.compute(it) }
+                def results = awaitAll(awaitables as Object[])
+                assert results.size() == 1000
+                assert results[0] == 2
+                assert results[999] == 2000
+            }
+        '''
+    }
+
+    // ---- Executor configuration ----
+
+    @Test
+    void testCustomExecutorOverride() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+            import java.util.concurrent.Executors
+            import java.util.concurrent.atomic.AtomicReference
+
+            def savedExecutor = getExecutor()
+            try {
+                def customPool = Executors.newFixedThreadPool(2, { r ->
+                    def t = new Thread(r)
+                    t.setName("custom-async-" + t.getId())
+                    t
+                })
+                setExecutor(customPool)
+
+                def awaitable = async {
+                    Thread.currentThread().getName()
+                }
+                def threadName = await(awaitable)
+                assert threadName.startsWith("custom-async-")
+            } finally {
+                setExecutor(savedExecutor)
+            }
+        '''
+    }
+
+    @Test
+    void testSetExecutorNullResetsToDefault() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+            import java.util.concurrent.Executors
+
+            def originalExecutor = getExecutor()
+            // Set a custom executor
+            setExecutor(Executors.newSingleThreadExecutor())
+            assert getExecutor() != originalExecutor
+            // Reset to null — should restore default
+            setExecutor(null)
+            def restored = getExecutor()
+            assert restored != null
+            // Verify it works
+            def awaitable = groovy.concurrent.AsyncUtils.async { 42 }
+            assert groovy.concurrent.AsyncUtils.await(awaitable) == 42
+            // Restore original
+            setExecutor(originalExecutor)

Review Comment:
   `setExecutor(Executors.newSingleThreadExecutor())` allocates an executor 
that is never shut down (the reference is lost). Resetting the global executor 
to null/original does not stop the created thread, which can leak threads 
across tests. Store the executor in a variable and shut it down in a `finally` 
block.
   ```suggestion
               def customExecutor = Executors.newSingleThreadExecutor()
               try {
                   // Set a custom executor
                   setExecutor(customExecutor)
                   assert getExecutor() != originalExecutor
                   // Reset to null — should restore default
                   setExecutor(null)
                   def restored = getExecutor()
                   assert restored != null
                   // Verify it works
                   def awaitable = groovy.concurrent.AsyncUtils.async { 42 }
                   assert groovy.concurrent.AsyncUtils.await(awaitable) == 42
               } finally {
                   // Restore original
                   setExecutor(originalExecutor)
                   customExecutor.shutdown()
               }
   ```



##########
src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy:
##########
@@ -0,0 +1,1115 @@
+/*
+ *  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.transform
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Tests for virtual thread integration, executor configuration, exception
+ * handling consistency across async methods/closures/lambdas, and coverage
+ * improvements for the async/await feature.
+ *
+ * @since 6.0.0
+ */
+final class AsyncVirtualThreadTest {
+
+    // ---- Virtual thread detection and usage ----
+
+    @Test
+    void testVirtualThreadsAvailable() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+            // JDK 21+ should have virtual threads
+            def jdkVersion = Runtime.version().feature()
+            if (jdkVersion >= 21) {
+                assert isVirtualThreadsAvailable()
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncMethodRunsOnVirtualThread() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+
+            class VTService {
+                async checkThread() {
+                    return Thread.currentThread().isVirtual()
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def service = new VTService()
+                def result = service.checkThread().get()
+                assert result == true
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncClosureRunsOnVirtualThread() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+
+            if (isVirtualThreadsAvailable()) {
+                def awaitable = async { Thread.currentThread().isVirtual() }
+                assert await(awaitable) == true
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncLambdaRunsOnVirtualThread() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+
+            if (isVirtualThreadsAvailable()) {
+                def asyncFn = async { x -> Thread.currentThread().isVirtual() }
+                def result = await(asyncFn(42))
+                assert result == true
+            }
+        '''
+    }
+
+    @Test
+    void testForAwaitRunsOnVirtualThread() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+            import groovy.concurrent.AsyncStream
+
+            class VTGenerator {
+                async generate() {
+                    yield return Thread.currentThread().isVirtual()
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def gen = new VTGenerator()
+                def stream = gen.generate()
+                def results = []
+                for await (item in stream) {
+                    results << item
+                }
+                assert results == [true]
+            }
+        '''
+    }
+
+    @Test
+    void testHighConcurrencyWithVirtualThreads() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+
+            class HighConcurrency {
+                async compute(int n) {
+                    Thread.sleep(10)
+                    return n * 2
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def svc = new HighConcurrency()
+                // Launch 1000 concurrent tasks — trivial with virtual threads
+                def awaitables = (1..1000).collect { svc.compute(it) }
+                def results = awaitAll(awaitables as Object[])
+                assert results.size() == 1000
+                assert results[0] == 2
+                assert results[999] == 2000
+            }
+        '''
+    }
+
+    // ---- Executor configuration ----
+
+    @Test
+    void testCustomExecutorOverride() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+            import java.util.concurrent.Executors
+            import java.util.concurrent.atomic.AtomicReference
+
+            def savedExecutor = getExecutor()
+            try {
+                def customPool = Executors.newFixedThreadPool(2, { r ->
+                    def t = new Thread(r)
+                    t.setName("custom-async-" + t.getId())
+                    t
+                })
+                setExecutor(customPool)
+
+                def awaitable = async {
+                    Thread.currentThread().getName()
+                }
+                def threadName = await(awaitable)
+                assert threadName.startsWith("custom-async-")
+            } finally {
+                setExecutor(savedExecutor)
+            }

Review Comment:
   This test creates a fixed thread pool and sets it as the async executor, but 
the pool is never shut down. Since the thread factory creates non-daemon 
threads, this can leak threads and hang the test JVM. Ensure the custom 
executor is shut down (and ideally created with daemon threads) in the 
`finally` block after resetting the executor.



##########
src/test/groovy/org/codehaus/groovy/transform/AsyncVirtualThreadTest.groovy:
##########
@@ -0,0 +1,1115 @@
+/*
+ *  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.transform
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Tests for virtual thread integration, executor configuration, exception
+ * handling consistency across async methods/closures/lambdas, and coverage
+ * improvements for the async/await feature.
+ *
+ * @since 6.0.0
+ */
+final class AsyncVirtualThreadTest {
+
+    // ---- Virtual thread detection and usage ----
+
+    @Test
+    void testVirtualThreadsAvailable() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+            // JDK 21+ should have virtual threads
+            def jdkVersion = Runtime.version().feature()
+            if (jdkVersion >= 21) {
+                assert isVirtualThreadsAvailable()
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncMethodRunsOnVirtualThread() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+
+            class VTService {
+                async checkThread() {
+                    return Thread.currentThread().isVirtual()
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def service = new VTService()
+                def result = service.checkThread().get()
+                assert result == true
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncClosureRunsOnVirtualThread() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+
+            if (isVirtualThreadsAvailable()) {
+                def awaitable = async { Thread.currentThread().isVirtual() }
+                assert await(awaitable) == true
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncLambdaRunsOnVirtualThread() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+
+            if (isVirtualThreadsAvailable()) {
+                def asyncFn = async { x -> Thread.currentThread().isVirtual() }
+                def result = await(asyncFn(42))
+                assert result == true
+            }
+        '''
+    }
+
+    @Test
+    void testForAwaitRunsOnVirtualThread() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+            import groovy.concurrent.AsyncStream
+
+            class VTGenerator {
+                async generate() {
+                    yield return Thread.currentThread().isVirtual()
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def gen = new VTGenerator()
+                def stream = gen.generate()
+                def results = []
+                for await (item in stream) {
+                    results << item
+                }
+                assert results == [true]
+            }
+        '''
+    }
+
+    @Test
+    void testHighConcurrencyWithVirtualThreads() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+
+            class HighConcurrency {
+                async compute(int n) {
+                    Thread.sleep(10)
+                    return n * 2
+                }
+            }
+
+            if (isVirtualThreadsAvailable()) {
+                def svc = new HighConcurrency()
+                // Launch 1000 concurrent tasks — trivial with virtual threads
+                def awaitables = (1..1000).collect { svc.compute(it) }
+                def results = awaitAll(awaitables as Object[])
+                assert results.size() == 1000
+                assert results[0] == 2
+                assert results[999] == 2000
+            }
+        '''
+    }
+
+    // ---- Executor configuration ----
+
+    @Test
+    void testCustomExecutorOverride() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+            import java.util.concurrent.Executors
+            import java.util.concurrent.atomic.AtomicReference
+
+            def savedExecutor = getExecutor()
+            try {
+                def customPool = Executors.newFixedThreadPool(2, { r ->
+                    def t = new Thread(r)
+                    t.setName("custom-async-" + t.getId())
+                    t
+                })
+                setExecutor(customPool)
+
+                def awaitable = async {
+                    Thread.currentThread().getName()
+                }
+                def threadName = await(awaitable)
+                assert threadName.startsWith("custom-async-")
+            } finally {
+                setExecutor(savedExecutor)
+            }
+        '''
+    }
+
+    @Test
+    void testSetExecutorNullResetsToDefault() {
+        assertScript '''
+            import static groovy.concurrent.AsyncUtils.*
+            import java.util.concurrent.Executors
+
+            def originalExecutor = getExecutor()
+            // Set a custom executor
+            setExecutor(Executors.newSingleThreadExecutor())
+            assert getExecutor() != originalExecutor
+            // Reset to null — should restore default
+            setExecutor(null)
+            def restored = getExecutor()
+            assert restored != null
+            // Verify it works
+            def awaitable = groovy.concurrent.AsyncUtils.async { 42 }
+            assert groovy.concurrent.AsyncUtils.await(awaitable) == 42
+            // Restore original
+            setExecutor(originalExecutor)
+        '''
+    }
+
+    @Test
+    void testAsyncMethodWithCustomExecutorField() {
+        assertScript '''
+            import groovy.transform.Async
+            import static groovy.concurrent.AsyncUtils.*
+            import java.util.concurrent.Executor
+            import java.util.concurrent.Executors
+
+            class CustomExecutorService {
+                static Executor myPool = Executors.newFixedThreadPool(1, { r ->
+                    def t = new Thread(r)
+                    t.setName("my-pool-thread")
+                    t
+                })
+
+                @Async(executor = "myPool")
+                def doWork() {
+                    return Thread.currentThread().getName()
+                }
+            }
+
+            def svc = new CustomExecutorService()
+            def result = svc.doWork().get()
+            assert result.startsWith("my-pool-thread")
+        '''

Review Comment:
   This test defines a static `Executor` via 
`Executors.newFixedThreadPool(...)` but never shuts it down. Because the thread 
factory does not mark threads as daemon, this can leave non-daemon threads 
running after the test completes. Add cleanup to shut down `myPool` (or make 
its threads daemon) once the assertion is done.



##########
src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java:
##########
@@ -0,0 +1,597 @@
+/*
+ *  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.async;
+
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.AwaitableAdapterRegistry;
+import groovy.concurrent.AwaitResult;
+import groovy.lang.Closure;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import java.util.concurrent.Future;
+
+/**
+ * Internal runtime support for the {@code async}/{@code await} language 
feature
+ * and the {@link groovy.transform.Async @Async} annotation.
+ * <p>
+ * This class contains the actual implementation invoked by compiler-generated
+ * code.  User code should use the public-facing
+ * {@link groovy.concurrent.AsyncUtils} facade instead.
+ * <p>
+ * All overloads of {@code await()} go through the
+ * {@link AwaitableAdapterRegistry} so that third-party async types (RxJava
+ * {@code Single}, Reactor {@code Mono}, etc.) are supported transparently
+ * once an adapter is registered.
+ * <p>
+ * <b>Thread pool configuration</b>
+ * <ul>
+ *   <li>On JDK 21+ the default executor is a virtual-thread-per-task executor
+ *       obtained via {@code Executors.newVirtualThreadPerTaskExecutor()}.</li>
+ *   <li>On earlier JDKs the fallback is a dedicated fixed thread pool whose
+ *       size is controlled by the system property {@code 
groovy.async.parallelism}
+ *       (default: {@code availableProcessors + 1}).  All threads in this pool
+ *       are daemon threads named {@code groovy-async-<id>}.</li>
+ *   <li>The executor can be overridden at any time via {@link 
#setExecutor}.</li>
+ * </ul>
+ * <p>
+ * <b>Exception handling</b> follows the same transparency principle as C# and
+ * JavaScript: the <em>original</em> exception is rethrown without being 
wrapped
+ * in an {@link java.util.concurrent.ExecutionException ExecutionException} or
+ * {@link java.util.concurrent.CompletionException CompletionException}.
+ *
+ * @see groovy.concurrent.AsyncUtils
+ * @see groovy.transform.Async
+ * @see Awaitable
+ * @since 6.0.0
+ */
+public class AsyncSupport {
+
+    /**
+     * {@code true} if the running JVM supports virtual threads (JDK 21+).
+     * Detected once at class-load time via {@link MethodHandle} reflection.
+     */
+    private static final boolean VIRTUAL_THREADS_AVAILABLE;
+
+    private static final Executor VIRTUAL_THREAD_EXECUTOR;
+
+    /**
+     * Fallback thread pool size when virtual threads are unavailable.
+     * Configurable via the system property {@code groovy.async.parallelism}.
+     * Defaults to {@code Runtime.getRuntime().availableProcessors() + 1}.
+     */
+    private static final int FALLBACK_PARALLELISM;
+
+    private static final Executor FALLBACK_EXECUTOR;
+
+    static {
+        Executor vtExecutor = null;
+        boolean vtAvailable = false;
+        try {
+            MethodHandle mh = MethodHandles.lookup().findStatic(
+                    Executors.class, "newVirtualThreadPerTaskExecutor",
+                    MethodType.methodType(ExecutorService.class));
+            vtExecutor = (Executor) mh.invoke();
+            vtAvailable = true;
+        } catch (Throwable ignored) {
+            // JDK < 21 — virtual threads not available
+        }
+        VIRTUAL_THREAD_EXECUTOR = vtExecutor;
+        VIRTUAL_THREADS_AVAILABLE = vtAvailable;
+
+        FALLBACK_PARALLELISM = 
org.apache.groovy.util.SystemUtil.getIntegerSafe(
+                "groovy.async.parallelism",
+                Runtime.getRuntime().availableProcessors() + 1);
+        if (!VIRTUAL_THREADS_AVAILABLE) {
+            FALLBACK_EXECUTOR = 
Executors.newFixedThreadPool(FALLBACK_PARALLELISM, r -> {
+                Thread t = new Thread(r);
+                t.setDaemon(true);
+                t.setName("groovy-async-" + t.threadId());

Review Comment:
   `Thread#threadId()` is not available on the project's target Java version 
(17), so this won't compile. Use `Thread#getId()` (or a reflection-based 
fallback) for naming the fallback pool threads.
   ```suggestion
                   t.setName("groovy-async-" + t.getId());
   ```



##########
src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java:
##########
@@ -0,0 +1,123 @@
+/*
+ *  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.async;
+
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.Awaitable;
+
+import java.util.concurrent.SynchronousQueue;
+
+/**
+ * A producer/consumer implementation of {@link AsyncStream} used by
+ * {@code async} methods that contain {@code yield return} statements.
+ * <p>
+ * The producer (method body) runs on a separate thread and calls
+ * {@link #yield(Object)} for each emitted element. The consumer
+ * calls {@link #moveNext()}/{@link #getCurrent()} — typically via
+ * a {@code for await} loop.
+ * <p>
+ * Uses a {@link SynchronousQueue} to provide natural back-pressure:
+ * the producer thread blocks at each {@code yield return} until the
+ * consumer has consumed the previous element (mirroring C#'s async
+ * iterator suspension semantics).
+ * <p>
+ * This class is an internal implementation detail and should not be referenced
+ * directly by user code.
+ *
+ * @param <T> the element type
+ * @since 6.0.0
+ */
+public class AsyncStreamGenerator<T> implements AsyncStream<T> {
+
+    private static final Object DONE = new Object();
+
+    private final SynchronousQueue<Object> queue = new SynchronousQueue<>();
+    private T current;
+
+    /**
+     * Produces the next element. Called from the generator body when
+     * a {@code yield return expr} statement is executed. Blocks until
+     * the consumer is ready.
+     */
+    public void yield(Object value) {
+        try {
+            queue.put(new Item(value));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new java.util.concurrent.CancellationException("Interrupted 
during yield");
+        }
+    }
+
+    /**
+     * Signals that the generator has completed (no more elements).
+     */
+    public void complete() {
+        try {
+            queue.put(DONE);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    /**
+     * Signals that the generator failed with an exception.
+     */
+    public void error(Throwable t) {
+        try {
+            queue.put(new ErrorItem(t));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }

Review Comment:
   `complete()`/`error()` swallow `InterruptedException` without enqueuing 
`DONE`/`ErrorItem`. If the producer thread is interrupted at this point, 
consumers blocked in `moveNext()` can hang indefinitely. Consider retrying the 
`queue.put(...)`, or recording terminal state and ensuring `moveNext()` can 
observe completion/failure even when the producer is interrupted.



##########
src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java:
##########
@@ -0,0 +1,324 @@
+/*
+ *  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.transform;
+
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.Awaitable;
+import groovy.transform.Async;
+import org.apache.groovy.runtime.async.AsyncSupport;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
+import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.FieldNode;
+import org.codehaus.groovy.ast.GenericsType;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.VariableScope;
+import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
+import org.codehaus.groovy.ast.expr.VariableExpression;
+import org.codehaus.groovy.ast.stmt.ExpressionStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
+import static 
org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafeWithGenerics;
+
+/**
+ * Handles code generation for the {@link Async @Async} annotation.
+ * <p>
+ * Transforms the annotated method so that:
+ * <ol>
+ *   <li>{@code await(expr)} calls within the method body are redirected to
+ *       {@link AsyncSupport#await(Object) AsyncSupport.await()}</li>
+ *   <li>The method body is executed asynchronously via
+ *       {@link AsyncSupport#executeAsync AsyncSupport.executeAsync}
+ *       (or {@link AsyncSupport#executeAsyncVoid 
AsyncSupport.executeAsyncVoid}
+ *       for {@code void} methods)</li>
+ *   <li>Generator methods (containing {@code yield return}) are transformed to
+ *       use {@link AsyncSupport#generateAsyncStream 
AsyncSupport.generateAsyncStream},
+ *       returning an {@link AsyncStream}{@code <T>}</li>
+ *   <li>The return type becomes {@link Awaitable}{@code <T>}
+ *       (or {@link AsyncStream}{@code <T>} for generators)</li>
+ * </ol>
+ * <p>
+ * This transformation runs during the {@link CompilePhase#CANONICALIZATION}
+ * phase — before type resolution, which allows the modified return types to
+ * participate in normal type checking.
+ *
+ * @see Async
+ * @see AsyncSupport
+ * @since 6.0.0
+ */
+@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
+public class AsyncASTTransformation extends AbstractASTTransformation {
+
+    private static final Class<?> MY_CLASS = Async.class;
+    private static final ClassNode MY_TYPE = ClassHelper.make(MY_CLASS);
+    private static final String MY_TYPE_NAME = "@" + 
MY_TYPE.getNameWithoutPackage();
+    private static final ClassNode AWAITABLE_TYPE = 
ClassHelper.make(Awaitable.class);
+    private static final ClassNode ASYNC_STREAM_TYPE = 
ClassHelper.make(AsyncStream.class);
+    private static final ClassNode ASYNC_UTILS_TYPE = 
ClassHelper.make(AsyncSupport.class);
+
+    @Override
+    public void visit(ASTNode[] nodes, SourceUnit source) {
+        init(nodes, source);
+        AnnotatedNode parent = (AnnotatedNode) nodes[1];
+        AnnotationNode anno = (AnnotationNode) nodes[0];
+        if (!MY_TYPE.equals(anno.getClassNode())) return;
+
+        if (!(parent instanceof MethodNode mNode)) return;
+
+        // Validate
+        if (mNode.isAbstract()) {
+            addError(MY_TYPE_NAME + " cannot be applied to abstract method '" 
+ mNode.getName() + "'", mNode);
+            return;
+        }
+        if ("<init>".equals(mNode.getName()) || 
"<clinit>".equals(mNode.getName())) {
+            addError(MY_TYPE_NAME + " cannot be applied to constructors", 
mNode);
+            return;
+        }
+        ClassNode originalReturnType = mNode.getReturnType();
+        if (AWAITABLE_TYPE.getName().equals(originalReturnType.getName())
+                || 
ASYNC_STREAM_TYPE.getName().equals(originalReturnType.getName())
+                || 
"java.util.concurrent.CompletableFuture".equals(originalReturnType.getName())) {
+            addError(MY_TYPE_NAME + " cannot be applied to a method that 
already returns an async type", mNode);
+            return;
+        }

Review Comment:
   The validation for “already returns an async type” only checks exact type 
names. This allows methods returning an `Awaitable`/`AsyncStream` subtype (e.g. 
`GroovyPromise`) to be annotated and then double-wrapped, which contradicts the 
restriction described in `@Async` Javadoc. Consider using 
`implementsInterface(AWAITABLE_TYPE)` / `isDerivedFrom(ASYNC_STREAM_TYPE)` (or 
similar) instead of name equality.



-- 
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