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

asf-gitbox-commits pushed a commit to branch GROOVY-12181
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 305bc11cc35353c9ef4f66793b9c540d5c483e3e
Author: Daniel Sun <[email protected]>
AuthorDate: Tue Jul 21 00:58:40 2026 +0900

    GROOVY-12181: Refactor async runtime and AST helpers introduced by 
GROOVY-9381
---
 src/main/java/groovy/concurrent/AwaitResult.java   |   5 +-
 src/main/java/groovy/concurrent/Awaitable.java     |   7 +-
 src/main/java/groovy/concurrent/package-info.java  |  17 +-
 .../apache/groovy/parser/antlr4/AstBuilder.java    |  49 +---
 .../groovy/runtime/async/AsyncExecutors.java       | 128 +++++++++
 .../apache/groovy/runtime/async/AsyncSupport.java  | 297 ++++++---------------
 .../groovy/runtime/async/AwaitCombinators.java     | 201 ++++++++++++++
 .../apache/groovy/runtime/async/GroovyPromise.java |   9 +-
 .../apache/groovy/runtime/async/package-info.java  |  20 +-
 .../groovy/transform/AsyncTransformHelper.java     | 124 ++++++---
 src/spec/doc/core-async-await.adoc                 |   4 +-
 src/test/groovy/groovy/AsyncAwaitTest.groovy       |  39 +++
 .../groovy/concurrent/AwaitResultTest.groovy       | 118 ++++++++
 .../runtime/async/AsyncSupportInternalsTest.groovy | 259 ++++++++++++++++++
 .../transform/AsyncTransformHelperTest.groovy      | 179 +++++++++++++
 15 files changed, 1134 insertions(+), 322 deletions(-)

diff --git a/src/main/java/groovy/concurrent/AwaitResult.java 
b/src/main/java/groovy/concurrent/AwaitResult.java
index f7f77879b9..eed54a524c 100644
--- a/src/main/java/groovy/concurrent/AwaitResult.java
+++ b/src/main/java/groovy/concurrent/AwaitResult.java
@@ -69,9 +69,8 @@ public final class AwaitResult<T> {
      * @param <T>   the value type
      * @return a success result wrapping the value
      */
-    @SuppressWarnings("unchecked")
-    public static <T> AwaitResult<T> success(Object value) {
-        return new AwaitResult<>((T) value, null, true);
+    public static <T> AwaitResult<T> success(T value) {
+        return new AwaitResult<>(value, null, true);
     }
 
     /**
diff --git a/src/main/java/groovy/concurrent/Awaitable.java 
b/src/main/java/groovy/concurrent/Awaitable.java
index 59f06f0876..bb941cd1e7 100644
--- a/src/main/java/groovy/concurrent/Awaitable.java
+++ b/src/main/java/groovy/concurrent/Awaitable.java
@@ -439,9 +439,10 @@ public interface Awaitable<T> {
      * Returns an {@code Awaitable} that completes with the result of the first
      * source that succeeds.  Individual failures are silently absorbed; only
      * when <em>all</em> sources have failed does the returned awaitable reject
-     * with an {@link IllegalStateException} whose
-     * {@linkplain Throwable#getSuppressed() suppressed} array contains every
-     * individual error.
+     * with an aggregate {@link java.util.concurrent.CompletionException}
+     * (message {@code "All N tasks failed"}, cause = first failure, remaining
+     * failures as suppressed). Under {@code await} exception transparency the
+     * cause is rethrown.
      * <p>
      * This is the Groovy equivalent of JavaScript's {@code Promise.any()}.
      * Contrast with {@link #any(Object...)} which returns the first result to
diff --git a/src/main/java/groovy/concurrent/package-info.java 
b/src/main/java/groovy/concurrent/package-info.java
index 1ffb533d6d..950b890619 100644
--- a/src/main/java/groovy/concurrent/package-info.java
+++ b/src/main/java/groovy/concurrent/package-info.java
@@ -21,10 +21,23 @@
  * Structured concurrency abstractions for asynchronous and parallel 
programming.
  *
  * <p>
- * Provides high-level concurrency primitives: {@link groovy.concurrent.Actor} 
(message-passing),
- * {@link groovy.concurrent.AsyncScope} (structured concurrency), {@link 
groovy.concurrent.Pool} (thread management),
+ * Language-facing async types:
+ * <ul>
+ *   <li>{@link groovy.concurrent.Awaitable} — async computation handle and
+ *       static combinator surface used with the {@code await} keyword</li>
+ *   <li>{@link groovy.concurrent.AwaitResult} — success/failure value from
+ *       {@link groovy.concurrent.Awaitable#allSettled(Object...)}</li>
+ *   <li>{@link groovy.concurrent.AsyncScope} — structured concurrency</li>
+ *   <li>{@link groovy.concurrent.AsyncChannel} — CSP-style async channel</li>
+ *   <li>{@link groovy.concurrent.AwaitableAdapter} /
+ *       {@link groovy.concurrent.AwaitableAdapterRegistry} — SPI for
+ *       third-party async types (RxJava, Reactor, …)</li>
+ * </ul>
+ * Additional high-level primitives: {@link groovy.concurrent.Actor}
+ * (message-passing), {@link groovy.concurrent.Pool} (thread management),
  * and {@link groovy.concurrent.ParallelScope} (combined async+parallel).
  * Supports virtual threads (JDK 21+) and fail-fast exception handling.
+ * Runtime implementation details live in {@code 
org.apache.groovy.runtime.async}.
  * </p>
  */
 package groovy.concurrent;
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java 
b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index deaaf9988d..a35a61c791 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -539,20 +539,7 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
         
visitAnnotationsOpt(ctx.annotationsOpt()).forEach(forStatement::addStatementAnnotation);
 
         if (isForAwait) {
-            // Transform collection expression: wrap in 
AsyncSupport.toIterable()
-            // and wrap the loop in try/finally to ensure cleanup on 
break/exception
-            Expression original = forStatement.getCollectionExpression();
-            String tempVar = "__forAwaitSource" + ctx.hashCode();
-            Expression toIterableCall = 
AsyncTransformHelper.buildToIterableCall(original);
-
-            // var $temp = AsyncSupport.toIterable(original)
-            Statement declStmt = stmt(declX(varX(tempVar), toIterableCall));
-            forStatement.setCollectionExpression(varX(tempVar));
-
-            // try { for (...) { body } } finally { 
AsyncSupport.closeIterable($temp) }
-            Statement finallyStmt = 
stmt(AsyncTransformHelper.buildCloseIterableCall(varX(tempVar)));
-            TryCatchStatement tryCatch = new TryCatchStatement(forStatement, 
finallyStmt);
-            return configureAST(block(declStmt, tryCatch), ctx);
+            return 
configureAST(AsyncTransformHelper.wrapForAwaitLoop(forStatement), ctx);
         }
 
         return forStatement;
@@ -3090,39 +3077,7 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
     @Override
     public Expression visitAsyncClosureExprAlt(final 
AsyncClosureExprAltContext ctx) {
         ClosureExpression closure = 
this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression());
-        boolean hasYieldReturn = 
AsyncTransformHelper.containsYieldReturn(closure.getCode());
-        boolean hasDefer = 
AsyncTransformHelper.containsDefer(closure.getCode());
-
-        if (hasDefer) {
-            Statement wrappedBody = 
AsyncTransformHelper.wrapWithDeferScope(closure.getCode());
-            ClosureExpression newClosure = new 
ClosureExpression(closure.getParameters(), wrappedBody);
-            newClosure.setVariableScope(closure.getVariableScope());
-            newClosure.setSourcePosition(closure);
-            closure = newClosure;
-        }
-
-        if (hasYieldReturn) {
-            // Inject synthetic $__asyncGen__ as first parameter
-            Parameter genParam = AsyncTransformHelper.createGenParam();
-            Parameter[] existingParams = closure.getParameters();
-            boolean hasUserParams = existingParams != null && 
existingParams.length > 0;
-            Parameter[] newParams;
-            if (hasUserParams) {
-                newParams = new Parameter[existingParams.length + 1];
-                newParams[0] = genParam;
-                System.arraycopy(existingParams, 0, newParams, 1, 
existingParams.length);
-            } else {
-                newParams = new Parameter[]{genParam};
-            }
-            ClosureExpression genClosure = new ClosureExpression(newParams, 
closure.getCode());
-            genClosure.setVariableScope(closure.getVariableScope());
-            genClosure.setSourcePosition(closure);
-            return configureAST(AsyncTransformHelper.buildAsyncGeneratorCall(
-                    new ArgumentListExpression(genClosure)), ctx);
-        } else {
-            return configureAST(AsyncTransformHelper.buildAsyncCall(
-                    new ArgumentListExpression(closure)), ctx);
-        }
+        return 
configureAST(AsyncTransformHelper.transformAsyncClosure(closure), ctx);
     }
 
     @Override
diff --git a/src/main/java/org/apache/groovy/runtime/async/AsyncExecutors.java 
b/src/main/java/org/apache/groovy/runtime/async/AsyncExecutors.java
new file mode 100644
index 0000000000..8114747b17
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncExecutors.java
@@ -0,0 +1,128 @@
+/*
+ *  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 java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Executor and scheduler configuration for the async runtime.
+ * <p>
+ * Package-private implementation detail of {@link AsyncSupport}. On 
JDK&nbsp;21+
+ * the default executor is a virtual-thread-per-task executor; earlier JDKs 
use a
+ * bounded cached daemon pool sized by {@code groovy.async.parallelism}.
+ *
+ * @since 6.0.0
+ */
+final class AsyncExecutors {
+
+    private static final boolean VIRTUAL_THREADS_AVAILABLE;
+    private static final Executor VIRTUAL_THREAD_EXECUTOR;
+    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;
+
+        int maxThreads = getIntegerSafe("groovy.async.parallelism", 256);
+        if (!VIRTUAL_THREADS_AVAILABLE) {
+            FALLBACK_EXECUTOR = new ThreadPoolExecutor(
+                    0, maxThreads,
+                    60L, TimeUnit.SECONDS,
+                    new SynchronousQueue<>(),
+                    r -> {
+                        Thread t = new Thread(r);
+                        t.setDaemon(true);
+                        @SuppressWarnings("deprecation")
+                        long id = t.getId();
+                        t.setName("groovy-async-" + id);
+                        return t;
+                    },
+                    new ThreadPoolExecutor.CallerRunsPolicy());
+        } else {
+            FALLBACK_EXECUTOR = null;
+        }
+    }
+
+    private static volatile Executor defaultExecutor = createDefaultExecutor();
+
+    private static final ScheduledExecutorService SCHEDULER =
+            Executors.newSingleThreadScheduledExecutor(r -> {
+                Thread t = new Thread(r, "groovy-async-scheduler");
+                t.setDaemon(true);
+                return t;
+            });
+
+    private AsyncExecutors() { }
+
+    static boolean isVirtualThreadsAvailable() {
+        return VIRTUAL_THREADS_AVAILABLE;
+    }
+
+    static Executor getExecutor() {
+        return defaultExecutor;
+    }
+
+    /**
+     * Sets the executor used for async tasks. Passing {@code null} restores 
the
+     * platform default (virtual threads on JDK&nbsp;21+, cached pool 
otherwise).
+     */
+    static void setExecutor(Executor executor) {
+        defaultExecutor = executor != null ? executor : 
createDefaultExecutor();
+    }
+
+    static void resetExecutor() {
+        defaultExecutor = createDefaultExecutor();
+    }
+
+    static ScheduledExecutorService getScheduler() {
+        return SCHEDULER;
+    }
+
+    private static Executor createDefaultExecutor() {
+        return VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : 
FALLBACK_EXECUTOR;
+    }
+
+    private static int getIntegerSafe(String name, int defaultValue) {
+        try {
+            return Integer.getInteger(name, defaultValue);
+        } catch (SecurityException ignore) {
+            return defaultValue;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java 
b/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java
index 588f5a3084..3417fc795f 100644
--- a/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java
@@ -23,13 +23,9 @@ import groovy.concurrent.Awaitable;
 import groovy.concurrent.AwaitableAdapterRegistry;
 
 import java.io.Closeable;
-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.ArrayDeque;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Deque;
@@ -43,26 +39,22 @@ 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;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
 /**
  * Internal runtime support for the {@code async}/{@code await}/{@code defer} 
language features.
  * <p>
- * This class contains the actual implementation invoked by compiler-generated 
code.
- * User code should prefer the static methods on {@link 
groovy.concurrent.Awaitable}
- * for combinators and configuration.
+ * This class is the entry point invoked by compiler-generated code and also
+ * exposes the combinator and configuration surface used by
+ * {@link groovy.concurrent.Awaitable}. Combinator algorithms live in
+ * {@link AwaitCombinators}; executor/scheduler configuration lives in
+ * {@link AsyncExecutors}.
  * <p>
  * <b>Thread pool configuration:</b>
  * <ul>
@@ -71,7 +63,8 @@ import java.util.function.Supplier;
  *   <li>On JDK 17-20 the fallback is a cached daemon thread pool
  *       whose maximum size is controlled by the system property
  *       {@code groovy.async.parallelism} (default: {@code 256}).</li>
- *   <li>The executor can be overridden at any time via {@link 
#setExecutor}.</li>
+ *   <li>The executor can be overridden via {@link #setExecutor}; pass
+ *       {@code null} to restore the platform default.</li>
  * </ul>
  * <p>
  * <b>Exception handling</b> follows a transparency principle: the
@@ -82,82 +75,42 @@ import java.util.function.Supplier;
  */
 public class AsyncSupport {
 
-    private static final boolean VIRTUAL_THREADS_AVAILABLE;
-    private static final Executor VIRTUAL_THREAD_EXECUTOR;
-    private static final int FALLBACK_MAX_THREADS;
-    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_MAX_THREADS = getIntegerSafe("groovy.async.parallelism", 256);
-        if (!VIRTUAL_THREADS_AVAILABLE) {
-            FALLBACK_EXECUTOR = new ThreadPoolExecutor(
-                    0, FALLBACK_MAX_THREADS,
-                    60L, TimeUnit.SECONDS,
-                    new SynchronousQueue<>(),
-                    r -> {
-                        Thread t = new Thread(r);
-                        t.setDaemon(true);
-                        @SuppressWarnings("deprecation")
-                        long id = t.getId();
-                        t.setName("groovy-async-" + id);
-                        return t;
-                    },
-                    new ThreadPoolExecutor.CallerRunsPolicy());
-        } else {
-            FALLBACK_EXECUTOR = null;
-        }
-    }
-
-    private static volatile Executor defaultExecutor = createDefaultExecutor();
-
-    private static final ScheduledExecutorService SCHEDULER =
-            Executors.newSingleThreadScheduledExecutor(r -> {
-                Thread t = new Thread(r, "groovy-async-scheduler");
-                t.setDaemon(true);
-                return t;
-            });
-
     private AsyncSupport() { }
 
     // ---- executor configuration -----------------------------------------
 
     /** Returns the shared scheduler for delays, timeouts, and scope 
deadlines. */
     public static ScheduledExecutorService getScheduler() {
-        return SCHEDULER;
+        return AsyncExecutors.getScheduler();
     }
 
     /** Returns {@code true} if running on JDK 21+ with virtual thread 
support. */
     public static boolean isVirtualThreadsAvailable() {
-        return VIRTUAL_THREADS_AVAILABLE;
+        return AsyncExecutors.isVirtualThreadsAvailable();
     }
 
     /** Returns the current executor used for async tasks. */
     public static Executor getExecutor() {
-        return defaultExecutor;
+        return AsyncExecutors.getExecutor();
     }
 
-    /** Sets the executor used for async tasks. */
+    /**
+     * Sets the executor used for async tasks.
+     * <p>
+     * Pass {@code null} to restore the platform default (virtual threads on
+     * JDK&nbsp;21+, cached daemon pool otherwise). The change takes effect
+     * for subsequent {@code async} launches; in-flight tasks keep the
+     * executor that started them.
+     *
+     * @param executor the executor to use, or {@code null} to reset
+     */
     public static void setExecutor(Executor executor) {
-        defaultExecutor = Objects.requireNonNull(executor, "executor must not 
be null");
+        AsyncExecutors.setExecutor(executor);
     }
 
-    /** Resets the executor to the default (virtual threads on JDK 21+, cached 
pool otherwise). */
+    /** Resets the executor to the platform default. Equivalent to {@code 
setExecutor(null)}. */
     public static void resetExecutor() {
-        defaultExecutor = createDefaultExecutor();
+        AsyncExecutors.resetExecutor();
     }
 
     // ---- await overloads ------------------------------------------------
@@ -211,7 +164,11 @@ public class AsyncSupport {
 
     /**
      * Awaits an arbitrary object by adapting it via {@link 
Awaitable#from(Object)}.
-     * This is the fallback overload called by compiler-generated await 
expressions.
+     * <p>
+     * This is the single-argument overload preferred by compiler-generated 
code
+     * (via {@link #awaitAny(Object)}) so overload resolution does not have to
+     * choose among {@code Awaitable}, {@code CompletionStage}, and {@code 
Future}
+     * when a value implements more than one of them.
      */
     @SuppressWarnings("unchecked")
     public static <T> T await(Object source) {
@@ -223,6 +180,22 @@ public class AsyncSupport {
         return await(Awaitable.from(source));
     }
 
+    /**
+     * Compiler-facing await entry point that always takes {@link Object}.
+     * <p>
+     * Generated code calls this method so the bytecode does not need an
+     * explicit cast-to-{@code Object} to force dynamic overload selection.
+     * Behaviour is identical to {@link #await(Object)}.
+     *
+     * @param source the value to await; may be {@code null}
+     * @param <T>    the result type
+     * @return the awaited result
+     * @since 6.0.0
+     */
+    public static <T> T awaitAny(Object source) {
+        return await(source);
+    }
+
     // ---- async execution ------------------------------------------------
 
     /**
@@ -231,7 +204,7 @@ public class AsyncSupport {
      */
     public static <T> Awaitable<T> executeAsync(Supplier<T> supplier, Executor 
executor) {
         Objects.requireNonNull(supplier, "supplier must not be null");
-        Executor targetExecutor = executor != null ? executor : 
defaultExecutor;
+        Executor targetExecutor = executor != null ? executor : 
AsyncExecutors.getExecutor();
         return GroovyPromise.of(CompletableFuture.supplyAsync(() -> {
             try {
                 return supplier.get();
@@ -245,11 +218,12 @@ public class AsyncSupport {
      * Executes the given supplier asynchronously using the default executor.
      */
     public static <T> Awaitable<T> async(Supplier<T> supplier) {
-        return executeAsync(supplier, defaultExecutor);
+        return executeAsync(supplier, AsyncExecutors.getExecutor());
     }
 
     /**
-     * Lightweight task spawn. Executes the supplier asynchronously using the 
default executor.
+     * Lightweight task spawn. Alias of {@link #async(Supplier)} for Go-style
+     * ergonomics; semantics are identical.
      */
     public static <T> Awaitable<T> go(Supplier<T> supplier) {
         return async(supplier);
@@ -286,7 +260,6 @@ public class AsyncSupport {
      * subsequent exceptions are added as suppressed. If a deferred action 
returns
      * a Future/Awaitable, the result is awaited before continuing.
      */
-    @SuppressWarnings("unchecked")
     public static void executeDeferScope(Deque<Callable<?>> scope) {
         if (scope == null || scope.isEmpty()) return;
         Throwable firstError = null;
@@ -349,11 +322,10 @@ public class AsyncSupport {
      * @param <T>  the element type
      * @return an Iterable that yields values from the generator
      */
-    @SuppressWarnings("unchecked")
     public static <T> Iterable<T> asyncGenerator(Consumer<Object> body) {
         Objects.requireNonNull(body, "body must not be null");
         GeneratorBridge<T> bridge = new GeneratorBridge<>();
-        defaultExecutor.execute(() -> {
+        AsyncExecutors.getExecutor().execute(() -> {
             try {
                 body.accept(bridge);
                 bridge.complete();
@@ -387,14 +359,15 @@ public class AsyncSupport {
             return () -> iter;
         }
         if (source instanceof Object[]) return (Iterable<T>) 
Arrays.asList((Object[]) source);
-        // Try adapter registry
         return AwaitableAdapterRegistry.toIterable(source);
     }
 
     /**
      * Closes a source if it implements {@link Closeable} or
      * {@link AutoCloseable}. Called by compiler-generated finally block
-     * in {@code for await} loops.
+     * in {@code for await} loops. Cleanup exceptions are swallowed so they
+     * cannot mask the original loop error; prefer robust {@code close()}
+     * implementations.
      */
     public static void closeIterable(Object source) {
         if (source instanceof Closeable c) {
@@ -410,31 +383,31 @@ public class AsyncSupport {
         }
     }
 
-    // ---- combinators ----------------------------------------------------
+    // ---- combinators (delegate to AwaitCombinators) ---------------------
 
     /**
      * Waits for all given sources to complete, returning their results in 
order.
-     * Multi-arg {@code await(a, b, c)} desugars to this.
+     * Multi-arg {@code await(a, b, c)} desugars to the non-blocking
+     * {@link #allAsync(Object...)} form, then awaits it.
      */
     public static <T> List<T> all(Object... sources) {
-        CompletableFuture<?>[] futures = toCombinatorFutures(sources);
-        CompletableFuture.allOf(futures).join();
-        return getJoinedResults(futures);
+        return AwaitCombinators.all(sources);
     }
 
     /**
      * Returns the result of the first source to complete (success or failure).
      */
     public static <T> T any(Object... sources) {
-        return 
AsyncSupport.await(AsyncSupport.<T>anyAsync(sources).toCompletableFuture());
+        return AwaitCombinators.any(sources);
     }
 
     /**
      * Returns the result of the first source to complete 
<em>successfully</em>.
-     * Only fails when all sources fail.
+     * Only fails when all sources fail (aggregate {@link CompletionException};
+     * {@code await} transparency rethrows the cause).
      */
     public static <T> T first(Object... sources) {
-        return 
AsyncSupport.await(AsyncSupport.<T>firstAsync(sources).toCompletableFuture());
+        return AwaitCombinators.first(sources);
     }
 
     /**
@@ -442,136 +415,27 @@ public class AsyncSupport {
      * {@link AwaitResult} without throwing.
      */
     public static List<AwaitResult<Object>> allSettled(Object... sources) {
-        return 
AsyncSupport.await(allSettledAsync(sources).toCompletableFuture());
+        return AwaitCombinators.allSettled(sources);
     }
 
-    // ---- async combinator variants (return Awaitable, non-blocking) ------
-
     /** Non-blocking variant of {@link #all} — returns an Awaitable. */
     public static Awaitable<List<Object>> allAsync(Object... sources) {
-        CompletableFuture<?>[] futures = toCombinatorFutures(sources);
-
-        // allOf is naturally fail-fast: it completes as soon as any
-        // source fails OR all sources succeed.  We track the first
-        // failure explicitly because allOf doesn't guarantee which
-        // exception propagates when multiple futures fail.
-        var firstError = new AtomicReference<Throwable>();
-        for (CompletableFuture<?> f : futures) {
-            f.whenComplete((v, e) -> {
-                if (e != null) firstError.compareAndSet(null, e);
-            });
-        }
-
-        CompletableFuture<List<Object>> combined = 
CompletableFuture.allOf(futures)
-                .thenApply(v -> getJoinedResults(futures));
-
-        // Replace allOf's arbitrary exception with the temporally-first one
-        CompletableFuture<List<Object>> withFirstError = 
combined.exceptionally(e -> {
-            Throwable first = firstError.get();
-            if (first != null && first != e && first != e.getCause()) {
-                throw first instanceof CompletionException ce ? ce : new 
CompletionException(first);
-            }
-            throw e instanceof CompletionException ce ? ce : new 
CompletionException(e);
-        });
-        return GroovyPromise.of(withFirstError);
+        return AwaitCombinators.allAsync(sources);
     }
 
     /** Non-blocking variant of {@link #any} — returns an Awaitable. */
-    @SuppressWarnings("unchecked")
     public static <T> Awaitable<T> anyAsync(Object... sources) {
-        return (Awaitable<T>) 
GroovyPromise.of(CompletableFuture.anyOf(toCombinatorFutures(sources)));
+        return AwaitCombinators.anyAsync(sources);
     }
 
     /** Non-blocking variant of {@link #first} — returns an Awaitable. */
-    @SuppressWarnings("unchecked")
     public static <T> Awaitable<T> firstAsync(Object... sources) {
-        CompletableFuture<T>[] futures = toFirstCombinatorFutures(sources);
-        CompletableFuture<T> result = new CompletableFuture<>();
-        var remainingFailures = new AtomicInteger(futures.length);
-        List<Throwable> errors = Collections.synchronizedList(new 
ArrayList<>(futures.length));
-        for (CompletableFuture<T> future : futures) {
-            future.whenComplete((value, error) -> {
-                if (error == null) {
-                    result.complete(value);
-                    return;
-                }
-                errors.add(error);
-                if (remainingFailures.decrementAndGet() == 0) {
-                    
result.completeExceptionally(aggregateFirstFailures(futures.length, errors));
-                }
-            });
-        }
-        return GroovyPromise.of(result);
+        return AwaitCombinators.firstAsync(sources);
     }
 
     /** Non-blocking variant of {@link #allSettled} — returns an Awaitable. */
     public static Awaitable<List<AwaitResult<Object>>> 
allSettledAsync(Object... sources) {
-        CompletableFuture<?>[] futures = toCombinatorFutures(sources);
-        CompletableFuture<List<AwaitResult<Object>>> combined = 
CompletableFuture.allOf(
-                Arrays.stream(futures)
-                    .map(future -> future.handle((value, error) -> null))
-                    .toArray(CompletableFuture[]::new)
-            )
-                .thenApply(v -> getAwaitResults(futures));
-        return GroovyPromise.of(combined);
-    }
-
-    @SuppressWarnings("unchecked")
-    private static <T> List<T> getJoinedResults(CompletableFuture<?>[] 
futures) {
-        List<T> results = new ArrayList<>(futures.length);
-        for (CompletableFuture<?> future : futures) {
-            results.add((T) future.join());
-        }
-        return results;
-    }
-
-    private static List<AwaitResult<Object>> 
getAwaitResults(CompletableFuture<?>[] futures) {
-        List<AwaitResult<Object>> results = new ArrayList<>(futures.length);
-        for (CompletableFuture<?> f : futures) {
-            try {
-                results.add(AwaitResult.success(f.join()));
-            } catch (CompletionException e) {
-                results.add(AwaitResult.failure(unwrap(e)));
-            } catch (CancellationException e) {
-                results.add(AwaitResult.failure(e));
-            }
-        }
-        return results;
-    }
-
-    private static CompletableFuture<?>[] toCombinatorFutures(Object... 
sources) {
-        return Arrays.stream(sources)
-                .map(source -> Awaitable.from(source).toCompletableFuture())
-                .toArray(CompletableFuture[]::new);
-    }
-
-    @SuppressWarnings("unchecked")
-    private static <T> CompletableFuture<T>[] 
toFirstCombinatorFutures(Object... sources) {
-        validateFirstSources(sources);
-        return (CompletableFuture<T>[]) toCombinatorFutures(sources);
-    }
-
-    private static void validateFirstSources(Object[] sources) {
-        if (sources == null) {
-            throw new IllegalArgumentException("sources must not be null");
-        }
-        if (sources.length == 0) {
-            throw new IllegalArgumentException("sources must not be empty");
-        }
-        for (Object source : sources) {
-            if (source == null) {
-                throw new IllegalArgumentException("sources must not contain 
null elements");
-            }
-        }
-    }
-
-    private static CompletionException aggregateFirstFailures(int sourceCount, 
List<Throwable> errors) {
-        CompletionException aggregate = new CompletionException(
-                "All " + sourceCount + " tasks failed", errors.get(0));
-        for (int i = 1; i < errors.size(); i++) {
-            aggregate.addSuppressed(errors.get(i));
-        }
-        return aggregate;
+        return AwaitCombinators.allSettledAsync(sources);
     }
 
     // ---- delay and timeout ----------------------------------------------
@@ -580,27 +444,26 @@ public class AsyncSupport {
      * Returns an Awaitable that completes after the specified delay.
      */
     public static Awaitable<Void> delay(long millis) {
-        CompletableFuture<Void> future = new CompletableFuture<>();
-        SCHEDULER.schedule(() -> future.complete(null), millis, 
TimeUnit.MILLISECONDS);
-        return GroovyPromise.of(future);
+        return delay(millis, TimeUnit.MILLISECONDS);
     }
 
     /** Delay with explicit time unit. */
     public static Awaitable<Void> delay(long duration, TimeUnit unit) {
         CompletableFuture<Void> future = new CompletableFuture<>();
-        SCHEDULER.schedule(() -> future.complete(null), duration, unit);
+        AsyncExecutors.getScheduler().schedule(() -> future.complete(null), 
duration, unit);
         return GroovyPromise.of(future);
     }
 
     /**
      * Wraps a source with a timeout. If the source does not complete within
-     * the specified time, the returned Awaitable fails with {@link 
TimeoutException}.
+     * the specified time, the returned Awaitable fails with {@link 
TimeoutException}
+     * and the underlying computation is cancelled.
      */
     @SuppressWarnings("unchecked")
     public static <T> Awaitable<T> orTimeout(Object source, long timeout, 
TimeUnit unit) {
         CompletableFuture<T> future = (CompletableFuture<T>) 
Awaitable.from(source).toCompletableFuture();
         CompletableFuture<T> result = new CompletableFuture<>();
-        ScheduledFuture<?> timer = SCHEDULER.schedule(() -> {
+        ScheduledFuture<?> timer = AsyncExecutors.getScheduler().schedule(() 
-> {
             if (!result.isDone()) {
                 result.completeExceptionally(new TimeoutException("Timed out 
after " + timeout + " " + unit));
                 future.cancel(true);
@@ -621,12 +484,13 @@ public class AsyncSupport {
 
     /**
      * Wraps a source with a timeout that uses a fallback value instead of 
throwing.
+     * On timeout the underlying computation is cancelled.
      */
     @SuppressWarnings("unchecked")
     public static <T> Awaitable<T> completeOnTimeout(Object source, T 
fallback, long timeout, TimeUnit unit) {
         CompletableFuture<T> future = (CompletableFuture<T>) 
Awaitable.from(source).toCompletableFuture();
         CompletableFuture<T> result = new CompletableFuture<>();
-        ScheduledFuture<?> timer = SCHEDULER.schedule(() -> {
+        ScheduledFuture<?> timer = AsyncExecutors.getScheduler().schedule(() 
-> {
             if (!result.isDone()) {
                 result.complete(fallback);
                 future.cancel(true);
@@ -658,6 +522,11 @@ public class AsyncSupport {
         return null; // unreachable
     }
 
+    /**
+     * Strips JDK wrapper layers ({@link CompletionException},
+     * {@link ExecutionException}, {@link InvocationTargetException},
+     * {@link UndeclaredThrowableException}) to expose the original cause.
+     */
     public static Throwable unwrap(Throwable t) {
         while ((t instanceof CompletionException || t instanceof 
ExecutionException
                 || t instanceof InvocationTargetException
@@ -675,22 +544,10 @@ public class AsyncSupport {
 
     // ---- internal utilities ---------------------------------------------
 
-    private static Executor createDefaultExecutor() {
-        return VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : 
FALLBACK_EXECUTOR;
-    }
-
     private static CancellationException interruptedAwait(String message, 
InterruptedException cause) {
         Thread.currentThread().interrupt();
         CancellationException cancellation = new 
CancellationException(message);
         cancellation.initCause(cause);
         return cancellation;
     }
-
-    private static int getIntegerSafe(String name, int defaultValue) {
-        try {
-            return Integer.getInteger(name, defaultValue);
-        } catch (SecurityException ignore) {
-            return defaultValue;
-        }
-    }
 }
diff --git 
a/src/main/java/org/apache/groovy/runtime/async/AwaitCombinators.java 
b/src/main/java/org/apache/groovy/runtime/async/AwaitCombinators.java
new file mode 100644
index 0000000000..9e103818a8
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AwaitCombinators.java
@@ -0,0 +1,201 @@
+/*
+ *  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.AwaitResult;
+import groovy.concurrent.Awaitable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Combinators for joining multiple async sources ({@code all}, {@code any},
+ * {@code first}, {@code allSettled}).
+ * <p>
+ * Package-private implementation detail of {@link AsyncSupport}. Blocking
+ * variants are thin wrappers over the non-blocking ones via
+ * {@link AsyncSupport#await(Object)}, so each combinator has a single
+ * implementation of its joining algorithm.
+ *
+ * @since 6.0.0
+ */
+final class AwaitCombinators {
+
+    private AwaitCombinators() { }
+
+    // ---- blocking -------------------------------------------------------
+
+    @SuppressWarnings("unchecked")
+    static <T> List<T> all(Object... sources) {
+        return (List<T>) AsyncSupport.await(allAsync(sources));
+    }
+
+    static <T> T any(Object... sources) {
+        return AsyncSupport.await(AwaitCombinators.<T>anyAsync(sources));
+    }
+
+    static <T> T first(Object... sources) {
+        return AsyncSupport.await(AwaitCombinators.<T>firstAsync(sources));
+    }
+
+    static List<AwaitResult<Object>> allSettled(Object... sources) {
+        return AsyncSupport.await(allSettledAsync(sources));
+    }
+
+    // ---- non-blocking ---------------------------------------------------
+
+    static Awaitable<List<Object>> allAsync(Object... sources) {
+        CompletableFuture<?>[] futures = toFutures(sources);
+
+        // allOf is fail-fast but does not guarantee which exception propagates
+        // when several futures fail; track the temporally-first error 
ourselves.
+        var firstError = new AtomicReference<Throwable>();
+        for (CompletableFuture<?> future : futures) {
+            future.whenComplete((value, error) -> {
+                if (error != null) {
+                    firstError.compareAndSet(null, error);
+                }
+            });
+        }
+
+        CompletableFuture<List<Object>> combined = 
CompletableFuture.allOf(futures)
+                .thenApply(ignored -> getJoinedResults(futures));
+
+        CompletableFuture<List<Object>> withFirstError = 
combined.exceptionally(error -> {
+            Throwable first = firstError.get();
+            if (first != null && first != error && first != error.getCause()) {
+                throw asCompletionException(first);
+            }
+            throw asCompletionException(error);
+        });
+        return GroovyPromise.of(withFirstError);
+    }
+
+    @SuppressWarnings("unchecked")
+    static <T> Awaitable<T> anyAsync(Object... sources) {
+        return (Awaitable<T>) 
GroovyPromise.of(CompletableFuture.anyOf(toFutures(sources)));
+    }
+
+    @SuppressWarnings("unchecked")
+    static <T> Awaitable<T> firstAsync(Object... sources) {
+        validateNonEmptySources(sources);
+        CompletableFuture<T>[] futures = (CompletableFuture<T>[]) 
toFutures(sources);
+        CompletableFuture<T> result = new CompletableFuture<>();
+        var remainingFailures = new AtomicInteger(futures.length);
+        List<Throwable> errors = Collections.synchronizedList(new 
ArrayList<>(futures.length));
+        for (CompletableFuture<T> future : futures) {
+            future.whenComplete((value, error) -> {
+                if (error == null) {
+                    result.complete(value);
+                    return;
+                }
+                errors.add(error);
+                if (remainingFailures.decrementAndGet() == 0) {
+                    
result.completeExceptionally(aggregateFirstFailures(futures.length, errors));
+                }
+            });
+        }
+        return GroovyPromise.of(result);
+    }
+
+    static Awaitable<List<AwaitResult<Object>>> allSettledAsync(Object... 
sources) {
+        CompletableFuture<?>[] futures = toFutures(sources);
+        CompletableFuture<List<AwaitResult<Object>>> combined = 
CompletableFuture.allOf(
+                        Arrays.stream(futures)
+                                .map(future -> future.handle((value, error) -> 
null))
+                                .toArray(CompletableFuture[]::new))
+                .thenApply(ignored -> getAwaitResults(futures));
+        return GroovyPromise.of(combined);
+    }
+
+    // ---- helpers --------------------------------------------------------
+
+    private static CompletableFuture<?>[] toFutures(Object... sources) {
+        if (sources == null) {
+            return new CompletableFuture<?>[0];
+        }
+        CompletableFuture<?>[] futures = new 
CompletableFuture<?>[sources.length];
+        for (int i = 0; i < sources.length; i++) {
+            futures[i] = Awaitable.from(sources[i]).toCompletableFuture();
+        }
+        return futures;
+    }
+
+    private static void validateNonEmptySources(Object[] sources) {
+        if (sources == null) {
+            throw new IllegalArgumentException("sources must not be null");
+        }
+        if (sources.length == 0) {
+            throw new IllegalArgumentException("sources must not be empty");
+        }
+        for (Object source : sources) {
+            if (source == null) {
+                throw new IllegalArgumentException("sources must not contain 
null elements");
+            }
+        }
+    }
+
+    /**
+     * Builds the aggregate {@link CompletionException} for {@code first}
+     * when every source fails. The first failure is the cause; remaining
+     * failures are attached as suppressed exceptions.
+     */
+    private static CompletionException aggregateFirstFailures(int sourceCount, 
List<Throwable> errors) {
+        CompletionException aggregate = new CompletionException(
+                "All " + sourceCount + " tasks failed", errors.get(0));
+        for (int i = 1; i < errors.size(); i++) {
+            aggregate.addSuppressed(errors.get(i));
+        }
+        return aggregate;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> List<T> getJoinedResults(CompletableFuture<?>[] 
futures) {
+        List<T> results = new ArrayList<>(futures.length);
+        for (CompletableFuture<?> future : futures) {
+            results.add((T) future.join());
+        }
+        return results;
+    }
+
+    private static List<AwaitResult<Object>> 
getAwaitResults(CompletableFuture<?>[] futures) {
+        List<AwaitResult<Object>> results = new ArrayList<>(futures.length);
+        for (CompletableFuture<?> future : futures) {
+            try {
+                results.add(AwaitResult.success(future.join()));
+            } catch (CompletionException e) {
+                results.add(AwaitResult.failure(AsyncSupport.unwrap(e)));
+            } catch (CancellationException e) {
+                results.add(AwaitResult.failure(e));
+            }
+        }
+        return results;
+    }
+
+    private static CompletionException asCompletionException(Throwable error) {
+        return error instanceof CompletionException ce ? ce : new 
CompletionException(error);
+    }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java 
b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
index 044b948d0f..fa8bdf6803 100644
--- a/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
+++ b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
@@ -18,7 +18,6 @@
  */
 package org.apache.groovy.runtime.async;
 
-// AsyncContext support reserved for future enhancement
 import groovy.concurrent.Awaitable;
 
 import java.util.Objects;
@@ -179,9 +178,7 @@ public class GroovyPromise<T> implements Awaitable<T> {
      * {@inheritDoc}
      * <p>
      * Returns a new {@code GroovyPromise} whose result is obtained by applying
-     * the given function to this promise's result.  The current
-     * {@code AsyncContext} snapshot is captured when the continuation is
-     * registered and restored when it executes.
+     * the given function to this promise's result when it completes.
      */
     @Override
     public <U> Awaitable<U> then(Function<? super T, ? extends U> fn) {
@@ -193,8 +190,6 @@ public class GroovyPromise<T> implements Awaitable<T> {
      * <p>
      * Returns a new {@code GroovyPromise} that is the result of composing this
      * promise with the async function, enabling flat-mapping of awaitables.
-     * The current {@code AsyncContext} snapshot is captured when the
-     * continuation is registered and restored when it executes.
      */
     @Override
     public <U> Awaitable<U> thenCompose(Function<? super T, ? extends 
Awaitable<U>> fn) {
@@ -208,8 +203,6 @@ public class GroovyPromise<T> implements Awaitable<T> {
      * Returns a new {@code GroovyPromise} that handles exceptions thrown by 
this promise.
      * The throwable passed to the handler is deeply unwrapped to strip JDK
      * wrapper layers ({@code CompletionException}, {@code 
ExecutionException}).
-     * The handler runs with the {@code AsyncContext} snapshot that was active
-     * when the recovery continuation was registered.
      */
     @Override
     public Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn) {
diff --git a/src/main/java/org/apache/groovy/runtime/async/package-info.java 
b/src/main/java/org/apache/groovy/runtime/async/package-info.java
index f40eece142..5b03865bd0 100644
--- a/src/main/java/org/apache/groovy/runtime/async/package-info.java
+++ b/src/main/java/org/apache/groovy/runtime/async/package-info.java
@@ -18,6 +18,24 @@
  */
 
 /**
- * Runtime support for async/await expressions. Provides continuation-passing 
style transformation and execution framework.
+ * Runtime support for Groovy's {@code async}/{@code await}/{@code defer}
+ * language features.
+ * <p>
+ * Layout:
+ * <ul>
+ *   <li>{@link org.apache.groovy.runtime.async.AsyncSupport} — public entry
+ *       point used by compiler-generated code and by
+ *       {@link groovy.concurrent.Awaitable}</li>
+ *   <li>{@code AsyncExecutors} — executor/scheduler configuration</li>
+ *   <li>{@code AwaitCombinators} — {@code all}/{@code any}/{@code first}/
+ *       {@code allSettled} joining algorithms</li>
+ *   <li>{@link org.apache.groovy.runtime.async.GroovyPromise} — default
+ *       {@link groovy.concurrent.Awaitable} implementation</li>
+ *   <li>{@link org.apache.groovy.runtime.async.GeneratorBridge} —
+ *       producer/consumer bridge for {@code yield return}</li>
+ *   <li>{@link org.apache.groovy.runtime.async.DefaultAsyncScope},
+ *       {@link org.apache.groovy.runtime.async.DefaultAsyncChannel} —
+ *       structured concurrency and CSP channel implementations</li>
+ * </ul>
  */
 package org.apache.groovy.runtime.async;
diff --git 
a/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java 
b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
index f4275e6246..3922cf1320 100644
--- a/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
@@ -22,20 +22,21 @@ import groovy.concurrent.Awaitable;
 import org.apache.groovy.runtime.async.AsyncSupport;
 import org.codehaus.groovy.ast.ClassHelper;
 import org.codehaus.groovy.ast.ClassNode;
-import org.codehaus.groovy.ast.CodeVisitorSupport;
 import org.codehaus.groovy.ast.Parameter;
 import org.codehaus.groovy.ast.expr.ArgumentListExpression;
 import org.codehaus.groovy.ast.expr.ClosureExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.StaticMethodCallExpression;
 import org.codehaus.groovy.ast.query.AstQuery;
+import org.codehaus.groovy.ast.stmt.ForStatement;
 import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.ast.stmt.TryCatchStatement;
+
+import java.util.concurrent.atomic.AtomicLong;
 
 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.castX;
-import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.tryCatchS;
@@ -44,7 +45,9 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
 /**
  * Shared AST utilities for the {@code async}/{@code await}/{@code defer} 
language features.
  * <p>
- * Centralises AST node construction for the parser ({@code AstBuilder}).
+ * Centralises AST node construction and higher-level rewrites used by the
+ * parser ({@code AstBuilder}) so feature logic does not sprawl across the
+ * visitor.
  *
  * @since 6.0.0
  */
@@ -55,6 +58,7 @@ public final class AsyncTransformHelper {
     private static final String DEFER_SCOPE_VAR = "$__deferScope__";
 
     private static final String ASYNC_GEN_PARAM_NAME = "$__asyncGen__";
+    private static final String AWAIT_ANY_METHOD = "awaitAny";
     private static final String AWAIT_METHOD = "await";
     private static final String YIELD_RETURN_METHOD = "yieldReturn";
     private static final String ASYNC_METHOD = "async";
@@ -65,6 +69,9 @@ public final class AsyncTransformHelper {
     private static final String DEFER_METHOD = "defer";
     private static final String EXECUTE_DEFER_SCOPE_METHOD = 
"executeDeferScope";
 
+    /** Monotonic suffix for synthetic {@code for await} source variables. */
+    private static final AtomicLong FOR_AWAIT_SEQ = new AtomicLong();
+
     private AsyncTransformHelper() { }
 
     private static Expression ensureArgs(Expression expr) {
@@ -72,19 +79,18 @@ public final class AsyncTransformHelper {
     }
 
     /**
-     * Builds {@code AsyncSupport.await(arg)} or for multi-arg:
+     * Builds {@code AsyncSupport.awaitAny(arg)} or for multi-arg:
      * {@code AsyncSupport.await(Awaitable.all(arg1, arg2, ...))}.
+     * <p>
+     * Single-arg form uses {@link AsyncSupport#awaitAny(Object)} so generated
+     * bytecode never needs a cast-to-{@code Object} overload trick.
      */
     public static Expression buildAwaitCall(Expression arg) {
         if (arg instanceof ArgumentListExpression args && 
args.getExpressions().size() > 1) {
-            Expression allCall = callX(classX(AWAITABLE_TYPE), "all", args);
+            Expression allCall = callX(AWAITABLE_TYPE, "all", args);
             return callX(ASYNC_SUPPORT_TYPE, AWAIT_METHOD, new 
ArgumentListExpression(allCall));
         }
-        // Cast to Object to force dynamic dispatch to the await(Object) 
overload,
-        // avoiding ambiguity when the argument implements multiple async 
interfaces
-        // (e.g., CompletableFuture implements both CompletionStage and Future)
-        return callX(ASYNC_SUPPORT_TYPE, AWAIT_METHOD,
-                new ArgumentListExpression(castX(ClassHelper.OBJECT_TYPE, 
arg)));
+        return callX(ASYNC_SUPPORT_TYPE, AWAIT_ANY_METHOD, ensureArgs(arg));
     }
 
     /**
@@ -143,34 +149,12 @@ public final class AsyncTransformHelper {
     public static boolean containsYieldReturn(Statement stmt) {
         return AstQuery.from(stmt)
                 .descendants(StaticMethodCallExpression.class)
-                .notInto(ClosureExpression.class) // don't descend into nested 
closures
+                .notInto(ClosureExpression.class)
                 .where(call -> YIELD_RETURN_METHOD.equals(call.getMethod())
                         && 
AsyncSupport.class.getName().equals(call.getOwnerType().getName()))
                 .any();
     }
 
-    /**
-     * Rewrites {@code yieldReturn(expr)} calls to {@code 
yieldReturn($__asyncGen__, expr)}
-     * by injecting the generator parameter reference.
-     */
-    public static void injectGenParamIntoYieldReturnCalls(Statement stmt, 
Parameter genParam) {
-        stmt.visit(new CodeVisitorSupport() {
-            @Override
-            public void 
visitStaticMethodCallExpression(StaticMethodCallExpression call) {
-                if (YIELD_RETURN_METHOD.equals(call.getMethod())
-                        && 
AsyncSupport.class.getName().equals(call.getOwnerType().getName())) {
-                    // Already built with gen param by buildYieldReturnCall — 
no action needed
-                    // This method is a hook point for future transformations
-                }
-                super.visitStaticMethodCallExpression(call);
-            }
-            @Override
-            public void visitClosureExpression(ClosureExpression expression) {
-                // Don't descend into nested closures
-            }
-        });
-    }
-
     /**
      * Returns {@code true} if the statement tree contains a {@code defer} 
call,
      * without descending into nested closures.
@@ -178,7 +162,7 @@ public final class AsyncTransformHelper {
     public static boolean containsDefer(Statement stmt) {
         return AstQuery.from(stmt)
                 .descendants(StaticMethodCallExpression.class)
-                .notInto(ClosureExpression.class) // don't descend into nested 
closures
+                .notInto(ClosureExpression.class)
                 .where(call -> DEFER_METHOD.equals(call.getMethod())
                         && 
AsyncSupport.class.getName().equals(call.getOwnerType().getName()))
                 .any();
@@ -193,14 +177,80 @@ public final class AsyncTransformHelper {
      * </pre>
      */
     public static Statement wrapWithDeferScope(Statement body) {
-        // var $__deferScope__ = AsyncSupport.createDeferScope()
         Statement declStmt = declS(varX(DEFER_SCOPE_VAR),
                 callX(ASYNC_SUPPORT_TYPE, CREATE_DEFER_SCOPE_METHOD));
 
-        // try { body } finally { 
AsyncSupport.executeDeferScope($__deferScope__) }
         Statement finallyStmt = stmt(callX(ASYNC_SUPPORT_TYPE, 
EXECUTE_DEFER_SCOPE_METHOD,
                 args(varX(DEFER_SCOPE_VAR))));
 
         return block(declStmt, tryCatchS(body, finallyStmt));
     }
+
+    /**
+     * Rewrites a {@code for await} loop into a block that materialises the
+     * source iterable, runs the loop, and closes the source in a finally 
block:
+     * <pre>
+     * var $__forAwaitSource_N = AsyncSupport.toIterable(collection)
+     * try {
+     *     for (... in $__forAwaitSource_N) { body }
+     * } finally {
+     *     AsyncSupport.closeIterable($__forAwaitSource_N)
+     * }
+     * </pre>
+     *
+     * @param forStatement the for statement whose collection is the await 
source
+     * @return the rewritten statement block
+     */
+    public static Statement wrapForAwaitLoop(ForStatement forStatement) {
+        Expression original = forStatement.getCollectionExpression();
+        String tempVar = "$__forAwaitSource_" + 
FOR_AWAIT_SEQ.incrementAndGet();
+        Expression toIterableCall = buildToIterableCall(original);
+
+        Statement declStmt = declS(varX(tempVar), toIterableCall);
+        forStatement.setCollectionExpression(varX(tempVar));
+
+        Statement finallyStmt = stmt(buildCloseIterableCall(varX(tempVar)));
+        TryCatchStatement tryCatch = new TryCatchStatement(forStatement, 
finallyStmt);
+        return block(declStmt, tryCatch);
+    }
+
+    /**
+     * Applies defer-scope wrapping and generator parameter injection to an
+     * {@code async { ... }} closure, then builds the runtime call
+     * ({@code async} or {@code asyncGenerator}).
+     *
+     * @param closure the parsed async closure expression
+     * @return the desugared runtime call expression
+     */
+    public static Expression transformAsyncClosure(ClosureExpression closure) {
+        boolean hasYieldReturn = containsYieldReturn(closure.getCode());
+        boolean hasDefer = containsDefer(closure.getCode());
+
+        if (hasDefer) {
+            Statement wrappedBody = wrapWithDeferScope(closure.getCode());
+            ClosureExpression newClosure = new 
ClosureExpression(closure.getParameters(), wrappedBody);
+            newClosure.setVariableScope(closure.getVariableScope());
+            newClosure.setSourcePosition(closure);
+            closure = newClosure;
+        }
+
+        if (hasYieldReturn) {
+            Parameter genParam = createGenParam();
+            Parameter[] existingParams = closure.getParameters();
+            boolean hasUserParams = existingParams != null && 
existingParams.length > 0;
+            Parameter[] newParams;
+            if (hasUserParams) {
+                newParams = new Parameter[existingParams.length + 1];
+                newParams[0] = genParam;
+                System.arraycopy(existingParams, 0, newParams, 1, 
existingParams.length);
+            } else {
+                newParams = new Parameter[]{genParam};
+            }
+            ClosureExpression genClosure = new ClosureExpression(newParams, 
closure.getCode());
+            genClosure.setVariableScope(closure.getVariableScope());
+            genClosure.setSourcePosition(closure);
+            return buildAsyncGeneratorCall(new 
ArgumentListExpression(genClosure));
+        }
+        return buildAsyncCall(new ArgumentListExpression(closure));
+    }
 }
diff --git a/src/spec/doc/core-async-await.adoc 
b/src/spec/doc/core-async-await.adoc
index 858a52bcda..dfff725c31 100644
--- a/src/spec/doc/core-async-await.adoc
+++ b/src/spec/doc/core-async-await.adoc
@@ -359,7 +359,9 @@ import org.apache.groovy.runtime.async.AsyncSupport
 import java.util.concurrent.Executors
 
 AsyncSupport.setExecutor(Executors.newFixedThreadPool(4))
-AsyncSupport.resetExecutor()  // restore default
+AsyncSupport.setExecutor(null)  // restore platform default
+// or equivalently:
+AsyncSupport.resetExecutor()
 ----
 
 [[async-jdk-integration]]
diff --git a/src/test/groovy/groovy/AsyncAwaitTest.groovy 
b/src/test/groovy/groovy/AsyncAwaitTest.groovy
index ffc5826a34..148370ee7c 100644
--- a/src/test/groovy/groovy/AsyncAwaitTest.groovy
+++ b/src/test/groovy/groovy/AsyncAwaitTest.groovy
@@ -1116,11 +1116,50 @@ final class AsyncAwaitTest {
                 await Awaitable.first(f1, f2)
                 assert false : 'should have thrown'
             } catch (Exception e) {
+                // await exception transparency peels the aggregate 
CompletionException
                 assert e != null
             }
         '''
     }
 
+    @Test
+    void testSetExecutorNullResetsToDefault() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import org.apache.groovy.runtime.async.AsyncSupport
+            import java.util.concurrent.Executors
+
+            def original = Awaitable.getExecutor()
+            def custom = Executors.newSingleThreadExecutor()
+            try {
+                Awaitable.setExecutor(custom)
+                assert Awaitable.getExecutor().is(custom)
+                Awaitable.setExecutor(null)
+                assert Awaitable.getExecutor() != null
+                assert !Awaitable.getExecutor().is(custom)
+                // AsyncSupport path is equivalent
+                AsyncSupport.setExecutor(custom)
+                AsyncSupport.setExecutor(null)
+                assert AsyncSupport.getExecutor() != null
+            } finally {
+                Awaitable.setExecutor(original)
+                custom.shutdown()
+            }
+        '''
+    }
+
+    @Test
+    void testAwaitAnyMatchesAwaitObject() {
+        assertScript '''
+            import org.apache.groovy.runtime.async.AsyncSupport
+            import groovy.concurrent.Awaitable
+
+            assert AsyncSupport.awaitAny(null) == null
+            assert AsyncSupport.awaitAny(Awaitable.of(42)) == 42
+            assert AsyncSupport.awaitAny(async { 'x' }) == 'x'
+        '''
+    }
+
     @Test
     void testAllSettledWithCancelledTask() {
         assertScript '''
diff --git a/src/test/groovy/groovy/concurrent/AwaitResultTest.groovy 
b/src/test/groovy/groovy/concurrent/AwaitResultTest.groovy
new file mode 100644
index 0000000000..72da8c98e6
--- /dev/null
+++ b/src/test/groovy/groovy/concurrent/AwaitResultTest.groovy
@@ -0,0 +1,118 @@
+/*
+ *  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.concurrent
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Unit tests for {@link AwaitResult} value-object behaviour.
+ */
+final class AwaitResultTest {
+
+    @Test
+    void testSuccessHoldsValueIncludingNull() {
+        def ok = AwaitResult.success('hello')
+        assert ok.success
+        assert !ok.failure
+        assert ok.value == 'hello'
+        assert ok.toString() == 'AwaitResult.Success[hello]'
+
+        def nil = AwaitResult.success(null)
+        assert nil.success
+        assert nil.value == null
+    }
+
+    @Test
+    void testFailureHoldsError() {
+        def err = new RuntimeException('boom')
+        def fail = AwaitResult.failure(err)
+        assert fail.failure
+        assert !fail.success
+        assert fail.error.is(err)
+        assert fail.toString().contains('boom')
+    }
+
+    @Test
+    void testFailureRejectsNullError() {
+        shouldFail(NullPointerException) {
+            AwaitResult.failure(null)
+        }
+    }
+
+    @Test
+    void testGetValueOnFailureThrows() {
+        def fail = AwaitResult.failure(new Exception('x'))
+        def e = shouldFail(IllegalStateException) {
+            fail.value
+        }
+        assert e.message.contains('failed')
+    }
+
+    @Test
+    void testGetErrorOnSuccessThrows() {
+        def ok = AwaitResult.success(1)
+        def e = shouldFail(IllegalStateException) {
+            ok.error
+        }
+        assert e.message.contains('successful')
+    }
+
+    @Test
+    void testGetOrElse() {
+        assert AwaitResult.success(7).getOrElse { -1 } == 7
+        assert AwaitResult.failure(new Exception('e')).getOrElse { 
it.message.length() } == 1
+    }
+
+    @Test
+    void testMapOnSuccessAndFailure() {
+        assert AwaitResult.success('ab').map { it.length() }.value == 2
+        def fail = AwaitResult.<String>failure(new Exception('keep'))
+        def mapped = fail.map { it.toUpperCase() }
+        assert mapped.failure
+        assert mapped.error.message == 'keep'
+    }
+
+    @Test
+    void testMapRejectsNullFunction() {
+        shouldFail(NullPointerException) {
+            AwaitResult.success(1).map(null)
+        }
+    }
+
+    @Test
+    void testEqualsAndHashCode() {
+        def a = AwaitResult.success('x')
+        def b = AwaitResult.success('x')
+        def c = AwaitResult.success('y')
+        def err = new RuntimeException('e')
+        def f1 = AwaitResult.failure(err)
+        def f2 = AwaitResult.failure(err)
+
+        assert a == b
+        assert a.hashCode() == b.hashCode()
+        assert a != c
+        assert a != f1
+        assert f1 == f2
+        assert f1.hashCode() == f2.hashCode()
+        assert a != null
+        assert a != 'x'
+    }
+}
diff --git 
a/src/test/groovy/org/apache/groovy/runtime/async/AsyncSupportInternalsTest.groovy
 
b/src/test/groovy/org/apache/groovy/runtime/async/AsyncSupportInternalsTest.groovy
new file mode 100644
index 0000000000..b0f4d508e0
--- /dev/null
+++ 
b/src/test/groovy/org/apache/groovy/runtime/async/AsyncSupportInternalsTest.groovy
@@ -0,0 +1,259 @@
+/*
+ *  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.Awaitable
+import org.junit.jupiter.api.Test
+
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionException
+import java.util.concurrent.CompletionStage
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Focused unit coverage for the async runtime refactor
+ * ({@link AsyncSupport}, {@link AsyncExecutors}, {@link AwaitCombinators}).
+ */
+final class AsyncSupportInternalsTest {
+
+    @Test
+    void testGoIsAliasOfAsync() {
+        def viaGo = AsyncSupport.go { 11 }
+        def viaAsync = AsyncSupport.async { 22 }
+        assert AsyncSupport.await(viaGo) == 11
+        assert AsyncSupport.await(viaAsync) == 22
+    }
+
+    @Test
+    void testAwaitAnyOnCompletableFutureImplementsMultipleInterfaces() {
+        // CompletableFuture is CompletionStage and Future; awaitAny must not 
be ambiguous
+        CompletableFuture<String> cf = CompletableFuture.completedFuture('cf')
+        assert AsyncSupport.awaitAny(cf) == 'cf'
+        assert AsyncSupport.awaitAny((CompletionStage<String>) cf) == 'cf'
+        assert AsyncSupport.awaitAny((Object) cf) == 'cf'
+    }
+
+    @Test
+    void testAwaitUnwrapsNestedWrappers() {
+        def cause = new IllegalStateException('root')
+        def cf = new CompletableFuture()
+        cf.completeExceptionally(new CompletionException(new 
ExecutionException(cause)))
+        def thrown = shouldFail(IllegalStateException) {
+            AsyncSupport.await(cf)
+        }
+        assert thrown.is(cause)
+    }
+
+    @Test
+    void testAllAsyncEmptyAndSingle() {
+        assert AsyncSupport.await(AsyncSupport.allAsync()) == []
+        assert AsyncSupport.await(AsyncSupport.allAsync(Awaitable.of(1))) == 
[1]
+        assert AsyncSupport.all() == []
+        assert AsyncSupport.all(Awaitable.of('a'), Awaitable.of('b')) == ['a', 
'b']
+    }
+
+    @Test
+    void testAllPropagatesOriginalFailureNotWrapper() {
+        def fail = Awaitable.failed(new RuntimeException('all-x'))
+        def ok = Awaitable.of(1)
+        def thrown = shouldFail(RuntimeException) {
+            AsyncSupport.all(ok, fail)
+        }
+        assert thrown.message == 'all-x'
+    }
+
+    @Test
+    void testFirstAllFailThrowsUnwrappedCause() {
+        def f1 = Awaitable.failed(new RuntimeException('e1'))
+        def f2 = Awaitable.failed(new RuntimeException('e2'))
+        // Aggregate is a CompletionException; await transparency peels to the 
cause
+        def thrown = shouldFail(RuntimeException) {
+            AsyncSupport.await(AsyncSupport.firstAsync(f1, f2))
+        }
+        assert thrown.message == 'e1'
+    }
+
+    @Test
+    void testFirstRejectsNullArrayAndNullElementAndEmpty() {
+        shouldFail(IllegalArgumentException) {
+            AsyncSupport.firstAsync((Object[]) null)
+        }
+        shouldFail(IllegalArgumentException) {
+            AsyncSupport.firstAsync()
+        }
+        shouldFail(IllegalArgumentException) {
+            AsyncSupport.firstAsync(Awaitable.of(1), null)
+        }
+    }
+
+    @Test
+    void testAnyAsyncPicksCompletedValue() {
+        def result = AsyncSupport.await(AsyncSupport.anyAsync(
+                Awaitable.of('winner'),
+                asyncSleep(500, 'loser')))
+        assert result == 'winner'
+    }
+
+    @Test
+    void testAllSettledMixed() {
+        def results = AsyncSupport.allSettled(
+                Awaitable.of('ok'),
+                Awaitable.failed(new RuntimeException('bad')))
+        assert results.size() == 2
+        assert results[0].success && results[0].value == 'ok'
+        assert results[1].failure && results[1].error.message == 'bad'
+    }
+
+    @Test
+    void testSetExecutorNullAndReset() {
+        def original = AsyncSupport.getExecutor()
+        def custom = Executors.newSingleThreadExecutor()
+        try {
+            AsyncSupport.setExecutor(custom)
+            assert AsyncSupport.getExecutor().is(custom)
+            AsyncSupport.setExecutor(null)
+            assert AsyncSupport.getExecutor() != null
+            assert !AsyncSupport.getExecutor().is(custom)
+            AsyncSupport.setExecutor(custom)
+            AsyncSupport.resetExecutor()
+            assert !AsyncSupport.getExecutor().is(custom)
+        } finally {
+            AsyncSupport.setExecutor(original)
+            custom.shutdownNow()
+        }
+    }
+
+    @Test
+    void testDelayAndTimeouts() {
+        assert AsyncSupport.await(AsyncSupport.delay(1)) == null
+        assert AsyncSupport.await(AsyncSupport.delay(1, 
TimeUnit.MILLISECONDS)) == null
+
+        def slow = asyncSleep(5_000, 'late')
+        def timedOut = shouldFail(TimeoutException) {
+            AsyncSupport.await(AsyncSupport.orTimeoutMillis(slow, 20))
+        }
+        assert timedOut.message.contains('Timed out')
+
+        def fallback = AsyncSupport.await(
+                AsyncSupport.completeOnTimeoutMillis(asyncSleep(5_000, 
'late'), 'fb', 20))
+        assert fallback == 'fb'
+    }
+
+    @Test
+    void testToIterableAndCloseIterable() {
+        assert AsyncSupport.toIterable(null).iterator().toList() == []
+        assert AsyncSupport.toIterable([1, 2]).iterator().toList() == [1, 2]
+        assert AsyncSupport.toIterable((Object[]) ['a', 
'b']).iterator().toList() == ['a', 'b']
+        def it = [9].iterator()
+        assert AsyncSupport.toIterable(it).iterator().is(it)
+
+        def closed = new boolean[1]
+        def closeable = new Closeable() {
+            @Override void close() { closed[0] = true }
+        }
+        AsyncSupport.closeIterable(closeable)
+        assert closed[0]
+
+        def autoClosed = new boolean[1]
+        def auto = new AutoCloseable() {
+            @Override void close() { autoClosed[0] = true }
+        }
+        AsyncSupport.closeIterable(auto)
+        assert autoClosed[0]
+
+        // non-closeable is a no-op
+        AsyncSupport.closeIterable('plain')
+    }
+
+    @Test
+    void testCloseIterableSwallowsCloseException() {
+        def bad = new Closeable() {
+            @Override void close() { throw new IOException('ignore-me') }
+        }
+        AsyncSupport.closeIterable(bad) // must not throw
+    }
+
+    @Test
+    void testDeferScopeLIFOAndNullGuards() {
+        def log = []
+        def scope = AsyncSupport.createDeferScope()
+        AsyncSupport.defer(scope, { log << 'a'; null })
+        AsyncSupport.defer(scope, { log << 'b'; null })
+        AsyncSupport.executeDeferScope(scope)
+        assert log == ['b', 'a']
+
+        shouldFail(IllegalStateException) {
+            AsyncSupport.defer(null, { null })
+        }
+        shouldFail(IllegalArgumentException) {
+            AsyncSupport.defer(AsyncSupport.createDeferScope(), null)
+        }
+        AsyncSupport.executeDeferScope(null) // no-op
+        AsyncSupport.executeDeferScope(AsyncSupport.createDeferScope()) // 
empty no-op
+    }
+
+    @Test
+    void testYieldReturnRequiresGeneratorBridge() {
+        shouldFail(IllegalStateException) {
+            AsyncSupport.yieldReturn('not-a-bridge', 1)
+        }
+    }
+
+    @Test
+    void testAsyncGeneratorProducesValues() {
+        def values = AsyncSupport.<Integer>asyncGenerator { bridge ->
+            AsyncSupport.yieldReturn(bridge, 1)
+            AsyncSupport.yieldReturn(bridge, 2)
+        }.iterator().toList()
+        assert values == [1, 2]
+    }
+
+    @Test
+    void testWrapForFutureAndUnwrap() {
+        def ce = new CompletionException(new RuntimeException('x'))
+        assert AsyncSupport.wrapForFuture(ce).is(ce)
+        def wrapped = AsyncSupport.wrapForFuture(new RuntimeException('y'))
+        assert wrapped instanceof CompletionException
+        assert AsyncSupport.unwrap(wrapped).message == 'y'
+    }
+
+    @Test
+    void testIsVirtualThreadsAvailableIsConsistent() {
+        boolean available = AsyncSupport.isVirtualThreadsAvailable()
+        assert available == Awaitable.isVirtualThreadsAvailable()
+    }
+
+    @Test
+    void testSchedulerIsDaemon() {
+        def scheduler = AsyncSupport.getScheduler()
+        assert scheduler != null
+    }
+
+    private static Awaitable asyncSleep(long millis, Object value) {
+        AsyncSupport.async {
+            Thread.sleep(millis)
+            value
+        }
+    }
+}
diff --git 
a/src/test/groovy/org/codehaus/groovy/transform/AsyncTransformHelperTest.groovy 
b/src/test/groovy/org/codehaus/groovy/transform/AsyncTransformHelperTest.groovy
new file mode 100644
index 0000000000..237b7a89f3
--- /dev/null
+++ 
b/src/test/groovy/org/codehaus/groovy/transform/AsyncTransformHelperTest.groovy
@@ -0,0 +1,179 @@
+/*
+ *  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.codehaus.groovy.ast.Parameter
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
+import org.codehaus.groovy.ast.expr.ClosureExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
+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.BlockStatement
+import org.codehaus.groovy.ast.stmt.EmptyStatement
+import org.codehaus.groovy.ast.stmt.ExpressionStatement
+import org.codehaus.groovy.ast.stmt.ForStatement
+import org.codehaus.groovy.ast.tools.GeneralUtils
+import org.junit.jupiter.api.Test
+
+/**
+ * Unit tests for {@link AsyncTransformHelper} AST construction helpers.
+ */
+final class AsyncTransformHelperTest {
+
+    @Test
+    void testBuildAwaitCallSingleArgUsesAwaitAny() {
+        def expr = AsyncTransformHelper.buildAwaitCall(new 
ConstantExpression(1))
+        assert methodName(expr) == 'awaitAny'
+        assert ownerName(expr).endsWith('AsyncSupport')
+    }
+
+    @Test
+    void testBuildAwaitCallMultiArgUsesAwaitableAllThenAwait() {
+        def args = new ArgumentListExpression(
+                new ConstantExpression(1),
+                new ConstantExpression(2))
+        def expr = AsyncTransformHelper.buildAwaitCall(args)
+        assert methodName(expr) == 'await'
+        def arg0 = ((StaticMethodCallExpression) expr).arguments.expressions[0]
+        assert methodName(arg0) == 'all'
+        assert ownerName(arg0).endsWith('Awaitable')
+    }
+
+    @Test
+    void testContainsYieldReturnAndDeferDoNotDescendIntoNestedClosures() {
+        def yieldCall = AsyncTransformHelper.buildYieldReturnCall(new 
ConstantExpression(1))
+        def deferCall = AsyncTransformHelper.buildDeferCall(
+                new ClosureExpression(Parameter.EMPTY_ARRAY, 
EmptyStatement.INSTANCE))
+
+        def outerWithYield = new BlockStatement()
+        outerWithYield.addStatement(new ExpressionStatement(yieldCall))
+        assert AsyncTransformHelper.containsYieldReturn(outerWithYield)
+        assert !AsyncTransformHelper.containsDefer(outerWithYield)
+
+        def outerWithDefer = new BlockStatement()
+        outerWithDefer.addStatement(new ExpressionStatement(deferCall))
+        assert AsyncTransformHelper.containsDefer(outerWithDefer)
+        assert !AsyncTransformHelper.containsYieldReturn(outerWithDefer)
+
+        // Nested inside a child closure — must be ignored
+        def nested = new ClosureExpression(Parameter.EMPTY_ARRAY, 
outerWithYield)
+        def outerOnlyNested = new BlockStatement()
+        outerOnlyNested.addStatement(new ExpressionStatement(nested))
+        assert !AsyncTransformHelper.containsYieldReturn(outerOnlyNested)
+    }
+
+    @Test
+    void testWrapWithDeferScopeStructure() {
+        def body = EmptyStatement.INSTANCE
+        def wrapped = AsyncTransformHelper.wrapWithDeferScope(body)
+        assert wrapped instanceof BlockStatement
+        assert wrapped.statements.size() == 2
+    }
+
+    @Test
+    void testWrapForAwaitLoopUsesUniqueSyntheticNames() {
+        def collection = new ConstantExpression([1, 2, 3])
+        def loop1 = new ForStatement(
+                new Parameter(org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE, 
'item'),
+                collection,
+                EmptyStatement.INSTANCE)
+        def loop2 = new ForStatement(
+                new Parameter(org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE, 
'item'),
+                collection,
+                EmptyStatement.INSTANCE)
+
+        def block1 = AsyncTransformHelper.wrapForAwaitLoop(loop1) as 
BlockStatement
+        def block2 = AsyncTransformHelper.wrapForAwaitLoop(loop2) as 
BlockStatement
+
+        def name1 = extractDeclaredName(block1)
+        def name2 = extractDeclaredName(block2)
+        assert name1.startsWith('$__forAwaitSource_')
+        assert name2.startsWith('$__forAwaitSource_')
+        assert name1 != name2
+    }
+
+    @Test
+    void testTransformAsyncClosurePlainAndGenerator() {
+        def plain = new ClosureExpression(Parameter.EMPTY_ARRAY, 
EmptyStatement.INSTANCE)
+        def plainCall = AsyncTransformHelper.transformAsyncClosure(plain)
+        assert methodName(plainCall) == 'async'
+
+        def yieldCall = AsyncTransformHelper.buildYieldReturnCall(new 
ConstantExpression(1))
+        def yieldBody = new BlockStatement()
+        yieldBody.addStatement(new ExpressionStatement(yieldCall))
+        def genClosure = new ClosureExpression(Parameter.EMPTY_ARRAY, 
yieldBody)
+        def genCall = AsyncTransformHelper.transformAsyncClosure(genClosure)
+        assert methodName(genCall) == 'asyncGenerator'
+    }
+
+    @Test
+    void testTransformAsyncClosureWithDefer() {
+        def deferCall = AsyncTransformHelper.buildDeferCall(
+                new ClosureExpression(Parameter.EMPTY_ARRAY, 
EmptyStatement.INSTANCE))
+        def body = new BlockStatement()
+        body.addStatement(new ExpressionStatement(deferCall))
+        def closure = new ClosureExpression(Parameter.EMPTY_ARRAY, body)
+        def call = AsyncTransformHelper.transformAsyncClosure(closure)
+        assert methodName(call) == 'async'
+    }
+
+    @Test
+    void testCreateGenParamAndBuilders() {
+        def p = AsyncTransformHelper.createGenParam()
+        assert p.name == '$__asyncGen__'
+
+        assert methodName(AsyncTransformHelper.buildAsyncCall(
+                new ClosureExpression(Parameter.EMPTY_ARRAY, 
EmptyStatement.INSTANCE))) == 'async'
+        assert methodName(AsyncTransformHelper.buildAsyncGeneratorCall(
+                new ClosureExpression(Parameter.EMPTY_ARRAY, 
EmptyStatement.INSTANCE))) == 'asyncGenerator'
+        assert methodName(AsyncTransformHelper.buildToIterableCall(new 
ConstantExpression([]))) == 'toIterable'
+        assert 
methodName(AsyncTransformHelper.buildCloseIterableCall(GeneralUtils.varX('x'))) 
== 'closeIterable'
+    }
+
+    private static String methodName(Object expr) {
+        if (expr instanceof StaticMethodCallExpression) {
+            return expr.method
+        }
+        if (expr instanceof MethodCallExpression) {
+            return expr.methodAsString
+        }
+        throw new AssertionError("unexpected expression: 
${expr?.getClass()?.name}")
+    }
+
+    private static String ownerName(Object expr) {
+        if (expr instanceof StaticMethodCallExpression) {
+            return expr.ownerType.name
+        }
+        if (expr instanceof MethodCallExpression) {
+            return expr.objectExpression.type.name
+        }
+        throw new AssertionError("unexpected expression: 
${expr?.getClass()?.name}")
+    }
+
+    private static String extractDeclaredName(BlockStatement block) {
+        def declStmt = block.statements[0] as ExpressionStatement
+        def decl = declStmt.expression
+        def left = decl.leftExpression
+        if (left instanceof VariableExpression) {
+            return left.name
+        }
+        return decl.variableExpression.name
+    }
+}

Reply via email to