You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/03/07 09:44:07 UTC

(groovy) branch GROOVY-9381_3 updated (0223300a80 -> 1e70c7f90b)

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

sunlan pushed a change to branch GROOVY-9381_3
in repository https://gitbox.apache.org/repos/asf/groovy.git


 discard 0223300a80 GROOVY-9381: Support async/await like ES7
     new 1e70c7f90b GROOVY-9381: Support async/await like ES7

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (0223300a80)
            \
             N -- N -- N   refs/heads/GROOVY-9381_3 (1e70c7f90b)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../concurrent/AwaitableAdapterRegistry.java       | 18 ++++++++++++++--
 .../groovy/runtime/async/AsyncStreamGenerator.java |  5 +++--
 .../groovy/transform/AsyncCoverageTest.groovy      | 24 +++++++++-------------
 3 files changed, 29 insertions(+), 18 deletions(-)


(groovy) 01/01: GROOVY-9381: Support async/await like ES7

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 1e70c7f90b099af1f9eb968b127df62f661abbec
Author: Daniel Sun <su...@apache.org>
AuthorDate: Thu Mar 5 03:40:15 2026 +0900

    GROOVY-9381: Support async/await like ES7
---
 build.gradle                                       |    2 +
 gradle/verification-metadata.xml                   |   16 +
 src/antlr/GroovyLexer.g4                           |    3 +
 src/antlr/GroovyParser.g4                          |   15 +-
 src/main/java/groovy/concurrent/AsyncStream.java   |   75 +
 src/main/java/groovy/concurrent/AwaitResult.java   |  109 +
 src/main/java/groovy/concurrent/Awaitable.java     |  266 +++
 .../java/groovy/concurrent/AwaitableAdapter.java   |   76 +
 .../concurrent/AwaitableAdapterRegistry.java       |  392 +++
 src/main/java/groovy/transform/Async.java          |  132 ++
 .../apache/groovy/parser/antlr4/AstBuilder.java    |  140 +-
 .../groovy/runtime/async/AsyncStreamGenerator.java |  125 +
 .../apache/groovy/runtime/async/AsyncSupport.java  |  915 +++++++
 .../apache/groovy/runtime/async/GroovyPromise.java |  136 ++
 .../java/org/codehaus/groovy/ast/ModifierNode.java |    2 +
 .../groovy/transform/AsyncASTTransformation.java   |  248 ++
 .../groovy/transform/AsyncTransformHelper.java     |  407 ++++
 src/spec/doc/core-async-await.adoc                 |  818 +++++++
 src/spec/test/AsyncAwaitSpecTest.groovy            | 1097 +++++++++
 .../groovy/transform/AsyncAwaitSyntaxTest.groovy   | 2109 +++++++++++++++++
 .../groovy/transform/AsyncBestPracticesTest.groovy | 1470 ++++++++++++
 .../groovy/transform/AsyncClosureLambdaTest.groovy |  701 ++++++
 .../groovy/transform/AsyncCoverageTest.groovy      | 2488 ++++++++++++++++++++
 .../groovy/transform/AsyncDeferFlowTest.groovy     |  407 ++++
 .../transform/AsyncExceptionHandlingTest.groovy    | 1088 +++++++++
 .../transform/AsyncFrameworkIntegrationTest.groovy |  667 ++++++
 .../groovy/transform/AsyncPatternsTest.groovy      |  987 ++++++++
 .../groovy/transform/AsyncTransformTest.groovy     | 1711 ++++++++++++++
 .../groovy/transform/AsyncVirtualThreadTest.groovy | 1068 +++++++++
 versions.properties                                |    2 +
 30 files changed, 17670 insertions(+), 2 deletions(-)

diff --git a/build.gradle b/build.gradle
index f14b9add9e..4f443fbaf4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -122,6 +122,8 @@ dependencies {
     testImplementation "com.thoughtworks.qdox:qdox:${versions.qdox}"
     testImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
     testImplementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${versions.jackson}"
+    testImplementation "io.reactivex.rxjava3:rxjava:${versions.rxjava3}"
+    testImplementation "io.projectreactor:reactor-core:${versions.reactor}"
 
     testFixturesImplementation projects.groovyXml
     testFixturesImplementation projects.groovyTest
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 0b6c75291a..045ed8f1bb 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -114,6 +114,7 @@
          <ignored-key id="C71FB765CD9DE313" reason="Key couldn't be downloaded from any key server"/>
          <ignored-key id="C7CA19B7B620D787" reason="Key couldn't be downloaded from any key server"/>
          <ignored-key id="CA80D1F0EB6CA4BA" reason="Key couldn't be downloaded from any key server"/>
+         <ignored-key id="D1031D14464180E0" reason="Key couldn't be downloaded from any key server"/>
          <ignored-key id="D2151178A123C97F" reason="Key couldn't be downloaded from any key server"/>
          <ignored-key id="D364ABAA39A47320" reason="Key couldn't be downloaded from any key server"/>
          <ignored-key id="D7742D58455ECC7C" reason="Key couldn't be downloaded from any key server"/>
@@ -861,6 +862,16 @@
             <sha512 value="f220e44fe6b61f8dbb61226f832dfb16a09584384540fd48a4dff5c4de9fee060623f85cbead720dfe776aa25105949e70758a9bb1d9db43f63068d8d22164c9" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="io.projectreactor" name="reactor-core" version="3.7.3">
+         <artifact name="reactor-core-3.7.3.jar">
+            <pgp value="48B086A7D843CFA258E83286928FBF39003C0425"/>
+         </artifact>
+      </component>
+      <component group="io.reactivex.rxjava3" name="rxjava" version="3.1.10">
+         <artifact name="rxjava-3.1.10.jar">
+            <pgp value="E9CC3CD1AE59E851E4DB3FA350FFD7487D34B5B9"/>
+         </artifact>
+      </component>
       <component group="jakarta.activation" name="jakarta.activation-api" version="1.2.1">
          <artifact name="jakarta.activation-api-1.2.1.jar">
             <pgp value="6DD3B8C64EF75253BEB2C53AD908A43FB7EC07AC"/>
@@ -2216,6 +2227,11 @@
             <sha512 value="adcc480f68828ffd68d03846be852988b595c2e1bb69224d273578dd6c2ad2773edfe96625a7c00bc40ae0f2d1cac8412eaa54b88cc8e681b0b4c0ee3b082333" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="org.reactivestreams" name="reactive-streams" version="1.0.4">
+         <artifact name="reactive-streams-1.0.4.jar">
+            <sha512 value="cdab6bd156f39106cd6bbfd47df1f4b0a89dc4aa28c68c31ef12a463193c688897e415f01b8d7f0d487b0e6b5bd2f19044bf8605704b024f26d6aa1f4f9a2471" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
+         </artifact>
+      </component>
       <component group="org.reflections" name="reflections" version="0.10.2">
          <artifact name="reflections-0.10.2.jar">
             <pgp value="3F2A008A91D11A7FAC4A0786F13D3E721D56BD54"/>
diff --git a/src/antlr/GroovyLexer.g4 b/src/antlr/GroovyLexer.g4
index 79ddf78dbd..f1d69c5c95 100644
--- a/src/antlr/GroovyLexer.g4
+++ b/src/antlr/GroovyLexer.g4
@@ -392,6 +392,9 @@ DEF             : 'def';
 IN              : 'in';
 TRAIT           : 'trait';
 THREADSAFE      : 'threadsafe'; // reserved keyword
+ASYNC           : 'async';
+AWAIT           : 'await';
+DEFER           : 'defer';
 
 // §3.9 Keywords
 BuiltInPrimitiveType
diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index 88e3b8b713..daccdcdcc3 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -134,6 +134,7 @@ modifier
           |   VOLATILE
           |   DEF
           |   VAR
+          |   ASYNC
           )
     ;
 
@@ -600,7 +601,7 @@ switchStatement
     ;
 
 loopStatement
-    :   FOR LPAREN forControl RPAREN nls statement                                                            #forStmtAlt
+    :   FOR AWAIT? LPAREN forControl RPAREN nls statement                                                     #forStmtAlt
     |   WHILE expressionInPar nls statement                                                                   #whileStmtAlt
     |   DO nls statement nls WHILE expressionInPar                                                            #doWhileStmtAlt
     ;
@@ -642,6 +643,8 @@ statement
     |   continueStatement                                                                                   #continueStmtAlt
     |   { inSwitchExpressionLevel > 0 }?
         yieldStatement                                                                                      #yieldStmtAlt
+    |   YIELD RETURN nls expression                                                                         #yieldReturnStmtAlt
+    |   DEFER nls statementExpression                                                                       #deferStmtAlt
     |   identifier COLON nls statement                                                                      #labeledStmtAlt
     |   assertStatement                                                                                     #assertStmtAlt
     |   localVariableDeclaration                                                                            #localVariableDeclarationStmtAlt
@@ -778,6 +781,10 @@ expression
     // must come before postfixExpression to resolve the ambiguities between casting and call on parentheses expression, e.g. (int)(1 / 2)
     :   castParExpression castOperandExpression                                             #castExprAlt
 
+    // async closure/lambda must come before postfixExpression to resolve the ambiguities between async and method call, e.g. async { ... }
+    |   ASYNC nls closureOrLambdaExpression                                                 #asyncClosureExprAlt
+    |   AWAIT nls (LPAREN expression RPAREN | expression)                                   #awaitExprAlt
+
     // qualified names, array expressions, method invocation, post inc/dec
     |   postfixExpression                                                                   #postfixExprAlt
 
@@ -1228,6 +1235,9 @@ identifier
     :   Identifier
     |   CapitalizedIdentifier
     |   AS
+    |   ASYNC
+    |   AWAIT
+    |   DEFER
     |   IN
     |   PERMITS
     |   RECORD
@@ -1246,12 +1256,15 @@ keywords
     :   ABSTRACT
     |   AS
     |   ASSERT
+    |   ASYNC
+    |   AWAIT
     |   BREAK
     |   CASE
     |   CATCH
     |   CLASS
     |   CONST
     |   CONTINUE
+    |   DEFER
     |   DEF
     |   DEFAULT
     |   DO
diff --git a/src/main/java/groovy/concurrent/AsyncStream.java b/src/main/java/groovy/concurrent/AsyncStream.java
new file mode 100644
index 0000000000..d6acfc5beb
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncStream.java
@@ -0,0 +1,75 @@
+/*
+ *  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;
+
+/**
+ * Asynchronous iteration abstraction, analogous to C#'s
+ * {@code IAsyncEnumerable<T>} or JavaScript's async iterables.
+ * <p>
+ * Used with the {@code for await} syntax:
+ * <pre>
+ * for await (item in asyncStream) {
+ *     process(item)
+ * }
+ * </pre>
+ * <p>
+ * An {@code AsyncStream} can be produced in several ways:
+ * <ul>
+ *   <li>Using {@code yield return} inside an {@code async} method or closure
+ *       to create a generator-style stream</li>
+ *   <li>Adapting JDK {@link java.util.concurrent.Flow.Publisher} instances
+ *       (supported out of the box by the built-in adapter)</li>
+ *   <li>Adapting third-party reactive types (Reactor {@code Flux}, RxJava
+ *       {@code Observable}) via {@link AwaitableAdapter}</li>
+ * </ul>
+ *
+ * @param <T> the element type
+ * @see AwaitableAdapter
+ * @see AwaitableAdapterRegistry
+ * @since 6.0.0
+ */
+public interface AsyncStream<T> {
+
+    /**
+     * Asynchronously advances to the next element. Returns an {@link Awaitable}
+     * that completes with {@code true} if an element is available, or
+     * {@code false} if the stream is exhausted.
+     */
+    Awaitable<Boolean> moveNext();
+
+    /**
+     * Returns the current element. Must only be called after {@link #moveNext()}
+     * has completed with {@code true}.
+     */
+    T getCurrent();
+
+    /**
+     * Returns an empty {@code AsyncStream} that completes immediately.
+     */
+    @SuppressWarnings("unchecked")
+    static <T> AsyncStream<T> empty() {
+        return (AsyncStream<T>) EMPTY;
+    }
+
+    /** Singleton empty stream instance. */
+    AsyncStream<Object> EMPTY = new AsyncStream<>() {
+        @Override public Awaitable<Boolean> moveNext() { return Awaitable.of(false); }
+        @Override public Object getCurrent() { return null; }
+    };
+}
diff --git a/src/main/java/groovy/concurrent/AwaitResult.java b/src/main/java/groovy/concurrent/AwaitResult.java
new file mode 100644
index 0000000000..9fba81d1e9
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitResult.java
@@ -0,0 +1,109 @@
+/*
+ *  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 java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Represents the outcome of an asynchronous computation that may have
+ * succeeded or failed. This is used by {@code awaitAllSettled()} —
+ * the Groovy equivalent of JavaScript's {@code Promise.allSettled()}.
+ * <p>
+ * An {@code AwaitResult} is either a {@linkplain #isSuccess() success}
+ * carrying a value, or a {@linkplain #isFailure() failure} carrying a
+ * {@link Throwable}.
+ *
+ * @param <T> the value type
+ * @since 6.0.0
+ */
+public final class AwaitResult<T> {
+
+    private final T value;
+    private final Throwable error;
+    private final boolean success;
+
+    private AwaitResult(T value, Throwable error, boolean success) {
+        this.value = value;
+        this.error = error;
+        this.success = success;
+    }
+
+    /**
+     * Creates a successful result with the given value.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> AwaitResult<T> success(Object value) {
+        return new AwaitResult<>((T) value, null, true);
+    }
+
+    /**
+     * Creates a failure result with the given exception.
+     */
+    public static <T> AwaitResult<T> failure(Throwable error) {
+        return new AwaitResult<>(null, Objects.requireNonNull(error), false);
+    }
+
+    /** Returns {@code true} if this result represents a successful completion. */
+    public boolean isSuccess() {
+        return success;
+    }
+
+    /** Returns {@code true} if this result represents a failed completion. */
+    public boolean isFailure() {
+        return !success;
+    }
+
+    /**
+     * Returns the value if successful.
+     *
+     * @return the computation result
+     * @throws IllegalStateException if this result represents a failure
+     */
+    public T getValue() {
+        if (!success) throw new IllegalStateException("Cannot get value from a failed result");
+        return value;
+    }
+
+    /**
+     * Returns the exception if failed.
+     *
+     * @return the exception that caused the failure
+     * @throws IllegalStateException if this result represents a success
+     */
+    public Throwable getError() {
+        if (success) throw new IllegalStateException("Cannot get error from a successful result");
+        return error;
+    }
+
+    /**
+     * Returns the value if successful, or applies the given function to
+     * the error to produce a fallback value.
+     */
+    public T getOrElse(Function<Throwable, ? extends T> fallback) {
+        return success ? value : fallback.apply(error);
+    }
+
+    @Override
+    public String toString() {
+        return success
+            ? "AwaitResult.Success[" + value + "]"
+            : "AwaitResult.Failure[" + error + "]";
+    }
+}
diff --git a/src/main/java/groovy/concurrent/Awaitable.java b/src/main/java/groovy/concurrent/Awaitable.java
new file mode 100644
index 0000000000..7cb541f7b2
--- /dev/null
+++ b/src/main/java/groovy/concurrent/Awaitable.java
@@ -0,0 +1,266 @@
+/*
+ *  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.apache.groovy.runtime.async.AsyncSupport;
+import org.apache.groovy.runtime.async.GroovyPromise;
+
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+
+/**
+ * Core abstraction for asynchronous computations in Groovy, analogous to
+ * C#'s {@code Task<T>} and JavaScript's {@code Promise<T>}.
+ * <p>
+ * {@code Awaitable} serves as both an instance type (the result of {@code async}
+ * methods and the input to {@code await}) and a static API surface for
+ * combinators, factories, and configuration — just as C#'s {@code Task} exposes
+ * {@code Task.WhenAll}, {@code Task.WhenAny}, {@code Task.Delay}, and
+ * {@code Task.FromResult} as static members.
+ * <p>
+ * <b>Static combinators</b> (all return {@code Awaitable}, suitable for use
+ * with {@code await}):
+ * <ul>
+ *   <li>{@link #all(Object...) Awaitable.all(a, b, c)} — like
+ *       {@code Task.WhenAll()} / {@code Promise.all()}</li>
+ *   <li>{@link #any(Object...) Awaitable.any(a, b)} — like
+ *       {@code Task.WhenAny()} / {@code Promise.race()}</li>
+ *   <li>{@link #allSettled(Object...) Awaitable.allSettled(a, b)} — like
+ *       {@code Promise.allSettled()}</li>
+ *   <li>{@link #delay(long) Awaitable.delay(ms)} — like
+ *       {@code Task.Delay()} / {@code setTimeout}</li>
+ * </ul>
+ * <p>
+ * <b>Static factories:</b>
+ * <ul>
+ *   <li>{@link #of(Object) Awaitable.of(value)} — like
+ *       {@code Task.FromResult()} / {@code Promise.resolve()}</li>
+ *   <li>{@link #failed(Throwable) Awaitable.failed(error)} — like
+ *       {@code Task.FromException()} / {@code Promise.reject()}</li>
+ * </ul>
+ * <p>
+ * Third-party frameworks (RxJava, Reactor, etc.) can integrate by registering
+ * an {@link AwaitableAdapter} via {@link AwaitableAdapterRegistry}.
+ * <p>
+ * The default implementation, {@link GroovyPromise}, delegates to
+ * {@link CompletableFuture} but users never need to depend on that detail.
+ *
+ * @param <T> the result type
+ * @see GroovyPromise
+ * @see AwaitableAdapter
+ * @since 6.0.0
+ */
+public interface Awaitable<T> {
+
+    /**
+     * Blocks until the computation completes and returns the result.
+     */
+    T get() throws InterruptedException, ExecutionException;
+
+    /**
+     * Blocks until the computation completes or the timeout expires.
+     */
+    T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
+
+    /**
+     * Returns {@code true} if the computation has completed (normally or exceptionally).
+     */
+    boolean isDone();
+
+    /**
+     * Attempts to cancel the computation. If the computation has not yet started
+     * or is still running, it will be cancelled with a {@link CancellationException}.
+     * Analogous to C#'s {@code CancellationToken}.
+     *
+     * @return {@code true} if the computation was successfully cancelled
+     */
+    boolean cancel();
+
+    /**
+     * Returns {@code true} if the computation was cancelled before completing normally.
+     */
+    boolean isCancelled();
+
+    /**
+     * Returns {@code true} if this computation completed exceptionally
+     * (including cancellation).
+     */
+    boolean isCompletedExceptionally();
+
+    /**
+     * Returns a new {@code Awaitable} whose result is obtained by applying the
+     * given function to this awaitable's result when it completes.
+     */
+    <U> Awaitable<U> then(Function<? super T, ? extends U> fn);
+
+    /**
+     * Returns a new {@code Awaitable} produced by applying the given function
+     * to this awaitable's result, flattening the nested {@code Awaitable}.
+     */
+    <U> Awaitable<U> thenCompose(Function<? super T, ? extends Awaitable<U>> fn);
+
+    /**
+     * Returns a new {@code Awaitable} that, if this one completes exceptionally,
+     * applies the given function to the exception to produce a recovery value.
+     */
+    Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn);
+
+    /**
+     * Converts this {@code Awaitable} to a JDK {@link CompletableFuture}
+     * for interoperability with APIs that require it.
+     */
+    CompletableFuture<T> toCompletableFuture();
+
+    // ---- Static factories ----
+
+    /**
+     * Returns an already-completed {@code Awaitable} with the given value.
+     */
+    static <T> Awaitable<T> of(T value) {
+        return new GroovyPromise<>(CompletableFuture.completedFuture(value));
+    }
+
+    /**
+     * Returns an already-failed {@code Awaitable} with the given exception.
+     * Analogous to C#'s {@code Task.FromException()} or JavaScript's
+     * {@code Promise.reject()}.
+     */
+    static <T> Awaitable<T> failed(Throwable error) {
+        return new GroovyPromise<>(CompletableFuture.failedFuture(error));
+    }
+
+    // ---- Combinators (like C#'s Task.WhenAll/WhenAny, JS's Promise.all/any) ----
+
+    /**
+     * Returns an {@code Awaitable} that completes when all given sources
+     * complete, with a list of their results in order.
+     * <p>
+     * Analogous to C#'s {@code Task.WhenAll()} or JavaScript's
+     * {@code Promise.all()}.  Unlike blocking APIs, this returns immediately
+     * and the caller should {@code await} the result:
+     * <pre>
+     * def results = await Awaitable.all(task1, task2, task3)
+     * </pre>
+     *
+     * @param sources the awaitables, futures, or adapted objects to wait for
+     * @return an awaitable that resolves to a list of results
+     */
+    static Awaitable<List<Object>> all(Object... sources) {
+        return AsyncSupport.allAsync(sources);
+    }
+
+    /**
+     * Returns an {@code Awaitable} that completes when the first of the given
+     * sources completes, with the winner's result.
+     * <p>
+     * Analogous to C#'s {@code Task.WhenAny()} or JavaScript's
+     * {@code Promise.any()} / {@code Promise.race()}.
+     * <pre>
+     * def winner = await Awaitable.any(task1, task2)
+     * </pre>
+     *
+     * @param sources the awaitables to race
+     * @return an awaitable that resolves to the first completed result
+     */
+    @SuppressWarnings("unchecked")
+    static <T> Awaitable<T> any(Object... sources) {
+        return AsyncSupport.anyAsync(sources);
+    }
+
+    /**
+     * Returns an {@code Awaitable} that completes when all given sources
+     * have settled (succeeded or failed), with a list of {@link AwaitResult}
+     * objects describing each outcome.
+     * <p>
+     * Analogous to JavaScript's {@code Promise.allSettled()}.  Never throws
+     * for individual failures; they are captured in the result list.
+     * <pre>
+     * def results = await Awaitable.allSettled(task1, task2)
+     * results.each { println it.success ? it.value : it.error }
+     * </pre>
+     *
+     * @param sources the awaitables to settle
+     * @return an awaitable that resolves to a list of settled results
+     */
+    static Awaitable<List<AwaitResult<Object>>> allSettled(Object... sources) {
+        return AsyncSupport.allSettledAsync(sources);
+    }
+
+    // ---- Delay (like C#'s Task.Delay, JS's setTimeout) ----
+
+    /**
+     * Returns an {@code Awaitable} that completes after the specified delay.
+     * Analogous to C#'s {@code Task.Delay()} or JavaScript's
+     * {@code new Promise(resolve =&gt; setTimeout(resolve, ms))}.
+     * <pre>
+     * await Awaitable.delay(1000)  // pause for 1 second
+     * </pre>
+     *
+     * @param milliseconds the delay in milliseconds (must be &ge; 0)
+     * @return an awaitable that completes after the delay
+     */
+    static Awaitable<Void> delay(long milliseconds) {
+        return AsyncSupport.delay(milliseconds);
+    }
+
+    /**
+     * Returns an {@code Awaitable} that completes after the specified delay.
+     *
+     * @param duration the delay duration (must be &ge; 0)
+     * @param unit     the time unit
+     * @return an awaitable that completes after the delay
+     */
+    static Awaitable<Void> delay(long duration, TimeUnit unit) {
+        return AsyncSupport.delay(duration, unit);
+    }
+
+    // ---- Executor configuration ----
+
+    /**
+     * Returns the current executor used for {@code async} operations.
+     * On JDK 21+, this defaults to a virtual-thread-per-task executor.
+     * On earlier JDKs, a cached daemon thread pool is used.
+     */
+    static Executor getExecutor() {
+        return AsyncSupport.getExecutor();
+    }
+
+    /**
+     * Sets the executor to use for {@code async} operations.
+     * Pass {@code null} to reset to the default executor.
+     *
+     * @param executor the executor to use, or {@code null} for default
+     */
+    static void setExecutor(Executor executor) {
+        AsyncSupport.setExecutor(executor);
+    }
+
+    /**
+     * Returns {@code true} if the running JVM supports virtual threads (JDK 21+).
+     */
+    static boolean isVirtualThreadsAvailable() {
+        return AsyncSupport.isVirtualThreadsAvailable();
+    }
+}
diff --git a/src/main/java/groovy/concurrent/AwaitableAdapter.java b/src/main/java/groovy/concurrent/AwaitableAdapter.java
new file mode 100644
index 0000000000..961ddbf5a7
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitableAdapter.java
@@ -0,0 +1,76 @@
+/*
+ *  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;
+
+/**
+ * Service Provider Interface (SPI) for adapting third-party asynchronous types
+ * to Groovy's {@link Awaitable} and {@link AsyncStream} abstractions.
+ * <p>
+ * Implementations are discovered automatically via {@link java.util.ServiceLoader}.
+ * To register an adapter, create a file
+ * {@code META-INF/services/groovy.concurrent.AwaitableAdapter} containing the
+ * fully-qualified class name of your implementation.
+ * <p>
+ * <b>Example – RxJava adapter:</b>
+ * <pre>
+ * public class RxJavaAwaitableAdapter implements AwaitableAdapter {
+ *     public boolean supportsAwaitable(Class&lt;?&gt; type) {
+ *         return io.reactivex.rxjava3.core.Single.class.isAssignableFrom(type);
+ *     }
+ *     public &lt;T&gt; Awaitable&lt;T&gt; toAwaitable(Object source) {
+ *         Single&lt;T&gt; single = (Single&lt;T&gt;) source;
+ *         return new org.apache.groovy.runtime.async.GroovyPromise&lt;&gt;(single.toCompletionStage().toCompletableFuture());
+ *     }
+ *     // ... supportsAsyncStream / toAsyncStream for Observable, Flowable, etc.
+ * }
+ * </pre>
+ *
+ * @see AwaitableAdapterRegistry
+ * @since 6.0.0
+ */
+public interface AwaitableAdapter {
+
+    /**
+     * Returns {@code true} if this adapter can convert instances of the given
+     * type to {@link Awaitable}.
+     */
+    boolean supportsAwaitable(Class<?> type);
+
+    /**
+     * Converts the given source object to an {@link Awaitable}.
+     * Called only when {@link #supportsAwaitable} returned {@code true}.
+     */
+    <T> Awaitable<T> toAwaitable(Object source);
+
+    /**
+     * Returns {@code true} if this adapter can convert instances of the given
+     * type to {@link AsyncStream}.
+     */
+    default boolean supportsAsyncStream(Class<?> type) {
+        return false;
+    }
+
+    /**
+     * Converts the given source object to an {@link AsyncStream}.
+     * Called only when {@link #supportsAsyncStream} returned {@code true}.
+     */
+    default <T> AsyncStream<T> toAsyncStream(Object source) {
+        throw new UnsupportedOperationException("AsyncStream conversion not supported by " + getClass().getName());
+    }
+}
diff --git a/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java b/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java
new file mode 100644
index 0000000000..542a19f875
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java
@@ -0,0 +1,392 @@
+/*
+ *  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.apache.groovy.runtime.async.GroovyPromise;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Central registry for {@link AwaitableAdapter} instances.
+ * <p>
+ * On class-load, adapters are discovered via {@link ServiceLoader} from
+ * {@code META-INF/services/groovy.concurrent.AwaitableAdapter}. A built-in
+ * adapter is always present as the lowest-priority fallback, handling:
+ * <ul>
+ *   <li>{@link CompletableFuture} and {@link CompletionStage}</li>
+ *   <li>{@link Future} (adapted via a blocking wrapper)</li>
+ *   <li>JDK {@link Flow.Publisher} — single-value
+ *       ({@link #toAwaitable}) and multi-value ({@link #toAsyncStream})
+ *       with backpressure support</li>
+ * </ul>
+ * <p>
+ * Additional adapters can be registered at runtime via {@link #register}.
+ *
+ * @see AwaitableAdapter
+ * @since 6.0.0
+ */
+public class AwaitableAdapterRegistry {
+
+    private static final List<AwaitableAdapter> ADAPTERS = new CopyOnWriteArrayList<>();
+
+    /**
+     * Optional executor supplier for blocking Future adaptation, to avoid
+     * starving the common pool. Defaults to null; when set, the provided
+     * executor is used instead of {@code CompletableFuture.runAsync}'s default.
+     */
+    private static volatile Executor blockingExecutor;
+
+    static {
+        // SPI-discovered adapters
+        for (AwaitableAdapter adapter : ServiceLoader.load(AwaitableAdapter.class)) {
+            ADAPTERS.add(adapter);
+        }
+        // Built-in fallback (lowest priority)
+        ADAPTERS.add(new BuiltInAdapter());
+    }
+
+    private AwaitableAdapterRegistry() { }
+
+    /**
+     * Registers an adapter with higher priority than existing ones.
+     *
+     * @return an {@link AutoCloseable} that, when closed, removes this adapter
+     *         from the registry. Useful for test isolation.
+     */
+    public static AutoCloseable register(AwaitableAdapter adapter) {
+        ADAPTERS.add(0, adapter);
+        return () -> ADAPTERS.remove(adapter);
+    }
+
+    /**
+     * Removes the given adapter from the registry.
+     *
+     * @return {@code true} if the adapter was found and removed
+     */
+    public static boolean unregister(AwaitableAdapter adapter) {
+        return ADAPTERS.remove(adapter);
+    }
+
+    /**
+     * Sets the executor used for blocking {@link Future#get()} adaptation.
+     * When non-null, this executor is used instead of
+     * {@link CompletableFuture#runAsync(Runnable)}'s default executor to avoid
+     * pool starvation when many blocking futures are being adapted
+     * simultaneously.
+     *
+     * @param executor the executor to use, or {@code null} to use the default
+     */
+    public static void setBlockingExecutor(Executor executor) {
+        blockingExecutor = executor;
+    }
+
+    /**
+     * Converts the given source to an {@link Awaitable}.
+     * If the source is already an {@code Awaitable}, it is returned as-is.
+     *
+     * @param source the source object; must not be {@code null}
+     * @throws IllegalArgumentException if {@code source} is {@code null}
+     *         or no adapter supports the source type
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Awaitable<T> toAwaitable(Object source) {
+        if (source == null) {
+            throw new IllegalArgumentException("Cannot convert null to Awaitable");
+        }
+        if (source instanceof Awaitable) return (Awaitable<T>) source;
+        Class<?> type = source.getClass();
+        for (AwaitableAdapter adapter : ADAPTERS) {
+            if (adapter.supportsAwaitable(type)) {
+                return adapter.toAwaitable(source);
+            }
+        }
+        throw new IllegalArgumentException(
+                "No AwaitableAdapter found for type: " + type.getName()
+                        + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader.");
+    }
+
+    /**
+     * Converts the given source to an {@link AsyncStream}.
+     * If the source is already an {@code AsyncStream}, it is returned as-is.
+     *
+     * @param source the source object; must not be {@code null}
+     * @throws IllegalArgumentException if {@code source} is {@code null}
+     *         or no adapter supports the source type
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> AsyncStream<T> toAsyncStream(Object source) {
+        if (source == null) {
+            throw new IllegalArgumentException("Cannot convert null to AsyncStream");
+        }
+        if (source instanceof AsyncStream) return (AsyncStream<T>) source;
+        Class<?> type = source.getClass();
+        for (AwaitableAdapter adapter : ADAPTERS) {
+            if (adapter.supportsAsyncStream(type)) {
+                return adapter.toAsyncStream(source);
+            }
+        }
+        throw new IllegalArgumentException(
+                "No AsyncStream adapter found for type: " + type.getName()
+                        + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader.");
+    }
+
+    /**
+     * Built-in adapter handling JDK {@link CompletableFuture}, {@link CompletionStage},
+     * {@link Future}, {@link Flow.Publisher},
+     * and {@link Iterable}/{@link Iterator} (for async stream bridging).
+     * <p>
+     * {@link CompletionStage} support enables seamless integration with frameworks
+     * that return {@code CompletionStage} (e.g., Spring's async APIs, Reactor's
+     * {@code Mono.toFuture()}, etc.) without any additional adapter registration.
+     * <p>
+     * {@link Flow.Publisher} support enables seamless
+     * consumption of reactive streams via {@code for await} without any adapter
+     * registration.  This covers any reactive library that implements the JDK
+     * standard reactive-streams interface (Reactor, RxJava via adapters, etc.).
+     */
+    private static class BuiltInAdapter implements AwaitableAdapter {
+
+        @Override
+        public boolean supportsAwaitable(Class<?> type) {
+            return CompletionStage.class.isAssignableFrom(type)
+                    || Future.class.isAssignableFrom(type)
+                    || Flow.Publisher.class.isAssignableFrom(type);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> Awaitable<T> toAwaitable(Object source) {
+            if (source instanceof CompletionStage) {
+                return new GroovyPromise<>(((CompletionStage<T>) source).toCompletableFuture());
+            }
+            if (source instanceof Flow.Publisher<?> pub) {
+                return publisherToAwaitable(pub);
+            }
+            if (source instanceof Future) {
+                Future<T> future = (Future<T>) source;
+                CompletableFuture<T> cf = new CompletableFuture<>();
+                if (future.isDone()) {
+                    completeFrom(cf, future);
+                } else {
+                    Executor exec = blockingExecutor;
+                    if (exec != null) {
+                        CompletableFuture.runAsync(() -> completeFrom(cf, future), exec);
+                    } else {
+                        CompletableFuture.runAsync(() -> completeFrom(cf, future));
+                    }
+                }
+                return new GroovyPromise<>(cf);
+            }
+            throw new IllegalArgumentException("Cannot convert to Awaitable: " + source.getClass());
+        }
+
+        @Override
+        public boolean supportsAsyncStream(Class<?> type) {
+            return Iterable.class.isAssignableFrom(type)
+                    || Iterator.class.isAssignableFrom(type)
+                    || Flow.Publisher.class.isAssignableFrom(type);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> AsyncStream<T> toAsyncStream(Object source) {
+            if (source instanceof Flow.Publisher<?> pub) {
+                return publisherToAsyncStream((Flow.Publisher<T>) pub);
+            }
+            final Iterator<T> iterator;
+            if (source instanceof Iterable) {
+                iterator = ((Iterable<T>) source).iterator();
+            } else if (source instanceof Iterator) {
+                iterator = (Iterator<T>) source;
+            } else {
+                throw new IllegalArgumentException("Cannot convert to AsyncStream: " + source.getClass());
+            }
+            return new AsyncStream<T>() {
+                private T current;
+
+                @Override
+                public Awaitable<Boolean> moveNext() {
+                    boolean hasNext = iterator.hasNext();
+                    if (hasNext) current = iterator.next();
+                    return Awaitable.of(hasNext);
+                }
+
+                @Override
+                public T getCurrent() {
+                    return current;
+                }
+            };
+        }
+
+        // Signal wrappers for the publisher-to-async-stream queue, ensuring
+        // that values, errors, and completion are never confused — even when
+        // the element type T extends Throwable.
+        private static final Object COMPLETE_SENTINEL = new Object();
+        private record ValueSignal<T>(T value) { }
+        private record ErrorSignal(Throwable error) { }
+
+        /**
+         * Adapts a {@link Flow.Publisher} to an {@link AsyncStream} using a
+         * bounded blocking queue to bridge the push-based reactive-streams
+         * protocol to the pull-based {@code moveNext}/{@code getCurrent}
+         * pattern.  Backpressure is managed by requesting one item at a time:
+         * each {@code moveNext()} call requests the next item from the upstream
+         * subscription only after the previous item has been consumed.
+         * <p>
+         * All queue entries are wrapped in typed signal objects
+         * ({@code ValueSignal}, {@code ErrorSignal}, or a completion sentinel)
+         * so that element types extending {@link Throwable} are never
+         * misidentified as error signals.
+         * <p>
+         * Thread interruption during {@code queue.take()} is converted to a
+         * {@link CancellationException} stored in the returned
+         * {@code CompletableFuture}.  The interrupt flag is intentionally
+         * <em>not</em> re-set, because the caller retrieves the result via
+         * {@code .get()} on the same thread — a lingering interrupt flag
+         * would poison {@code CompletableFuture.get()}, causing it to throw
+         * a JDK-internal exception instead of the stored
+         * {@code CancellationException}.  The original
+         * {@code InterruptedException} is preserved as the
+         * {@linkplain Throwable#getCause() cause} for inspection.
+         */
+        @SuppressWarnings("unchecked")
+        private static <T> AsyncStream<T> publisherToAsyncStream(Flow.Publisher<T> publisher) {
+            BlockingQueue<Object> queue = new LinkedBlockingQueue<>(256);
+            AtomicReference<Flow.Subscription> subRef = new AtomicReference<>();
+
+            publisher.subscribe(new Flow.Subscriber<T>() {
+                @Override
+                public void onSubscribe(Flow.Subscription s) {
+                    subRef.set(s);
+                    s.request(1);
+                }
+
+                @Override
+                public void onNext(T item) {
+                    queue.add(new ValueSignal<>(item));
+                }
+
+                @Override
+                public void onError(Throwable t) {
+                    queue.add(new ErrorSignal(t));
+                }
+
+                @Override
+                public void onComplete() {
+                    queue.add(COMPLETE_SENTINEL);
+                }
+            });
+
+            return new AsyncStream<T>() {
+                private volatile T current;
+
+                @Override
+                public Awaitable<Boolean> moveNext() {
+                    CompletableFuture<Boolean> cf = new CompletableFuture<>();
+                    try {
+                        Object signal = queue.take();
+                        if (signal == COMPLETE_SENTINEL) {
+                            cf.complete(false);
+                        } else if (signal instanceof ErrorSignal es) {
+                            cf.completeExceptionally(es.error());
+                        } else if (signal instanceof ValueSignal<?> vs) {
+                            current = (T) vs.value();
+                            cf.complete(true);
+                            Flow.Subscription sub = subRef.get();
+                            if (sub != null) sub.request(1);
+                        }
+                    } catch (InterruptedException e) {
+                        // Do NOT re-set the interrupt flag here.  The interrupt is fully
+                        // handled by converting it to a CancellationException stored in the
+                        // CompletableFuture.  Re-setting the flag would poison the caller's
+                        // subsequent cf.get() call on the same thread, causing
+                        // CompletableFuture.get() to throw a JDK-internal exception instead
+                        // of the CancellationException we stored.  The original
+                        // InterruptedException is preserved as the cause for inspection.
+                        CancellationException ce = new CancellationException("Interrupted while waiting for next item");
+                        ce.initCause(e);
+                        cf.completeExceptionally(ce);
+                    }
+                    return new GroovyPromise<>(cf);
+                }
+
+                @Override
+                public T getCurrent() {
+                    return current;
+                }
+            };
+        }
+
+        /**
+         * Adapts a single-value {@link Flow.Publisher} to
+         * an {@link Awaitable}.  Subscribes and takes the first emitted value.
+         */
+        @SuppressWarnings("unchecked")
+        private static <T> Awaitable<T> publisherToAwaitable(Flow.Publisher<?> publisher) {
+            CompletableFuture<T> cf = new CompletableFuture<>();
+            publisher.subscribe(new Flow.Subscriber<Object>() {
+                private Flow.Subscription subscription;
+
+                @Override
+                public void onSubscribe(Flow.Subscription s) {
+                    this.subscription = s;
+                    s.request(1);
+                }
+
+                @Override
+                @SuppressWarnings("unchecked")
+                public void onNext(Object item) {
+                    cf.complete((T) item);
+                    subscription.cancel();
+                }
+
+                @Override
+                public void onError(Throwable t) {
+                    cf.completeExceptionally(t);
+                }
+
+                @Override
+                public void onComplete() {
+                    if (!cf.isDone()) cf.complete(null);
+                }
+            });
+            return new GroovyPromise<>(cf);
+        }
+
+        private static <T> void completeFrom(CompletableFuture<T> cf, Future<T> future) {
+            try {
+                cf.complete(future.get());
+            } catch (Exception e) {
+                cf.completeExceptionally(e.getCause() != null ? e.getCause() : e);
+            }
+        }
+    }
+}
diff --git a/src/main/java/groovy/transform/Async.java b/src/main/java/groovy/transform/Async.java
new file mode 100644
index 0000000000..acdb8fab86
--- /dev/null
+++ b/src/main/java/groovy/transform/Async.java
@@ -0,0 +1,132 @@
+/*
+ *  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.transform;
+
+import org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Method annotation to make a method execute asynchronously and return an
+ * {@link groovy.concurrent.Awaitable Awaitable}.
+ * <p>
+ * When applied to a method, the {@code @Async} transformation will:
+ * <ul>
+ *   <li>Change the method's return type to {@code Awaitable<T>} (where {@code T} is the original return type)</li>
+ *   <li>Execute the method body asynchronously via {@link org.apache.groovy.runtime.async.AsyncSupport#executeAsync AsyncSupport.executeAsync}</li>
+ *   <li>Transform any {@code await(expr)} calls within the method to use {@link org.apache.groovy.runtime.async.AsyncSupport#await AsyncSupport.await}</li>
+ *   <li>For generator methods containing {@code yield return}, return
+ *       {@link groovy.concurrent.AsyncStream AsyncStream<T>} instead</li>
+ *   <li>If the method contains {@code defer} statements, wrap the body
+ *       in try-finally to execute deferred actions in LIFO order on exit</li>
+ * </ul>
+ * <p>
+ * The {@code await(future)} pattern inside an {@code @Async} method blocks the async thread until the
+ * given future completes and returns the unwrapped result. This provides a sequential programming model
+ * over asynchronous operations, similar to JavaScript's {@code async/await} or C#'s {@code async/await}.
+ * <p>
+ * <em>Example usage:</em>
+ * <pre>
+ * import groovy.transform.Async
+ * import groovy.concurrent.Awaitable
+ *
+ * class DataService {
+ *
+ *     {@code @}Async
+ *     def fetchUser(long id) {
+ *         def profile = await(fetchProfile(id))
+ *         def orders = await(fetchOrders(id))
+ *         return [profile: profile, orders: orders]
+ *     }
+ *
+ *     {@code @}Async
+ *     Awaitable&lt;Map&gt; fetchProfile(long id) {
+ *         return [name: "User$id"]
+ *     }
+ *
+ *     {@code @}Async
+ *     Awaitable&lt;List&gt; fetchOrders(long id) {
+ *         return ["order1", "order2"]
+ *     }
+ * }
+ *
+ * def service = new DataService()
+ * def awaitable = service.fetchUser(1)    // returns Awaitable
+ * def result = awaitable.get()            // blocks until complete
+ * assert result.profile.name == 'User1'
+ * assert result.orders.size() == 2
+ * </pre>
+ * <p>
+ * For ad-hoc async execution without annotating a method, use the {@code async}
+ * closure syntax.  Note that {@code async { ... }} returns a {@code Closure}
+ * that must be explicitly called:
+ * <pre>
+ * def task = async {
+ *     def data = await fetchFromRemote()
+ *     return process(data)
+ * }
+ * def result = task().get()    // call the closure, then get the result
+ * </pre>
+ * <p>
+ * <b>Custom Executor:</b> By default, tasks run on virtual threads (JDK 21+) or
+ * a cached daemon thread pool whose maximum size is controlled by the system
+ * property {@code groovy.async.parallelism} (JDK &lt; 21). You can specify a custom
+ * executor by providing the name of a field in the class:
+ * <pre>
+ * class MyService {
+ *     private final Executor myExecutor = Executors.newFixedThreadPool(10)
+ *
+ *     {@code @}Async(executor = "myExecutor")
+ *     def fetchData() {
+ *         return await(remoteCall())
+ *     }
+ * }
+ * </pre>
+ * <p>
+ * <b>Restrictions:</b>
+ * <ul>
+ *   <li>Cannot be applied to abstract methods</li>
+ *   <li>Cannot be applied to constructors</li>
+ *   <li>Cannot be applied to methods that already return {@code Awaitable}</li>
+ * </ul>
+ *
+ * @see groovy.concurrent.Awaitable
+ * @see groovy.concurrent.AsyncStream
+ * @since 6.0.0
+ */
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.METHOD)
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.AsyncASTTransformation")
+public @interface Async {
+    /**
+     * Optional name of a field in the declaring class that holds an
+     * {@link java.util.concurrent.Executor Executor} to use for async execution.
+     * If empty (the default), uses the default executor (virtual threads on
+     * JDK 21+, or a cached daemon thread pool controlled by the system
+     * property {@code groovy.async.parallelism}).
+     *
+     * @return the executor field name, or empty string for the default executor
+     */
+    String executor() default "";
+}
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 ff07bc7fb1..01e37f88a8 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -131,6 +131,7 @@ import org.codehaus.groovy.runtime.StringGroovyMethods;
 import org.codehaus.groovy.syntax.Numbers;
 import org.codehaus.groovy.syntax.SyntaxException;
 import org.codehaus.groovy.syntax.Types;
+import org.codehaus.groovy.transform.AsyncTransformHelper;
 import org.objectweb.asm.Opcodes;
 
 import java.io.BufferedReader;
@@ -465,7 +466,12 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     }
 
     @Override
-    public ForStatement visitForStmtAlt(final ForStmtAltContext ctx) {
+    public Statement visitForStmtAlt(final ForStmtAltContext ctx) {
+        // 'for await' async iteration
+        if (ctx.AWAIT() != null) {
+            return visitForAwait(ctx);
+        }
+
         Function<Statement, ForStatement> maker = this.visitForControl(ctx.forControl());
 
         Statement loopBody = this.unpackStatement((Statement) this.visit(ctx.statement()));
@@ -473,6 +479,56 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         return configureAST(maker.apply(loopBody), ctx);
     }
 
+    /**
+     * Transforms {@code for await (item in source) { ... }} into a while-loop
+     * over an {@link groovy.concurrent.AsyncStream}: the source expression is
+     * adapted via {@code AsyncSupport.toAsyncStream()}, then repeatedly polled
+     * with {@code moveNext()} / {@code getCurrent()}.
+     * <p>
+     * Variable modifiers (e.g. {@code final}) from the enhanced-for declaration
+     * are applied to the synthesised loop variable, consistent with the
+     * standard {@code for (... in ...)} handling in
+     * {@link #visitEnhancedForControl}.
+     */
+    private Statement visitForAwait(final ForStmtAltContext ctx) {
+        ForControlContext forCtrl = ctx.forControl();
+        EnhancedForControlContext enhCtrl = forCtrl.enhancedForControl();
+        if (enhCtrl == null) {
+            throw createParsingFailedException("for await requires enhanced for syntax: for await (item in source)", ctx);
+        }
+
+        ClassNode varType = this.visitType(enhCtrl.type());
+        String varName = this.visitIdentifier(enhCtrl.identifier());
+        Expression source = (Expression) this.visit(enhCtrl.expression());
+        Statement loopBody = this.unpackStatement((Statement) this.visit(ctx.statement()));
+
+        // Apply variable modifiers (e.g. final) to the loop variable
+        VariableExpression loopVar = varX(varName, varType);
+        ModifierManager modifierManager = new ModifierManager(this,
+                this.visitVariableModifiersOpt(enhCtrl.variableModifiersOpt()));
+        modifierManager.processVariableExpression(loopVar);
+
+        String streamVar = "$__asyncStream__" + (asyncStreamCounter++);
+
+        // def $__asyncStream__N = AsyncSupport.toAsyncStream(source)
+        Expression toStreamCall = AsyncTransformHelper.buildToAsyncStreamCall(source);
+        ExpressionStatement streamDecl = new ExpressionStatement(declX(varX(streamVar), toStreamCall));
+
+        // while (AsyncSupport.await($__asyncStream__N.moveNext()))
+        Expression moveNextCall = callX(varX(streamVar), "moveNext");
+        Expression awaitCall = AsyncTransformHelper.buildAwaitCall(moveNextCall);
+        BooleanExpression condition = new BooleanExpression(awaitCall);
+
+        // def <varName> = $__asyncStream__N.getCurrent()
+        Expression getCurrentCall = callX(varX(streamVar), "getCurrent");
+        ExpressionStatement getItemStmt = new ExpressionStatement(declX(loopVar, getCurrentCall));
+
+        BlockStatement whileBody = block(getItemStmt, loopBody);
+        WhileStatement whileStmt = new WhileStatement(condition, whileBody);
+
+        return configureAST(block(streamDecl, whileStmt), ctx);
+    }
+
     @Override
     public Function<Statement, ForStatement> visitForControl(final ForControlContext ctx) {
         // e.g. `for (var e : x) { }` and `for (e in x) { }`
@@ -865,6 +921,32 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         return configureAST(this.visitYieldStatement(ctx.yieldStatement()), ctx);
     }
 
+    @Override
+    public ExpressionStatement visitYieldReturnStmtAlt(final YieldReturnStmtAltContext ctx) {
+        Expression expr = (Expression) this.visit(ctx.expression());
+        Expression yieldCall = AsyncTransformHelper.buildYieldReturnCall(expr);
+        return configureAST(new ExpressionStatement(yieldCall), ctx);
+    }
+
+    @Override
+    public ExpressionStatement visitDeferStmtAlt(final DeferStmtAltContext ctx) {
+        Expression action;
+        ExpressionStatement stmtExprStmt = (ExpressionStatement) this.visit(ctx.statementExpression());
+        Expression expr = stmtExprStmt.getExpression();
+        if (expr instanceof ClosureExpression) {
+            // Already a closure/lambda — use directly as the defer action
+            action = expr;
+        } else {
+            // Wrap the statement expression in a closure: { -> expr }
+            ClosureExpression wrapper = closureX(Parameter.EMPTY_ARRAY, block(stmtExprStmt));
+            wrapper.setSourcePosition(stmtExprStmt);
+            action = wrapper;
+        }
+        // Emit: AsyncSupport.defer($__deferScope__, action)
+        Expression deferCall = AsyncTransformHelper.buildDeferCall(action);
+        return configureAST(new ExpressionStatement(deferCall), ctx);
+    }
+
     @Override
     public ContinueStatement visitContinueStatement(final ContinueStatementContext ctx) {
         if (switchExpressionRuleContextStack.peek() instanceof SwitchExpressionContext) {
@@ -1699,6 +1781,12 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         } else { // script method declaration
             methodNode = createScriptMethodNode(modifierManager, methodName, returnType, parameters, exceptions, code);
         }
+
+        // Inject @Async annotation for methods declared with the 'async' keyword modifier
+        if (modifierManager.containsAny(ASYNC)) {
+            methodNode.addAnnotation(new AnnotationNode(ClassHelper.make("groovy.transform.Async")));
+        }
+
         anonymousInnerClassList.forEach(e -> e.setEnclosingMethod(methodNode));
 
         methodNode.setGenericsTypes(this.visitTypeParameters(ctx.typeParameters()));
@@ -2877,6 +2965,55 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         throw createParsingFailedException("Unsupported unary expression: " + ctx.getText(), ctx);
     }
 
+    @Override
+    public Expression visitAwaitExprAlt(final AwaitExprAltContext ctx) {
+        Expression expr = (Expression) this.visit(ctx.expression());
+        return configureAST(
+                AsyncTransformHelper.buildAwaitCall(expr),
+                ctx);
+    }
+
+    @Override
+    public Expression visitAsyncClosureExprAlt(final AsyncClosureExprAltContext ctx) {
+        ClosureExpression closure = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression());
+        boolean hasUserParams = closure.getParameters() != null && closure.getParameters().length > 0;
+        boolean hasYieldReturn = AsyncTransformHelper.containsYieldReturn(closure.getCode());
+        boolean hasDefer = AsyncTransformHelper.containsDefer(closure.getCode());
+
+        // If defer is present, wrap the closure body in try-finally with defer scope
+        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 — rebuild closure
+            Parameter genParam = AsyncTransformHelper.createGenParam();
+            Parameter[] existingParams = closure.getParameters();
+            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);
+            // Transform yieldReturn(expr) → yieldReturn($__asyncGen__, expr)
+            AsyncTransformHelper.injectGenParamIntoYieldReturnCalls(genClosure.getCode(), genParam);
+            return configureAST(AsyncTransformHelper.buildWrapAsyncGeneratorCall(
+                    new ArgumentListExpression(genClosure)), ctx);
+        } else {
+            return configureAST(AsyncTransformHelper.buildWrapAsyncCall(
+                    new ArgumentListExpression(closure)), ctx);
+        }
+    }
+
     @Override
     public CastExpression visitCastExprAlt(final CastExprAltContext ctx) {
         Expression expr = (Expression) this.visit(ctx.expression());
@@ -4727,6 +4864,7 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     private final GroovyLangParser parser;
     private final GroovydocManager groovydocManager;
     private final TryWithResourcesASTTransformation tryWithResourcesASTTransformation;
+    private int asyncStreamCounter;
 
     private final List<ClassNode> classNodeList = new ArrayList<>();
     private final Deque<ClassNode> classNodeStack = new ArrayDeque<>();
diff --git a/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java b/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
new file mode 100644
index 0000000000..d4dcd7b72b
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
@@ -0,0 +1,125 @@
+/*
+ *  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.CancellationException;
+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 volatile 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 CancellationException("Interrupted during yield");
+        }
+    }
+
+    /**
+     * Signals that the generator has completed (no more elements).
+     * If interrupted, the completion signal is delivered on a best-effort basis.
+     */
+    public void complete() {
+        try {
+            queue.put(DONE);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            // Best-effort delivery: use non-blocking offer as fallback
+            queue.offer(DONE);
+        }
+    }
+
+    /**
+     * Signals that the generator failed with an exception.
+     * If interrupted, the error signal is delivered on a best-effort basis.
+     */
+    public void error(Throwable t) {
+        try {
+            queue.put(new ErrorItem(t != null ? t : new NullPointerException("null error in generator")));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            // Best-effort delivery: use non-blocking offer as fallback
+            queue.offer(new ErrorItem(t != null ? t : new NullPointerException("null error in generator")));
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Awaitable<Boolean> moveNext() {
+        try {
+            Object next = queue.take();
+            if (next == DONE) {
+                return Awaitable.of(false);
+            }
+            if (next instanceof ErrorItem ei) {
+                Throwable cause = ei.error;
+                if (cause instanceof Error err) throw err;
+                throw AsyncSupport.sneakyThrow(cause);
+            }
+            current = (T) ((Item) next).value;
+            return Awaitable.of(true);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new CancellationException("Interrupted during moveNext");
+        }
+    }
+
+    @Override
+    public T getCurrent() {
+        return current;
+    }
+
+    // Wrapper to handle null values in the queue
+    private record Item(Object value) { }
+    private record ErrorItem(Throwable error) { }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java b/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java
new file mode 100644
index 0000000000..8875fd52d0
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java
@@ -0,0 +1,915 @@
+/*
+ *  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.AwaitResult;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.AwaitableAdapterRegistry;
+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.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+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;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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 prefer the static methods on
+ * {@link groovy.concurrent.Awaitable} (e.g., {@code Awaitable.all()},
+ * {@code Awaitable.delay()}) for combinators and configuration.
+ * <p>
+ * Key responsibilities:
+ * <ul>
+ *   <li><b>Async execution</b> — {@code executeAsync()}, {@code executeAsyncVoid()},
+ *       and {@code wrapAsync()} run closures on the configured executor</li>
+ *   <li><b>Await</b> — all {@code await()} overloads 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</li>
+ *   <li><b>Async generators</b> — {@code generateAsyncStream()} manages the
+ *       producer/consumer bridge for methods using {@code yield return}</li>
+ *   <li><b>Async stream conversion</b> — {@code toAsyncStream()} adapts
+ *       collections, arrays, and adapter-supported types for {@code for await}</li>
+ *   <li><b>Defer</b> — {@code createDeferScope()}, {@code defer()}, and
+ *       {@code executeDeferScope()} implement Go-style deferred cleanup with
+ *       LIFO execution and exception suppression</li>
+ *   <li><b>Delay</b> — {@code delay()} provides non-blocking delays using a
+ *       shared {@link java.util.concurrent.ScheduledExecutorService}</li>
+ * </ul>
+ * <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()}.
+ *       Virtual threads make blocking within {@code await()} essentially free:
+ *       the JVM parks the virtual thread and releases the underlying carrier
+ *       thread, achieving efficiency comparable to C#'s state-machine-based
+ *       approach without requiring compiler-level control-flow rewriting.</li>
+ *   <li>On earlier JDKs the fallback is a cached (elastic) daemon thread pool
+ *       whose maximum size is controlled by the system property
+ *       {@code groovy.async.parallelism} (default: {@code 256}).  Idle threads
+ *       are reaped after 60 seconds.  A cached pool is used instead of a
+ *       fixed pool to prevent thread starvation when async tasks block
+ *       (e.g., in generators or nested {@code await} calls).  A
+ *       {@link java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
+ *       CallerRunsPolicy} is used so that task submission never fails
+ *       with {@link java.util.concurrent.RejectedExecutionException} when
+ *       the pool is saturated.</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.Awaitable
+ * @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;
+
+    /**
+     * Maximum thread pool size when virtual threads are unavailable.
+     * Configurable via the system property {@code groovy.async.parallelism}.
+     * Defaults to {@code 256} to prevent runaway thread creation while still
+     * allowing ample concurrency for blocking async operations.
+     */
+    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 = org.apache.groovy.util.SystemUtil.getIntegerSafe(
+                "groovy.async.parallelism", 256);
+        if (!VIRTUAL_THREADS_AVAILABLE) {
+            // Use a cached thread pool (threads created on demand, idle threads
+            // reaped after 60 s) to avoid thread starvation.  Async/await relies
+            // on blocking operations (SynchronousQueue in generators, Future.get
+            // in await) — a fixed-size pool can deadlock when all threads block.
+            // CallerRunsPolicy provides graceful degradation when all threads
+            // are busy: the submitting thread runs the task itself, preventing
+            // RejectedExecutionException at the cost of temporary back-pressure.
+            FALLBACK_EXECUTOR = new ThreadPoolExecutor(
+                    0, FALLBACK_MAX_THREADS,
+                    60L, TimeUnit.SECONDS,
+                    new SynchronousQueue<>(),
+                    r -> {
+                        Thread t = new Thread(r);
+                        t.setDaemon(true);
+                        t.setName("groovy-async-" + t.getId());
+                        return t;
+                    },
+                    new ThreadPoolExecutor.CallerRunsPolicy());
+        } else {
+            FALLBACK_EXECUTOR = null;
+        }
+    }
+
+    private static volatile Executor defaultExecutor =
+            VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : FALLBACK_EXECUTOR;
+
+    private AsyncSupport() { }
+
+    // ---- await overloads ------------------------------------------------
+
+    /**
+     * Awaits the result of an {@link Awaitable}.
+     * <p>
+     * Blocks the calling thread until the computation completes and returns
+     * its result.  If the computation failed, the original exception is
+     * rethrown transparently (even if it is a checked exception).  If the
+     * waiting thread is interrupted, an {@link java.util.concurrent.CancellationException}
+     * is thrown and the interrupt flag is restored.
+     *
+     * @param awaitable the computation to await
+     * @param <T>       the result type
+     * @return the computed result
+     */
+    public static <T> T await(Awaitable<T> awaitable) {
+        try {
+            return awaitable.get();
+        } catch (ExecutionException e) {
+            throw rethrowUnwrapped(e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            CancellationException ce = new CancellationException("Interrupted while awaiting");
+            ce.initCause(e);
+            throw ce;
+        }
+    }
+
+    /**
+     * Awaits the result of a {@link CompletableFuture}.
+     * Uses {@link CompletableFuture#join()} for non-interruptible waiting.
+     *
+     * @param future the future to await
+     * @param <T>    the result type
+     * @return the computed result
+     */
+    public static <T> T await(CompletableFuture<T> future) {
+        try {
+            return future.join();
+        } catch (CompletionException e) {
+            throw rethrowUnwrapped(e);
+        } catch (CancellationException e) {
+            throw e;
+        }
+    }
+
+    /**
+     * Awaits the result of a {@link CompletionStage} by converting it to a
+     * {@link CompletableFuture} first.
+     *
+     * @param stage the completion stage to await
+     * @param <T>   the result type
+     * @return the computed result
+     */
+    public static <T> T await(CompletionStage<T> stage) {
+        return await(stage.toCompletableFuture());
+    }
+
+    /**
+     * Awaits the result of a {@link Future}.  If the future is a
+     * {@link CompletableFuture}, delegates to the more efficient
+     * {@link #await(CompletableFuture)} overload.
+     *
+     * @param future the future to await
+     * @param <T>    the result type
+     * @return the computed result
+     */
+    public static <T> T await(Future<T> future) {
+        if (future instanceof CompletableFuture<T> cf) {
+            return await(cf);
+        }
+        try {
+            return future.get();
+        } catch (ExecutionException e) {
+            throw rethrowUnwrapped(e);
+        } catch (CancellationException e) {
+            throw e;
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            CancellationException ce = new CancellationException("Interrupted while awaiting future");
+            ce.initCause(e);
+            throw ce;
+        }
+    }
+
+    /**
+     * Awaits an arbitrary object by adapting it to {@link Awaitable} via the
+     * {@link AwaitableAdapterRegistry}.  This is the fallback overload called
+     * by the {@code await} expression when the compile-time type is not one
+     * of the other supported types.  Returns {@code null} for a {@code null}
+     * argument.
+     *
+     * @param source the object to await
+     * @param <T>    the result type
+     * @return the computed result, or {@code null} if {@code source} is {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T await(Object source) {
+        if (source == null) return null;
+        if (source instanceof Awaitable) return await((Awaitable<T>) source);
+        if (source instanceof CompletableFuture) return await((CompletableFuture<T>) source);
+        if (source instanceof CompletionStage) return await((CompletionStage<T>) source);
+        if (source instanceof Future) return await((Future<T>) source);
+        if (source instanceof Closure) return awaitClosure((Closure<?>) source);
+        return await(AwaitableAdapterRegistry.<T>toAwaitable(source));
+    }
+
+    /**
+     * Handles the case where a {@link Closure} is passed to {@code await}.
+     * <p>
+     * An {@code async} closure or lambda must be explicitly called before
+     * awaiting.  For example, {@code await(myClosure())} is correct, while
+     * {@code await(myClosure)} is not.  This method always throws an
+     * {@link IllegalArgumentException} with guidance on the correct usage.
+     *
+     * @param closure the closure that was incorrectly passed to await
+     * @param <T>     the result type (never actually returned)
+     * @return never returns
+     * @throws IllegalArgumentException always
+     */
+    @SuppressWarnings("unchecked")
+    private static <T> T awaitClosure(Closure<?> closure) {
+        throw new IllegalArgumentException(
+                "Cannot await a Closure directly. "
+                + "Call the closure first, then await the result: "
+                + "await(myClosure()) or await(myClosure(args))");
+    }
+
+    // ---- async ----------------------------------------------------------
+
+    /**
+     * Executes the given closure asynchronously on the specified executor,
+     * returning an {@link Awaitable} that completes with the closure's return
+     * value.  Used by {@link groovy.transform.Async @Async} methods.
+     *
+     * @param closure  the closure to execute
+     * @param executor the executor on which to run the closure
+     * @param <T>      the result type
+     * @return an awaitable representing the pending computation
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Awaitable<T> executeAsync(Closure<T> closure, Executor executor) {
+        return GroovyPromise.of(CompletableFuture.supplyAsync(() -> {
+            try {
+                return closure.call();
+            } catch (Throwable t) {
+                throw wrapForFuture(t);
+            }
+        }, executor));
+    }
+
+    /**
+     * Void variant of {@link #executeAsync(Closure, Executor)} for
+     * {@link groovy.transform.Async @Async} methods whose return type is
+     * {@code void}.
+     *
+     * @param closure  the closure to execute
+     * @param executor the executor on which to run the closure
+     * @return an awaitable that completes when the closure finishes
+     */
+    public static Awaitable<Void> executeAsyncVoid(Closure<?> closure, Executor executor) {
+        return GroovyPromise.of(CompletableFuture.runAsync(() -> {
+            try {
+                closure.call();
+            } catch (Throwable t) {
+                throw wrapForFuture(t);
+            }
+        }, executor));
+    }
+
+    /**
+     * Executes the given closure asynchronously using the default executor,
+     * returning an {@link Awaitable}.  This method is used internally by the
+     * {@link groovy.transform.Async @Async} annotation transformation and
+     * the {@code async} method modifier.  For the {@code async { ... }}
+     * closure expression syntax, see {@link #wrapAsync(Closure)}.
+     *
+     * @param closure the closure to execute
+     * @param <T>     the result type
+     * @return an awaitable representing the pending computation
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Awaitable<T> async(Closure<T> closure) {
+        return executeAsync(closure, defaultExecutor);
+    }
+
+    /**
+     * Wraps a closure so that each invocation executes the body asynchronously
+     * and returns an {@link Awaitable}.  This is the runtime entry point for
+     * the {@code async { ... }} expression syntax.  The returned closure must
+     * be explicitly called to start the async computation:
+     * <pre>
+     * def task = async { expensiveWork() }
+     * def result = await(task())  // explicit call required
+     * </pre>
+     *
+     * @param closure the closure to wrap
+     * @param <T>     the result type
+     * @return a new closure whose calls produce awaitables
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Closure<Awaitable<T>> wrapAsync(Closure<T> closure) {
+        return new Closure<Awaitable<T>>(closure.getOwner(), closure.getThisObject()) {
+            @SuppressWarnings("unused")
+            public Awaitable<T> doCall(Object... args) {
+                return GroovyPromise.of(CompletableFuture.supplyAsync(() -> {
+                    try {
+                        return closure.call(args);
+                    } catch (Throwable t) {
+                        throw wrapForFuture(t);
+                    }
+                }, defaultExecutor));
+            }
+        };
+    }
+
+    /**
+     * Wraps a generator closure (one containing {@code yield return} statements)
+     * so that each invocation returns an {@link AsyncStream} producing the
+     * yielded elements.  This is the runtime entry point for all async
+     * generator closures ({@code async { yield return ... }}).  The returned
+     * closure must be explicitly called to start the generation:
+     * <pre>
+     * def gen = async { yield return 1; yield return 2 }
+     * for await (item in gen()) { println item }
+     * </pre>
+     *
+     * @param closure the generator closure to wrap
+     * @param <T>     the element type
+     * @return a new closure whose calls produce async streams
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Closure<AsyncStream<T>> wrapAsyncGenerator(Closure<?> closure) {
+        return new Closure<AsyncStream<T>>(closure.getOwner(), closure.getThisObject()) {
+            @SuppressWarnings("unused")
+            public AsyncStream<T> doCall(Object... args) {
+                AsyncStreamGenerator<T> gen = new AsyncStreamGenerator<>();
+                CompletableFuture.runAsync(() -> {
+                    try {
+                        Object[] allArgs = new Object[args.length + 1];
+                        allArgs[0] = gen;
+                        System.arraycopy(args, 0, allArgs, 1, args.length);
+                        closure.call(allArgs);
+                        gen.complete();
+                    } catch (Throwable t) {
+                        gen.error(t);
+                    }
+                }, defaultExecutor);
+                return gen;
+            }
+        };
+    }
+
+    // ---- awaitAll / awaitAny / awaitAllSettled ---------------------------
+
+    /**
+     * Waits for all given awaitables to complete and returns their results
+     * as a list, preserving order.  Analogous to JavaScript's
+     * {@code Promise.all()}.  If any awaitable fails, the exception is
+     * rethrown immediately.
+     * <p>
+     * Returns an empty list if {@code awaitables} is {@code null} or empty.
+     *
+     * @param awaitables the awaitables (or futures, completion stages, etc.);
+     *                   elements must not be {@code null}
+     * @return a list of results in the same order as the arguments
+     * @throws IllegalArgumentException if any element is {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    public static List<Object> awaitAll(Object... awaitables) {
+        if (awaitables == null || awaitables.length == 0) {
+            return new ArrayList<>();
+        }
+        for (int i = 0; i < awaitables.length; i++) {
+            if (awaitables[i] == null) {
+                throw new IllegalArgumentException("awaitAll: element at index " + i + " is null");
+            }
+        }
+        CompletableFuture<?>[] futures =
+            Arrays.stream(awaitables).map(AsyncSupport::toCompletableFuture).toArray(CompletableFuture[]::new);
+        try {
+            CompletableFuture.allOf(futures).join();
+        } catch (CompletionException e) {
+            throw rethrowUnwrapped(e);
+        }
+        List<Object> results = new ArrayList<>(futures.length);
+        for (CompletableFuture<?> f : futures) {
+            results.add(f.join());
+        }
+        return results;
+    }
+
+    /**
+     * Waits for any one of the given awaitables to complete and returns
+     * its result.  Analogous to JavaScript's {@code Promise.any()} / {@code Promise.race()}.
+     *
+     * @param awaitables the awaitables to race; must not be {@code null},
+     *                   empty, or contain {@code null} elements
+     * @return the result of the first awaitable that completes
+     * @throws IllegalArgumentException if {@code awaitables} is {@code null},
+     *         empty, or contains {@code null} elements
+     */
+    @SuppressWarnings("unchecked")
+    public static Object awaitAny(Object... awaitables) {
+        if (awaitables == null || awaitables.length == 0) {
+            throw new IllegalArgumentException("awaitAny requires at least one awaitable");
+        }
+        for (int i = 0; i < awaitables.length; i++) {
+            if (awaitables[i] == null) {
+                throw new IllegalArgumentException("awaitAny: element at index " + i + " is null");
+            }
+        }
+        CompletableFuture<?>[] futures =
+            Arrays.stream(awaitables).map(AsyncSupport::toCompletableFuture).toArray(CompletableFuture[]::new);
+        try {
+            return CompletableFuture.anyOf(futures).join();
+        } catch (CompletionException e) {
+            throw rethrowUnwrapped(e);
+        }
+    }
+
+    /**
+     * Waits for all given awaitables to settle (complete either successfully
+     * or with an exception) and returns a list of {@link AwaitResult} objects.
+     * Analogous to JavaScript's {@code Promise.allSettled()}.  Never throws
+     * for individual failures; failures are captured in the result list.
+     * <p>
+     * Returns an empty list if {@code awaitables} is {@code null} or empty.
+     *
+     * @param awaitables the awaitables to settle; elements must not be {@code null}
+     * @return a list of results in the same order as the arguments
+     * @throws IllegalArgumentException if any element is {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    public static List<AwaitResult<Object>> awaitAllSettled(Object... awaitables) {
+        if (awaitables == null || awaitables.length == 0) {
+            return new ArrayList<>();
+        }
+        for (int i = 0; i < awaitables.length; i++) {
+            if (awaitables[i] == null) {
+                throw new IllegalArgumentException("awaitAllSettled: element at index " + i + " is null");
+            }
+        }
+        CompletableFuture<?>[] futures =
+            Arrays.stream(awaitables).map(AsyncSupport::toCompletableFuture).toArray(CompletableFuture[]::new);
+        CompletableFuture.allOf(
+            Arrays.stream(futures)
+                .map(f -> f.handle((v, t) -> null))
+                .toArray(CompletableFuture[]::new)
+        ).join();
+
+        return getAwaitResults(futures);
+    }
+
+    /** Converts an arbitrary source to a {@link CompletableFuture} for internal use. */
+    private static CompletableFuture<?> toCompletableFuture(Object source) {
+        if (source instanceof CompletableFuture<?> cf) return cf;
+        if (source instanceof Awaitable<?> a) return a.toCompletableFuture();
+        if (source instanceof CompletionStage<?> cs) return cs.toCompletableFuture();
+        return AwaitableAdapterRegistry.toAwaitable(source).toCompletableFuture();
+    }
+
+    // ---- non-blocking combinators (return Awaitable) --------------------
+
+    /**
+     * Non-blocking variant of {@link #awaitAll(Object...)} that returns an
+     * {@link Awaitable} instead of blocking.  Used by
+     * {@link Awaitable#all(Object...)}.
+     */
+    public static Awaitable<List<Object>> allAsync(Object... sources) {
+        if (sources == null || sources.length == 0) {
+            return Awaitable.of(new ArrayList<>());
+        }
+        CompletableFuture<?>[] futures = new CompletableFuture[sources.length];
+        for (int i = 0; i < sources.length; i++) {
+            if (sources[i] == null) {
+                throw new IllegalArgumentException("Awaitable.all: element at index " + i + " is null");
+            }
+            futures[i] = toCompletableFuture(sources[i]);
+        }
+        CompletableFuture<List<Object>> combined = CompletableFuture.allOf(futures)
+                .thenApply(v -> {
+                    List<Object> results = new ArrayList<>(futures.length);
+                    for (CompletableFuture<?> f : futures) {
+                        results.add(f.join());
+                    }
+                    return results;
+                });
+        return GroovyPromise.of(combined);
+    }
+
+    /**
+     * Non-blocking variant of {@link #awaitAny(Object...)} that returns an
+     * {@link Awaitable} instead of blocking.  Used by
+     * {@link Awaitable#any(Object...)}.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Awaitable<T> anyAsync(Object... sources) {
+        if (sources == null || sources.length == 0) {
+            throw new IllegalArgumentException("Awaitable.any requires at least one source");
+        }
+        for (int i = 0; i < sources.length; i++) {
+            if (sources[i] == null) {
+                throw new IllegalArgumentException("Awaitable.any: element at index " + i + " is null");
+            }
+        }
+        CompletableFuture<?>[] futures = Arrays.stream(sources).map(AsyncSupport::toCompletableFuture).toArray(CompletableFuture[]::new);
+        return GroovyPromise.of((CompletableFuture<T>) CompletableFuture.anyOf(futures));
+    }
+
+    /**
+     * Non-blocking variant of {@link #awaitAllSettled(Object...)} that returns an
+     * {@link Awaitable} instead of blocking.  Used by
+     * {@link Awaitable#allSettled(Object...)}.
+     */
+    public static Awaitable<List<AwaitResult<Object>>> allSettledAsync(Object... sources) {
+        if (sources == null || sources.length == 0) {
+            return Awaitable.of(new ArrayList<>());
+        }
+        for (int i = 0; i < sources.length; i++) {
+            if (sources[i] == null) {
+                throw new IllegalArgumentException("Awaitable.allSettled: element at index " + i + " is null");
+            }
+        }
+        CompletableFuture<?>[] futures = Arrays.stream(sources).map(AsyncSupport::toCompletableFuture).toArray(CompletableFuture[]::new);
+        // Wait for all to settle (handle converts failures to non-exceptional completions)
+        CompletableFuture<List<AwaitResult<Object>>> combined = CompletableFuture.allOf(
+                Arrays.stream(futures)
+                        .map(f -> f.handle((v, t) -> null))
+                        .toArray(CompletableFuture[]::new)
+        ).thenApply(v -> getAwaitResults(futures));
+        return GroovyPromise.of(combined);
+    }
+
+    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(deepUnwrap(e)));
+            } catch (CancellationException e) {
+                results.add(AwaitResult.failure(e));
+            }
+        }
+        return results;
+    }
+
+    // ---- delay ----------------------------------------------------------
+
+    /**
+     * Shared single-thread scheduler for {@link #delay} operations.
+     * Uses a daemon thread so it does not prevent JVM shutdown.
+     */
+    private static final ScheduledExecutorService DELAY_SCHEDULER =
+            Executors.newSingleThreadScheduledExecutor(r -> {
+                Thread t = new Thread(r, "groovy-async-delay");
+                t.setDaemon(true);
+                return t;
+            });
+
+    /**
+     * Returns an {@link Awaitable} that completes (with {@code null}) after
+     * the specified delay.  Analogous to C#'s {@code Task.Delay()} or
+     * JavaScript's {@code new Promise(resolve => setTimeout(resolve, ms))}.
+     * <p>
+     * This is a non-blocking delay: the calling thread is not blocked.
+     * Use {@code await delay(ms)} inside an {@code async} context to pause
+     * the async workflow for the given duration without blocking a thread.
+     *
+     * @param milliseconds the delay in milliseconds (must be &ge; 0)
+     * @return an awaitable that completes after the delay
+     * @throws IllegalArgumentException if {@code milliseconds} is negative
+     */
+    public static Awaitable<Void> delay(long milliseconds) {
+        return delay(milliseconds, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Returns an {@link Awaitable} that completes (with {@code null}) after
+     * the specified delay in the given time unit.
+     *
+     * @param duration the delay duration (must be &ge; 0)
+     * @param unit     the time unit (must not be {@code null})
+     * @return an awaitable that completes after the delay
+     * @throws IllegalArgumentException if {@code duration} is negative or
+     *         {@code unit} is {@code null}
+     */
+    public static Awaitable<Void> delay(long duration, TimeUnit unit) {
+        if (unit == null) {
+            throw new IllegalArgumentException("TimeUnit must not be null");
+        }
+        if (duration < 0) {
+            throw new IllegalArgumentException("delay duration must not be negative: " + duration);
+        }
+        if (duration == 0) {
+            return Awaitable.of(null);
+        }
+        CompletableFuture<Void> cf = new CompletableFuture<>();
+        DELAY_SCHEDULER.schedule(() -> cf.complete(null), duration, unit);
+        return GroovyPromise.of(cf);
+    }
+
+    // ---- for-await support ----------------------------------------------
+
+    /**
+     * Converts an arbitrary source to an {@link AsyncStream} via the adapter
+     * registry.  Used by compiler-generated code for {@code for await} loops.
+     * Returns {@link AsyncStream#empty()} for {@code null} input to ensure
+     * null-safe iteration.
+     *
+     * @param source the source object, or {@code null}
+     * @param <T>    the element type
+     * @return an async stream, never {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> AsyncStream<T> toAsyncStream(Object source) {
+        if (source == null) return AsyncStream.empty();
+        if (source instanceof AsyncStream) return (AsyncStream<T>) source;
+        return AwaitableAdapterRegistry.toAsyncStream(source);
+    }
+
+    // ---- yield return / async generator ----------------------------------
+
+    /**
+     * Safety overload: if {@code yield return} is used outside an async
+     * generator context, this single-argument overload produces a clear
+     * error message at runtime.
+     *
+     * @param value ignored — the method always throws
+     * @throws IllegalStateException always
+     */
+    public static void yieldReturn(Object value) {
+        throw new IllegalStateException(
+                "yield return can only be used inside an async generator method or closure");
+    }
+
+    /**
+     * Called by compiler-generated code for {@code yield return expr} statements
+     * inside async generator methods and closures.
+     *
+     * @param generator the {@link AsyncStreamGenerator} instance (injected as a
+     *                  synthetic parameter by the compiler)
+     * @param value     the value to yield to the consumer
+     * @throws IllegalStateException if {@code generator} is {@code null}
+     */
+    public static void yieldReturn(Object generator, Object value) {
+        if (generator == null) {
+            throw new IllegalStateException("yield return used outside of an async generator context");
+        }
+        ((AsyncStreamGenerator<?>) generator).yield(value);
+    }
+
+    /**
+     * Creates an {@link AsyncStream} from a generator closure.  The closure
+     * body is executed asynchronously on the default executor; each
+     * {@code yield return} statement produces an element in the stream.
+     * This method is used internally by {@link groovy.transform.Async @Async}
+     * generator methods.  For the {@code async { yield return ... }} closure
+     * expression syntax, see {@link #wrapAsyncGenerator(Closure)}.
+     *
+     * @param body the generator closure (receives an {@link AsyncStreamGenerator}
+     *             as its single argument)
+     * @param <T>  the element type
+     * @return a live async stream backed by the running generator
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> AsyncStream<T> generateAsyncStream(Closure<?> body) {
+        AsyncStreamGenerator<T> gen = new AsyncStreamGenerator<>();
+        CompletableFuture.runAsync(() -> {
+            try {
+                body.call(gen);
+                gen.complete();
+            } catch (Throwable t) {
+                gen.error(t);
+            }
+        }, defaultExecutor);
+        return gen;
+    }
+
+    // ---- executor configuration -----------------------------------------
+
+    /**
+     * Returns {@code true} if the running JVM supports virtual threads (JDK 21+).
+     */
+    public static boolean isVirtualThreadsAvailable() {
+        return VIRTUAL_THREADS_AVAILABLE;
+    }
+
+    /**
+     * Returns the current executor used for {@code async} operations.
+     *
+     * @return the active executor, never {@code null}
+     */
+    public static Executor getExecutor() {
+        return defaultExecutor;
+    }
+
+    /**
+     * Sets the executor to use for {@code async} operations.  Pass {@code null}
+     * to reset to the default executor (virtual thread executor on JDK 21+,
+     * or a cached daemon thread pool whose maximum size is controlled by the
+     * system property {@code groovy.async.parallelism}).
+     *
+     * @param executor the custom executor, or {@code null} to reset
+     */
+    public static void setExecutor(Executor executor) {
+        defaultExecutor = executor != null
+                ? executor
+                : (VIRTUAL_THREADS_AVAILABLE ? VIRTUAL_THREAD_EXECUTOR : FALLBACK_EXECUTOR);
+    }
+
+    // ---- defer support -----------------------------------------------------
+
+    /**
+     * Creates a new defer scope — a LIFO list of {@link Closure} actions
+     * to be executed when the enclosing method returns.  Called by
+     * compiler-generated code at the start of methods that contain
+     * {@code defer} statements.
+     *
+     * @return a new, empty defer scope (a mutable list of closures)
+     */
+    public static Deque<Closure<?>> createDeferScope() {
+        return new ArrayDeque<>();
+    }
+
+    /**
+     * Registers a deferred action in the given scope.  The action will be
+     * executed in LIFO order when {@link #executeDeferScope} is called
+     * (typically in a {@code finally} block generated by the compiler).
+     * <p>
+     * This method is the runtime entry point for the {@code defer { ... }}
+     * statement, inspired by Go's {@code defer} keyword.
+     *
+     * @param scope  the defer scope (created by {@link #createDeferScope()});
+     *               must not be {@code null}
+     * @param action the closure to execute on method exit;
+     *               must not be {@code null}
+     * @throws IllegalStateException    if {@code scope} is {@code null}
+     *         (indicates {@code defer} used outside an async context)
+     * @throws IllegalArgumentException if {@code action} is {@code null}
+     */
+    public static void defer(Deque<Closure<?>> scope, Closure<?> action) {
+        if (scope == null) {
+            throw new IllegalStateException("defer must be used inside an async method or closure");
+        }
+        if (action == null) {
+            throw new IllegalArgumentException("defer action must not be null");
+        }
+        scope.push(action);
+    }
+
+    /**
+     * Executes all deferred actions in the given scope in LIFO order.
+     * Each action is executed independently: if one throws an exception,
+     * subsequent actions still execute.  If multiple actions throw, the
+     * first exception is rethrown and subsequent ones are added as
+     * {@linkplain Throwable#addSuppressed(Throwable) suppressed exceptions}.
+     * <p>
+     * Called by compiler-generated code in the {@code finally} block of
+     * methods that contain {@code defer} statements.
+     * <p>
+     * A {@code null} or empty scope is treated as a no-op for robustness.
+     *
+     * @param scope the defer scope to execute; may be {@code null}
+     */
+    public static void executeDeferScope(Deque<Closure<?>> scope) {
+        if (scope == null || scope.isEmpty()) return;
+        Throwable firstError = null;
+        while (!scope.isEmpty()) {
+            try {
+                scope.pop().call();
+            } catch (Throwable t) {
+                if (firstError == null) {
+                    firstError = t;
+                } else {
+                    firstError.addSuppressed(t);
+                }
+            }
+        }
+        if (firstError != null) {
+            sneakyThrow(firstError);
+        }
+    }
+
+    // ---- internal -------------------------------------------------------
+
+    /**
+     * Deeply unwraps nested exception wrapper layers ({@link CompletionException},
+     * {@link ExecutionException}, {@link UndeclaredThrowableException},
+     * {@link InvocationTargetException}) to find the original root cause.
+     * <p>
+     * A depth limit of 64 is imposed to guard against circular cause chains,
+     * which could otherwise cause an infinite loop.
+     *
+     * @param t the wrapper exception
+     * @return the innermost non-wrapper exception, or the deepest reachable
+     *         cause if the depth limit is reached
+     */
+    public static Throwable deepUnwrap(Throwable t) {
+        Throwable cause = t;
+        int depth = 0;
+        while (cause.getCause() != null && depth++ < 64
+                && (cause instanceof CompletionException
+                    || cause instanceof ExecutionException
+                    || cause instanceof UndeclaredThrowableException
+                    || cause instanceof InvocationTargetException)) {
+            cause = cause.getCause();
+        }
+        return cause;
+    }
+
+    /**
+     * Unwraps and rethrows the original exception transparently, bypassing
+     * Java's checked-exception constraints via {@link #sneakyThrow}.
+     */
+    static RuntimeException rethrowUnwrapped(Throwable wrapper) {
+        Throwable cause = deepUnwrap(wrapper);
+        if (cause instanceof Error err) throw err;
+        throw sneakyThrow(cause);
+    }
+
+    /**
+     * Throws the given exception as an unchecked type using type-erasure.
+     * Enables transparent propagation of checked exceptions from async code.
+     */
+    @SuppressWarnings("unchecked")
+    static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
+        throw (T) t;
+    }
+
+    /**
+     * Wraps a throwable in a {@link CompletionException} if it is not already one.
+     * Used when rethrowing from within {@link CompletableFuture} lambdas.
+     */
+    static CompletionException wrapForFuture(Throwable t) {
+        if (t instanceof CompletionException ce) return ce;
+        return new CompletionException(t);
+    }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
new file mode 100644
index 0000000000..0b3a85a03e
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
@@ -0,0 +1,136 @@
+/*
+ *  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 java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+
+/**
+ * Default {@link Awaitable} implementation backed by a {@link CompletableFuture}.
+ * <p>
+ * This is the concrete type returned by {@code async} methods.  It delegates
+ * all operations to an underlying {@code CompletableFuture} while keeping the
+ * public API limited to the {@code Awaitable} contract, thereby decoupling
+ * user code from JDK-specific async APIs.
+ * <p>
+ * This class is an internal implementation detail and should not be referenced
+ * directly by user code. Use the {@link Awaitable} interface instead.
+ *
+ * @param <T> the result type
+ * @see Awaitable
+ * @since 6.0.0
+ */
+public class GroovyPromise<T> implements Awaitable<T> {
+
+    private final CompletableFuture<T> future;
+
+    public GroovyPromise(CompletableFuture<T> future) {
+        this.future = Objects.requireNonNull(future, "future must not be null");
+    }
+
+    /**
+     * Creates a {@code GroovyPromise} wrapping the given {@link CompletableFuture}.
+     */
+    public static <T> GroovyPromise<T> of(CompletableFuture<T> future) {
+        return new GroovyPromise<>(future);
+    }
+
+    @Override
+    public T get() throws InterruptedException, ExecutionException {
+        return future.get();
+    }
+
+    @Override
+    public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        return future.get(timeout, unit);
+    }
+
+    @Override
+    public boolean isDone() {
+        return future.isDone();
+    }
+
+    /**
+     * Attempts to cancel this computation. Delegates to
+     * {@link CompletableFuture#cancel(boolean) CompletableFuture.cancel(true)}.
+     * <p>
+     * <b>Note:</b> {@code CompletableFuture} cancellation sets the future's state
+     * to cancelled but does <em>not</em> reliably interrupt the underlying thread.
+     * Async work already in progress may continue running in the background.
+     * For cooperative cancellation, check {@link Thread#isInterrupted()} in
+     * long-running async bodies.
+     *
+     * @return {@code true} if the future was successfully cancelled
+     */
+    @Override
+    public boolean cancel() {
+        return future.cancel(true);
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return future.isCancelled();
+    }
+
+    @Override
+    public boolean isCompletedExceptionally() {
+        return future.isCompletedExceptionally();
+    }
+
+    @Override
+    public <U> Awaitable<U> then(Function<? super T, ? extends U> fn) {
+        return new GroovyPromise<>(future.thenApply(fn));
+    }
+
+    @Override
+    public <U> Awaitable<U> thenCompose(Function<? super T, ? extends Awaitable<U>> fn) {
+        return new GroovyPromise<>(future.thenCompose(t -> fn.apply(t).toCompletableFuture()));
+    }
+
+    @Override
+    public Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn) {
+        return new GroovyPromise<>(future.exceptionally(t -> {
+            // Unwrap all wrapper layers so handler sees the original exception
+            Throwable cause = AsyncSupport.deepUnwrap(t);
+            return fn.apply(cause);
+        }));
+    }
+
+    @Override
+    public CompletableFuture<T> toCompletableFuture() {
+        return future;
+    }
+
+    @Override
+    public String toString() {
+        if (future.isDone()) {
+            if (future.isCompletedExceptionally()) {
+                return "GroovyPromise{failed}";
+            }
+            return "GroovyPromise{completed}";
+        }
+        return "GroovyPromise{pending}";
+    }
+}
diff --git a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
index e76e1b4849..a141ffae12 100644
--- a/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ModifierNode.java
@@ -25,6 +25,7 @@ import java.util.Map;
 import java.util.Objects;
 
 import static org.apache.groovy.parser.antlr4.GroovyParser.ABSTRACT;
+import static org.apache.groovy.parser.antlr4.GroovyParser.ASYNC;
 import static org.apache.groovy.parser.antlr4.GroovyParser.DEF;
 import static org.apache.groovy.parser.antlr4.GroovyParser.DEFAULT;
 import static org.apache.groovy.parser.antlr4.GroovyParser.FINAL;
@@ -55,6 +56,7 @@ public class ModifierNode extends ASTNode {
     public static final int ANNOTATION_TYPE = -999;
     public static final Map<Integer, Integer> MODIFIER_OPCODE_MAP = Maps.of(
             ANNOTATION_TYPE, 0,
+            ASYNC, 0,
             DEF, 0,
             VAR, 0,
 
diff --git a/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java
new file mode 100644
index 0000000000..e39280af9e
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java
@@ -0,0 +1,248 @@
+/*
+ *  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.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.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.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+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.block;
+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
+ *       the runtime's {@code await()} via {@link AsyncTransformHelper}</li>
+ *   <li>The method body is executed asynchronously — or as an async generator
+ *       if it contains {@code yield return} — via factory methods on
+ *       {@link AsyncTransformHelper}</li>
+ *   <li>Methods containing {@code defer} statements are wrapped in a
+ *       try-finally block that executes deferred actions in LIFO order</li>
+ *   <li>The return type becomes {@code Awaitable<T>}
+ *       (or {@code AsyncStream<T>} for generators)</li>
+ * </ol>
+ * <p>
+ * All AST node construction for async runtime calls is delegated to
+ * {@link AsyncTransformHelper}, which encapsulates the internal method
+ * names and class references. This class focuses on method-level
+ * validation and structural transformation.
+ * <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 AsyncTransformHelper
+ * @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);
+
+    @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;
+        }
+        Statement originalBody = mNode.getCode();
+        if (originalBody == null) return;
+
+        ClassNode cNode = mNode.getDeclaringClass();
+
+        // Resolve executor expression
+        String executorFieldName = getMemberStringValue(anno, "executor");
+        Expression executorExpr;
+        if (executorFieldName != null && !executorFieldName.isEmpty()) {
+            FieldNode field = cNode.getDeclaredField(executorFieldName);
+            if (field == null) {
+                addError(MY_TYPE_NAME + ": executor field '" + executorFieldName
+                        + "' not found in class " + cNode.getName(), mNode);
+                return;
+            }
+            if (mNode.isStatic() && !field.isStatic()) {
+                addError(MY_TYPE_NAME + ": executor field '" + executorFieldName
+                        + "' must be static for static method '" + mNode.getName() + "'", mNode);
+                return;
+            }
+            executorExpr = varX(executorFieldName);
+        } else {
+            executorExpr = AsyncTransformHelper.buildGetExecutorCall();
+        }
+
+        // Step 1: Transform await() method calls to AsyncSupport.await()
+        new AwaitCallTransformer(source).visitMethod(mNode);
+
+        // Step 2: Check if body contains yield return (AsyncSupport.yieldReturn) calls
+        Statement transformedBody = mNode.getCode();
+        boolean hasYieldReturn = AsyncTransformHelper.containsYieldReturn(transformedBody);
+        boolean hasDefer = AsyncTransformHelper.containsDefer(transformedBody);
+
+        // Step 2a: If defer is present, wrap body in try-finally with defer scope
+        if (hasDefer) {
+            transformedBody = AsyncTransformHelper.wrapWithDeferScope(transformedBody);
+        }
+
+        Parameter[] closureParams;
+        if (hasYieldReturn) {
+            closureParams = new Parameter[]{ AsyncTransformHelper.createGenParam() };
+        } else {
+            closureParams = Parameter.EMPTY_ARRAY;
+        }
+        ClosureExpression closure = new ClosureExpression(closureParams, transformedBody);
+        VariableScope closureScope = new VariableScope(mNode.getVariableScope());
+        for (Parameter p : mNode.getParameters()) {
+            p.setClosureSharedVariable(true);
+            closureScope.putReferencedLocalVariable(p);
+        }
+        closure.setVariableScope(closureScope);
+
+        if (hasYieldReturn) {
+            // Transform yieldReturn(expr) → yieldReturn($__asyncGen__, expr) in closure body
+            AsyncTransformHelper.injectGenParamIntoYieldReturnCalls(transformedBody, closureParams[0]);
+
+            // Async generator: wrap body in AsyncSupport.generateAsyncStream { ... }
+            Expression genCall = AsyncTransformHelper.buildGenerateAsyncStreamCall(closure);
+            mNode.setCode(block(returnS(genCall)));
+
+            // Return type: AsyncStream<T>
+            ClassNode innerType;
+            if (ClassHelper.isPrimitiveVoid(originalReturnType) || ClassHelper.OBJECT_TYPE.equals(originalReturnType)) {
+                innerType = ClassHelper.OBJECT_TYPE;
+            } else if (ClassHelper.isPrimitiveType(originalReturnType)) {
+                innerType = ClassHelper.getWrapper(originalReturnType);
+            } else {
+                innerType = originalReturnType;
+            }
+            ClassNode streamReturnType = makeClassSafeWithGenerics(ASYNC_STREAM_TYPE, new GenericsType(innerType));
+            mNode.setReturnType(streamReturnType);
+        } else {
+            // Regular async: delegate to AsyncSupport.executeAsync/executeAsyncVoid
+            boolean isVoid = ClassHelper.isPrimitiveVoid(originalReturnType);
+            Expression asyncCall;
+            if (isVoid) {
+                asyncCall = AsyncTransformHelper.buildExecuteAsyncVoidCall(closure, executorExpr);
+            } else {
+                asyncCall = AsyncTransformHelper.buildExecuteAsyncCall(closure, executorExpr);
+            }
+            mNode.setCode(block(returnS(asyncCall)));
+
+            // Return type: Awaitable<T>
+            ClassNode innerType;
+            if (isVoid) {
+                innerType = ClassHelper.void_WRAPPER_TYPE;
+            } else if (ClassHelper.isPrimitiveType(originalReturnType)) {
+                innerType = ClassHelper.getWrapper(originalReturnType);
+            } else {
+                innerType = originalReturnType;
+            }
+            ClassNode awaitableReturnType = makeClassSafeWithGenerics(AWAITABLE_TYPE, new GenericsType(innerType));
+            mNode.setReturnType(awaitableReturnType);
+        }
+    }
+
+    /**
+     * Walks the method body and transforms {@code await(expr)} method calls
+     * into {@code AsyncSupport.await(expr)} static calls.  With the native
+     * grammar ({@code await expr}), most calls are already compiled to
+     * {@code AsyncSupport.await()} by the parser; this transformer handles
+     * the {@code await(expr)} call-style used in {@link Async @Async}-annotated
+     * methods where {@code await} is parsed as a regular method call.
+     */
+    private static class AwaitCallTransformer extends ClassCodeExpressionTransformer {
+
+        private final SourceUnit sourceUnit;
+
+        AwaitCallTransformer(SourceUnit sourceUnit) {
+            this.sourceUnit = sourceUnit;
+        }
+
+        @Override
+        protected SourceUnit getSourceUnit() {
+            return sourceUnit;
+        }
+
+        @Override
+        public Expression transform(Expression expr) {
+            if (expr == null) return null;
+
+            if (expr instanceof ClosureExpression ce) {
+                ce.getCode().visit(this);
+                return ce;
+            }
+
+            if (expr instanceof MethodCallExpression mce
+                    && AsyncTransformHelper.isAwaitMethodName(mce.getMethodAsString())
+                    && mce.isImplicitThis()) {
+                Expression transformedArgs = mce.getArguments().transformExpression(this);
+                Expression replacement = AsyncTransformHelper.buildAwaitCall(transformedArgs);
+                replacement.setSourcePosition(expr);
+                return replacement;
+            }
+
+            return expr.transformExpression(this);
+        }
+    }
+}
diff --git a/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
new file mode 100644
index 0000000000..1c084d01d3
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
@@ -0,0 +1,407 @@
+/*
+ *  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.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.DeclarationExpression;
+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.ast.stmt.TryCatchStatement;
+import org.codehaus.groovy.syntax.Token;
+import org.codehaus.groovy.syntax.Types;
+
+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.varX;
+
+/**
+ * Shared AST utilities for the {@code async}/{@code await} language feature.
+ * <p>
+ * Both the ANTLR4 parser ({@code AstBuilder}) and the annotation-driven
+ * {@link AsyncASTTransformation} need to detect, rewrite, and generate AST
+ * nodes for {@code await}, {@code yield return}, {@code defer}, and other
+ * async constructs. This helper class centralises those operations behind
+ * a clean API so that:
+ * <ul>
+ *   <li>Internal constants (method names, variable names, type references)
+ *       remain encapsulated — callers never reference them directly.</li>
+ *   <li>The two code paths (parser and annotation transform) stay in sync
+ *       with no duplicated logic.</li>
+ * </ul>
+ * <p>
+ * <b>AST factory methods</b> (e.g.
+ * {@link #buildAwaitCall(Expression)},
+ * {@link #buildYieldReturnCall(Expression)},
+ * {@link #buildDeferCall(Expression)})
+ * construct fully-formed AST nodes that reference the internal
+ * {@code AsyncSupport} runtime class, shielding callers from the underlying
+ * method names and class references.
+ * <p>
+ * <b>Statement scanners</b> ({@link #containsYieldReturn(Statement)},
+ * {@link #containsDefer(Statement)}) detect the presence of generator and
+ * defer constructs without descending into nested
+ * {@link ClosureExpression}s, since each nested {@code async { ... }} closure
+ * manages its own scope independently.
+ * <p>
+ * <b>Rewriting utilities</b>
+ * ({@link #injectGenParamIntoYieldReturnCalls(Statement, Parameter)},
+ * {@link #wrapWithDeferScope(Statement)}) perform targeted AST mutations
+ * that transform high-level syntax into the lower-level runtime calls.
+ *
+ * @see AsyncASTTransformation
+ * @since 6.0.0
+ */
+public final class AsyncTransformHelper {
+
+    // ---- internal constants (all private) --------------------------------
+
+    private static final String ASYNC_SUPPORT_CLASS = "org.apache.groovy.runtime.async.AsyncSupport";
+    private static final ClassNode ASYNC_SUPPORT_TYPE = ClassHelper.make(ASYNC_SUPPORT_CLASS);
+    private static final String ASYNC_GEN_PARAM_NAME = "$__asyncGen__";
+    private static final String DEFER_SCOPE_VAR = "$__deferScope__";
+
+    private static final String AWAIT_METHOD = "await";
+    private static final String YIELD_RETURN_METHOD = "yieldReturn";
+    private static final String WRAP_ASYNC_METHOD = "wrapAsync";
+    private static final String WRAP_ASYNC_GENERATOR_METHOD = "wrapAsyncGenerator";
+    private static final String GENERATE_ASYNC_STREAM_METHOD = "generateAsyncStream";
+    private static final String EXECUTE_ASYNC_METHOD = "executeAsync";
+    private static final String EXECUTE_ASYNC_VOID_METHOD = "executeAsyncVoid";
+    private static final String TO_ASYNC_STREAM_METHOD = "toAsyncStream";
+    private static final String CLOSE_STREAM_METHOD = "closeStream";
+    private static final String CREATE_DEFER_SCOPE_METHOD = "createDeferScope";
+    private static final String DEFER_METHOD = "defer";
+    private static final String EXECUTE_DEFER_SCOPE_METHOD = "executeDeferScope";
+
+    private AsyncTransformHelper() { }
+
+    // ---- AST factory methods --------------------------------------------
+
+    /**
+     * Ensures the expression is an {@link ArgumentListExpression}.
+     * If it already is one, returns it as-is; otherwise wraps it.
+     */
+    private static Expression ensureArgs(Expression expr) {
+        return expr instanceof ArgumentListExpression ? expr : new ArgumentListExpression(expr);
+    }
+
+    /**
+     * Builds {@code AsyncSupport.await(arg)}.
+     * Accepts either a single expression or a pre-assembled
+     * {@link ArgumentListExpression}.
+     *
+     * @param arg the expression to await
+     * @return an AST node representing the static call
+     */
+    public static Expression buildAwaitCall(Expression arg) {
+        return callX(ASYNC_SUPPORT_TYPE, AWAIT_METHOD, ensureArgs(arg));
+    }
+
+    /**
+     * Returns {@code true} if the given method name is the {@code await}
+     * keyword — used by the {@code @Async} annotation's
+     * {@link AsyncASTTransformation.AwaitCallTransformer AwaitCallTransformer}
+     * to identify {@code await(expr)} method calls that need rewriting.
+     *
+     * @param name the method name to check
+     * @return {@code true} if the name is {@code "await"}
+     */
+    public static boolean isAwaitMethodName(String name) {
+        return AWAIT_METHOD.equals(name);
+    }
+
+    /**
+     * Builds {@code AsyncSupport.yieldReturn(expr)}.
+     *
+     * @param arg the expression to yield
+     * @return an AST node representing the static call
+     */
+    public static Expression buildYieldReturnCall(Expression arg) {
+        return callX(ASYNC_SUPPORT_TYPE, YIELD_RETURN_METHOD, ensureArgs(arg));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.defer($__deferScope__, action)}.
+     * The synthetic defer-scope variable is injected automatically.
+     *
+     * @param action the deferred action expression (typically a closure)
+     * @return an AST node representing the static call
+     */
+    public static Expression buildDeferCall(Expression action) {
+        return callX(ASYNC_SUPPORT_TYPE, DEFER_METHOD,
+                args(varX(DEFER_SCOPE_VAR), action));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.toAsyncStream(source)}.
+     *
+     * @param source the expression to convert to an async stream
+     * @return an AST node representing the static call
+     */
+    public static Expression buildToAsyncStreamCall(Expression source) {
+        return callX(ASYNC_SUPPORT_TYPE, TO_ASYNC_STREAM_METHOD, ensureArgs(source));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.closeStream(stream)}.
+     *
+     * @param stream the stream expression to close
+     * @return an AST node representing the static call
+     */
+    public static Expression buildCloseStreamCall(Expression stream) {
+        return callX(ASYNC_SUPPORT_TYPE, CLOSE_STREAM_METHOD, ensureArgs(stream));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.generateAsyncStream(closure)}.
+     *
+     * @param closure the generator closure expression
+     * @return an AST node representing the static call
+     */
+    public static Expression buildGenerateAsyncStreamCall(Expression closure) {
+        return callX(ASYNC_SUPPORT_TYPE, GENERATE_ASYNC_STREAM_METHOD, ensureArgs(closure));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.wrapAsync(closure)}.
+     *
+     * @param closure the async closure expression
+     * @return an AST node representing the static call
+     */
+    public static Expression buildWrapAsyncCall(Expression closure) {
+        return callX(ASYNC_SUPPORT_TYPE, WRAP_ASYNC_METHOD, ensureArgs(closure));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.wrapAsyncGenerator(closure)}.
+     *
+     * @param closure the generator closure expression
+     * @return an AST node representing the static call
+     */
+    public static Expression buildWrapAsyncGeneratorCall(Expression closure) {
+        return callX(ASYNC_SUPPORT_TYPE, WRAP_ASYNC_GENERATOR_METHOD, ensureArgs(closure));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.executeAsync(closure, executor)}.
+     *
+     * @param closure  the async closure expression
+     * @param executor the executor expression
+     * @return an AST node representing the static call
+     */
+    public static Expression buildExecuteAsyncCall(Expression closure, Expression executor) {
+        return callX(ASYNC_SUPPORT_TYPE, EXECUTE_ASYNC_METHOD, args(closure, executor));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.executeAsyncVoid(closure, executor)}.
+     *
+     * @param closure  the async void closure expression
+     * @param executor the executor expression
+     * @return an AST node representing the static call
+     */
+    public static Expression buildExecuteAsyncVoidCall(Expression closure, Expression executor) {
+        return callX(ASYNC_SUPPORT_TYPE, EXECUTE_ASYNC_VOID_METHOD, args(closure, executor));
+    }
+
+    /**
+     * Builds {@code AsyncSupport.getExecutor()}.
+     *
+     * @return an AST node representing the static call
+     */
+    public static Expression buildGetExecutorCall() {
+        return callX(ASYNC_SUPPORT_TYPE, "getExecutor", ArgumentListExpression.EMPTY_ARGUMENTS);
+    }
+
+    // ---- statement scanners ---------------------------------------------
+
+    /**
+     * Checks whether the given statement tree contains any
+     * {@code AsyncSupport.yieldReturn()} calls, indicating that the enclosing
+     * body is an async generator (which should produce an
+     * {@link groovy.concurrent.AsyncStream AsyncStream} rather than an
+     * {@link groovy.concurrent.Awaitable Awaitable}).
+     * <p>
+     * The scan does <em>not</em> descend into nested {@link ClosureExpression}s
+     * because each nested {@code async { ... }} closure handles its own
+     * generator wrapping independently.
+     *
+     * @param code the statement tree to scan
+     * @return {@code true} if at least one {@code yieldReturn} call is found
+     */
+    public static boolean containsYieldReturn(Statement code) {
+        boolean[] found = {false};
+        code.visit(new CodeVisitorSupport() {
+            @Override
+            public void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
+                if (YIELD_RETURN_METHOD.equals(call.getMethod())
+                        && ASYNC_SUPPORT_CLASS.equals(call.getOwnerType().getName())) {
+                    found[0] = true;
+                }
+                if (!found[0]) {
+                    super.visitStaticMethodCallExpression(call);
+                }
+            }
+
+            @Override
+            public void visitClosureExpression(ClosureExpression expression) {
+                // Do not descend into nested closures — each manages its own generator
+            }
+        });
+        return found[0];
+    }
+
+    // ---- rewriting utilities --------------------------------------------
+
+    /**
+     * Rewrites {@code AsyncSupport.yieldReturn(expr)} calls to
+     * {@code AsyncSupport.yieldReturn($__asyncGen__, expr)}, injecting the
+     * synthetic generator parameter as the first argument.
+     * <p>
+     * This enables ThreadLocal-free yield semantics: the
+     * {@link org.apache.groovy.runtime.async.AsyncStreamGenerator} instance is
+     * passed explicitly through the closure parameter rather than being
+     * stored in a thread-local variable.
+     * <p>
+     * Does <em>not</em> descend into nested {@link ClosureExpression}s.
+     *
+     * @param code     the statement tree to transform
+     * @param genParam the synthetic {@code $__asyncGen__} parameter whose
+     *                 variable reference will be injected
+     */
+    public static void injectGenParamIntoYieldReturnCalls(Statement code, Parameter genParam) {
+        code.visit(new CodeVisitorSupport() {
+            @Override
+            public void visitExpressionStatement(ExpressionStatement stmt) {
+                Expression expr = stmt.getExpression();
+                if (expr instanceof StaticMethodCallExpression smce
+                        && YIELD_RETURN_METHOD.equals(smce.getMethod())
+                        && ASYNC_SUPPORT_CLASS.equals(smce.getOwnerType().getName())) {
+                    VariableExpression genRef = varX(ASYNC_GEN_PARAM_NAME);
+                    genRef.setAccessedVariable(genParam);
+                    ArgumentListExpression newArgs = new ArgumentListExpression();
+                    newArgs.addExpression(genRef);
+                    if (smce.getArguments() instanceof ArgumentListExpression argList) {
+                        for (Expression arg : argList.getExpressions()) {
+                            newArgs.addExpression(arg);
+                        }
+                    }
+                    StaticMethodCallExpression replacement =
+                            new StaticMethodCallExpression(smce.getOwnerType(), YIELD_RETURN_METHOD, newArgs);
+                    replacement.setSourcePosition(smce);
+                    stmt.setExpression(replacement);
+                }
+                super.visitExpressionStatement(stmt);
+            }
+
+            @Override
+            public void visitClosureExpression(ClosureExpression expression) {
+                // Do not descend into nested closures
+            }
+        });
+    }
+
+    /**
+     * Creates the synthetic {@code $__asyncGen__} parameter used by async
+     * generator closures.  The parameter is dynamically typed.
+     *
+     * @return a new {@link Parameter} instance
+     */
+    public static Parameter createGenParam() {
+        return new Parameter(ClassHelper.OBJECT_TYPE, ASYNC_GEN_PARAM_NAME);
+    }
+
+    /**
+     * Checks whether the given statement tree contains any
+     * {@code AsyncSupport.defer()} calls, indicating that the enclosing
+     * method uses the {@code defer} statement and needs a try-finally
+     * wrapper to execute deferred actions on exit.
+     * <p>
+     * Does <em>not</em> descend into nested {@link ClosureExpression}s.
+     *
+     * @param code the statement tree to scan
+     * @return {@code true} if at least one {@code defer} call is found
+     */
+    public static boolean containsDefer(Statement code) {
+        boolean[] found = {false};
+        code.visit(new CodeVisitorSupport() {
+            @Override
+            public void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
+                if (DEFER_METHOD.equals(call.getMethod())
+                        && ASYNC_SUPPORT_CLASS.equals(call.getOwnerType().getName())) {
+                    found[0] = true;
+                }
+                if (!found[0]) {
+                    super.visitStaticMethodCallExpression(call);
+                }
+            }
+
+            @Override
+            public void visitClosureExpression(ClosureExpression expression) {
+                // Do not descend into nested closures
+            }
+        });
+        return found[0];
+    }
+
+    /**
+     * Wraps the given statement body with defer scope management.
+     * Generates the equivalent of:
+     * <pre>{@code
+     * def $__deferScope__ = AsyncSupport.createDeferScope()
+     * try {
+     *     <originalBody>
+     * } finally {
+     *     AsyncSupport.executeDeferScope($__deferScope__)
+     * }
+     * }</pre>
+     *
+     * @param body the original method or closure body
+     * @return a new block statement with defer scope initialization and try-finally
+     */
+    public static Statement wrapWithDeferScope(Statement body) {
+        // def $__deferScope__ = AsyncSupport.createDeferScope()
+        Expression createCall = callX(ASYNC_SUPPORT_TYPE,
+                CREATE_DEFER_SCOPE_METHOD, ArgumentListExpression.EMPTY_ARGUMENTS);
+        ExpressionStatement initStmt = new ExpressionStatement(
+                new DeclarationExpression(
+                        varX(DEFER_SCOPE_VAR),
+                        Token.newSymbol(Types.ASSIGN, -1, -1),
+                        createCall));
+
+        // finally { AsyncSupport.executeDeferScope($__deferScope__) }
+        Expression executeCall = callX(ASYNC_SUPPORT_TYPE,
+                EXECUTE_DEFER_SCOPE_METHOD,
+                args(varX(DEFER_SCOPE_VAR)));
+        TryCatchStatement tryCatch = new TryCatchStatement(
+                body,
+                block(new ExpressionStatement(executeCall)));
+
+        return block(initStmt, tryCatch);
+    }
+}
diff --git a/src/spec/doc/core-async-await.adoc b/src/spec/doc/core-async-await.adoc
new file mode 100644
index 0000000000..9d804f4381
--- /dev/null
+++ b/src/spec/doc/core-async-await.adoc
@@ -0,0 +1,818 @@
+//////////////////////////////////////////
+
+  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.
+
+//////////////////////////////////////////
+
+= Async/Await
+:jdk: https://docs.oracle.com/en/java/javase/21/docs/api
+
+[[async-await]]
+== Introduction
+
+Groovy provides native `async`/`await` support as a language-level feature, enabling developers to write
+asynchronous code in a sequential, readable style. Inspired by similar constructs in JavaScript, C# and Go,
+Groovy's async/await integrates seamlessly with the JVM concurrency model while maintaining Groovy's hallmark
+conciseness and expressiveness.
+
+Key capabilities include:
+
+* **`async` methods** — declare methods that execute asynchronously and return an `Awaitable`
+* **`await` expressions** — suspend execution until an asynchronous result is available
+* **Async closures and lambdas** — create reusable asynchronous functions
+* **`for await`** — iterate over asynchronous data sources
+* **`yield return`** — produce asynchronous streams (generators)
+* **`defer`** — schedule Go-style cleanup actions that run when the method completes
+* **Framework integration** — built-in adapters for `CompletableFuture`, `Future`, and `Flow.Publisher`;
+extensible to RxJava, Reactor, and Spring via the adapter registry
+
+On JDK 21+, async methods automatically leverage
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual threads] for optimal scalability.
+
+[[async-methods]]
+== Async Methods
+
+An `async` method executes its body asynchronously and returns a
+`groovy.concurrent.Awaitable` — a promise-like abstraction analogous to C#'s `Task<T>` or JavaScript's
+`Promise<T>`.
+
+=== Basic Declaration
+
+Add the `async` modifier before the method's return type (or `def`):
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_method_basic,indent=0]
+----
+
+The caller receives an `Awaitable` immediately; the method body runs on a separate thread.
+Calling `get()` blocks until the result is available.
+
+=== Typed Return Values
+
+Async methods support explicit return types. The compiler wraps the declared type inside `Awaitable` automatically:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_method_typed,indent=0]
+----
+
+=== Void Methods
+
+Even `async void` methods return an `Awaitable<Void>`, allowing callers to wait for completion:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_method_void,indent=0]
+----
+
+=== Static Methods
+
+The `async` modifier works with static methods:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_method_static,indent=0]
+----
+
+=== Script-Level Methods
+
+Async methods can be defined directly in Groovy scripts:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_script,indent=0]
+----
+
+[[await-expression]]
+== The `await` Expression
+
+The `await` keyword suspends execution of the enclosing async method until the given asynchronous
+operation completes, then returns its result. It unwraps the value from an `Awaitable`,
+`CompletableFuture`, `CompletionStage`, `Future`, or any type registered with the adapter system.
+
+=== Basic Usage
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_basic,indent=0]
+----
+
+=== Awaiting CompletableFuture
+
+Groovy's `await` works directly with `java.util.concurrent.CompletableFuture`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_completable_future,indent=0]
+----
+
+=== Combining Multiple Awaits
+
+Multiple `await` expressions can appear in a single method:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_arithmetic,indent=0]
+----
+
+=== Parenthesized Form
+
+Both `await expr` and `await(expr)` are valid syntax:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_parenthesized,indent=0]
+----
+
+[NOTE]
+====
+The parenthesized form `await(expr)` is recommended when `await` is used in complex expressions
+(e.g., `await(f1) + await(f2)`) to avoid potential ambiguities.
+====
+
+[[async-closures-lambdas]]
+== Async Closures and Lambdas
+
+The `async` keyword can precede a closure or lambda expression to create a reusable
+asynchronous function. The result is a `Closure` that, when invoked, returns an `Awaitable`.
+
+[IMPORTANT]
+====
+`async { ... }` creates a _closure_, not an immediately executing task. You must explicitly
+invoke the closure (e.g., `asyncTask()`) to start asynchronous execution.
+This is analogous to defining an `async function` in JavaScript without calling it.
+====
+
+=== No-Argument Closure
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_closure_basic,indent=0]
+----
+
+=== Parameterized Closure
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_closure_params,indent=0]
+----
+
+=== Multiple Parameters
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_closure_multi_params,indent=0]
+----
+
+=== Closures in Collections
+
+Async closures are first-class values and can be stored in data structures:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_closure_collection,indent=0]
+----
+
+=== Lambda Syntax
+
+The `async` keyword also works with Groovy's lambda syntax:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_lambda,indent=0]
+----
+
+[[for-await]]
+== `for await` — Async Iteration
+
+The `for await` loop iterates over asynchronous data sources (`AsyncStream`, `Flow.Publisher`, `Iterable`).
+Each element is resolved asynchronously before the loop body executes. This is analogous to JavaScript's
+`for await...of` and C#'s `await foreach`.
+
+=== Basic Iteration
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_basic,indent=0]
+----
+
+=== Transformation
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_transform,indent=0]
+----
+
+=== Early Termination
+
+Standard flow control (`return`, `break`) works inside `for await`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_break,indent=0]
+----
+
+=== Colon Notation
+
+`for await` supports both `in` and `:` notation:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_colon,indent=0]
+----
+
+[[yield-return]]
+== `yield return` — Async Generators
+
+The `yield return` statement enables async methods to produce a stream of values lazily.
+A method containing `yield return` returns an `AsyncStream<T>` instead of a regular `Awaitable<T>`.
+This is analogous to C#'s `yield return` within `IAsyncEnumerable<T>` and JavaScript's `async function*`
+generators.
+
+The producer (generator) and consumer (`for await` loop) coordinate via a
+`SynchronousQueue`, providing natural back-pressure: the producer blocks on each `yield return`
+until the consumer is ready for the next value.
+
+=== Basic Generator
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=yield_return_basic,indent=0]
+----
+
+=== Generator with Loop
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=yield_return_loop,indent=0]
+----
+
+=== Filtered Generator
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=yield_return_filter,indent=0]
+----
+
+=== Combining `yield return` with `await`
+
+Async generators can perform asynchronous operations between yielding values:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=yield_return_with_await,indent=0]
+----
+
+[[defer]]
+== `defer` — Go-Style Cleanup
+
+The `defer` keyword schedules a block of code to execute when the enclosing async method
+completes, regardless of whether it succeeds or throws an exception. Multiple deferred blocks
+execute in _last-in, first-out_ (LIFO) order, exactly as in
+https://go.dev/blog/defer-panic-and-recover[Go's `defer` statement].
+
+=== Basic Usage
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=defer_basic,indent=0]
+----
+
+=== Guaranteed Execution on Exception
+
+Deferred blocks always execute, even when the method throws an exception:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=defer_exception,indent=0]
+----
+
+=== Resource Cleanup Pattern
+
+`defer` is ideal for resource management — acquire a resource, immediately defer its release:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=defer_resource_cleanup,indent=0]
+----
+
+[TIP]
+====
+When an exception occurs in a deferred block and the enclosing method has also thrown,
+the deferred exception is _suppressed_ (attached via `addSuppressed()`) so that the original
+exception remains the primary failure.
+====
+
+[[exception-handling]]
+== Exception Handling
+
+Groovy's async/await provides transparent exception propagation. Exceptions thrown inside
+an async method are captured by the `Awaitable` and re-thrown when the caller invokes `await`.
+The original exception type, message, and stack trace are preserved — wrapper types such as
+`CompletionException` and `ExecutionException` are automatically unwrapped.
+
+=== Exception Transparency
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=exception_transparency,indent=0]
+----
+
+Standard `try`/`catch` blocks work naturally with `await`, catching the original exception type
+(not a wrapper). This is the same transparency guarantee provided by C#'s `await` and
+JavaScript's `await`.
+
+=== Handling Failures from Multiple Tasks
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=exception_multiple_tasks,indent=0]
+----
+
+[[utility-methods]]
+== Combinators
+
+The `groovy.concurrent.Awaitable` interface provides static combinator methods for common async
+coordination patterns, analogous to JavaScript's `Promise.all()` / `Promise.any()` /
+`Promise.allSettled()` and C#'s `Task.WhenAll()` / `Task.WhenAny()`.
+
+=== `all` — Parallel Execution
+
+Wait for all tasks to complete and collect their results. Fails fast on the first error:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all,indent=0]
+----
+
+=== `any` — Race Pattern
+
+Return the result of the first task to complete:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_any,indent=0]
+----
+
+=== `allSettled` — Inspect All Outcomes
+
+Wait for all tasks to complete, collecting both successes and failures. Never throws —
+each result is wrapped in an `AwaitResult` with `isSuccess()`, `isFailure()`, `value`, and `error` properties:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all_settled,indent=0]
+----
+
+=== `delay` — Non-Blocking Pause
+
+Create an awaitable timer (analogous to JavaScript's `setTimeout` wrapped in a `Promise`):
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=delay_example,indent=0]
+----
+
+[[flow-publisher]]
+== `Flow.Publisher` Integration
+
+Groovy's await system has built-in support for
+{jdk}/java.base/java/util/concurrent/Flow.Publisher.html[`java.util.concurrent.Flow.Publisher`],
+enabling seamless consumption of reactive streams. The internal buffer is bounded (capacity 256)
+to prevent unbounded memory growth with fast publishers.
+
+=== Awaiting a Single Value
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=flow_publisher_await,indent=0]
+----
+
+=== Iterating with `for await`
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=flow_publisher_for_await,indent=0]
+----
+
+[[adapter-registry]]
+== Adapter Registry
+
+The `groovy.concurrent.AwaitableAdapterRegistry` allows extending `await` to support additional
+asynchronous types from third-party frameworks. Built-in adapters handle:
+
+* `groovy.concurrent.Awaitable` (native Groovy promise)
+* `java.util.concurrent.CompletableFuture` and `CompletionStage`
+* `java.util.concurrent.Future`
+* `java.util.concurrent.Flow.Publisher`
+
+=== Registering a Custom Adapter
+
+Implement the `AwaitableAdapter` interface and register it with the registry. The `register()`
+method returns an `AutoCloseable` handle for deregistration:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=adapter_registration,indent=0]
+----
+
+=== Framework Integration
+
+With appropriate adapters registered, `await` works transparently with any async type.
+Adapter implementations for frameworks such as RxJava 3, Project Reactor, and Spring
+should delegate to the framework's native blocking or conversion methods:
+
+* **RxJava 3**: `Single.blockingGet()`, `Observable.blockingIterable()` (for `AsyncStream`)
+* **Reactor**: `Mono.block()`, `Flux.toIterable()` (for `AsyncStream`)
+* **Spring**: Return `Awaitable` from `@Async` service methods; adapt `ListenableFuture` via `toCompletableFuture()`
+
+Adapters can also be discovered automatically via `java.util.ServiceLoader` by registering them
+in `META-INF/services/groovy.concurrent.AwaitableAdapter`.
+
+[[async-annotation]]
+== `@Async` Annotation
+
+As an alternative to the `async` keyword modifier, the `@Async` annotation provides the same
+functionality in a more traditional Java-style declaration:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_annotation,indent=0]
+----
+
+The `async` keyword modifier is the preferred form. The `@Async` annotation is useful when
+interoperating with tools or frameworks that rely on annotation processing. Both forms
+produce identical compiled bytecode.
+
+[[executor-configuration]]
+== Executor Configuration
+
+=== Default Behavior
+
+On **JDK 21+**, async methods run on
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual threads]
+for optimal scalability.
+On **JDK 17–20**, a cached thread pool is used (up to 256 threads by default, configurable
+via the `groovy.async.parallelism` system property). When all threads are busy, the submitting
+thread executes the task directly (caller-runs policy), providing natural back-pressure instead
+of rejecting the task.
+
+=== Custom Executor
+
+Override the default executor via `Awaitable.setExecutor()`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=custom_executor,indent=0]
+----
+
+Pass `null` to restore the default executor.
+
+[[patterns]]
+== Common Patterns
+
+=== Parallel Web Requests
+
+Launch multiple I/O tasks concurrently and collect all results:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=parallel_pattern,indent=0]
+----
+
+=== Retry with Exponential Backoff
+
+Automatically retry a transient failure with increasing delays:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=retry_pattern,indent=0]
+----
+
+=== Timeout
+
+Use `Awaitable.any()` to race an operation against a delay, implementing a timeout:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=timeout_pattern,indent=0]
+----
+
+[[cancellation]]
+=== Cancellation
+
+An `Awaitable` can be cancelled via the `cancel()` method. Once cancelled, any subsequent
+`await` on the task throws a `java.util.concurrent.CancellationException`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=cancellation,indent=0]
+----
+
+[NOTE]
+====
+Cancellation sets the `Awaitable` state to cancelled but does _not_ reliably interrupt the
+underlying thread (a limitation of `CompletableFuture`). For cooperative cancellation in
+long-running async bodies, check `Thread.currentThread().isInterrupted()` periodically.
+====
+
+[[best-practices]]
+== Best Practices
+
+[[async-all-the-way]]
+=== Asynchrony Is Viral — `await` All the Way
+
+Once a method becomes `async`, its callers should also be `async`. Partially async call chains
+— where an intermediate layer blocks with `.get()` — waste threads and risk pool starvation:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=sync_over_async,indent=0]
+----
+
+[[prefer-awaitable-of]]
+=== Prefer `Awaitable.of()` for Pre-Computed Data
+
+When the result is already known, use `Awaitable.of(value)` (equivalent to C#'s
+`Task.FromResult` and JavaScript's `Promise.resolve`) to avoid spawning a thread.
+Use `Awaitable.failed(error)` for pre-computed error results:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=awaitable_of,indent=0]
+----
+
+[[prefer-await-over-then]]
+=== Prefer `await` over `.then()` / `.thenCompose()`
+
+Sequential `await` calls read like synchronous code and provide better stack traces
+than chained `.then()` / `.thenCompose()`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=prefer_await_over_then,indent=0]
+----
+
+[[async-caching]]
+=== Async Lazy Caching
+
+Store `Awaitable` values in a `ConcurrentHashMap` so that only the first request triggers the
+async operation, and subsequent requests await the same cached result:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_caching,indent=0]
+----
+
+[[static-factory-pattern]]
+=== Static Factory for Async Construction
+
+When a class requires asynchronous initialization, use a static `async` factory
+method instead of blocking in the constructor:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=static_factory,indent=0]
+----
+
+[[fire-and-forget]]
+=== Fire-and-Forget Safely
+
+Always capture the `Awaitable` returned by async methods. Uncaptured awaitables may
+lose exceptions silently:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=fire_and_forget,indent=0]
+----
+
+[[implementation-details]]
+== Implementation Notes
+
+This section provides architectural context for Groovy language maintainers and advanced users.
+
+[[implementation-strategy]]
+=== Thread-Based vs State Machine
+
+Languages like C# transform async methods into **state machine** classes at
+compile time. Each `await` point becomes a numbered state, and the compiler rewrites the
+method body so it can be suspended and resumed without blocking an OS thread.
+
+Groovy takes a different approach: each `async` method body runs on its **own thread**
+(a virtual thread on JDK 21+, or a pooled platform thread on JDK 17–20). The `await`
+keyword blocks the running thread until the awaited result is available.
+
+On JDK 21+, virtual threads make this blocking essentially free — the JVM automatically
+parks the virtual thread and releases the underlying carrier thread, achieving the same
+practical efficiency as a state machine. The thread-based approach has several advantages:
+
+* **Simpler debugging** — stack traces show the complete call chain rather than
+  fragmented state machine frames
+* **Full language support** — all control-flow constructs (`try`/`catch`, loops,
+  `switch`, closures) work naturally inside async methods without special compiler handling
+* **Minimal compiler changes** — no need to rewrite control flow into a switch-based
+  state machine, keeping the implementation maintainable
+* **Natural blocking semantics** — existing blocking APIs (I/O, JDBC, etc.) work
+  transparently inside `async` methods without explicit wrapping
+
+On JDK 17–20, the thread pool is bounded (default 256 threads) with a caller-runs policy
+for back-pressure, ensuring stable performance even without virtual threads.
+
+[[architecture]]
+=== Architecture Overview
+
+The async/await feature is implemented across three layers:
+
+**Grammar layer** (`GroovyLexer.g4`, `GroovyParser.g4`): Defines `async`, `await`, `defer` as contextual
+keywords, along with parser rules for `for await`, `yield return`, and `defer` statements.
+
+**AST transformation layer** (`AstBuilder`, `AsyncASTTransformation`, `AsyncTransformHelper`):
+The `AstBuilder` converts parse trees into AST nodes, rewriting `async` method bodies into calls
+to `AsyncSupport` static methods. The `@Async` annotation is processed by `AsyncASTTransformation`,
+which delegates to the same `AsyncTransformHelper` utility class. This shared helper ensures
+consistent code generation for both the keyword and annotation forms.
+
+**Runtime layer** (`AsyncSupport`, `GroovyPromise`, `AsyncStreamGenerator`,
+`AwaitableAdapterRegistry`): `AsyncSupport` is the central runtime class containing static
+methods for `await`, `async`, `defer`, `yield return`, and combinators. `GroovyPromise` wraps
+`CompletableFuture` to implement the `Awaitable` interface, decoupling the public API from the
+JDK implementation. `AsyncStreamGenerator` implements the producer/consumer pattern for async
+generators using a `SynchronousQueue`. `AwaitableAdapterRegistry` provides the SPI extension
+point for third-party async type support.
+
+[[thread-safety]]
+=== Thread Safety
+
+All runtime components employ lock-free or minimal-contention synchronization:
+
+* `AsyncSupport.defaultExecutor` — `volatile` field for safe non-synchronized reads
+* `AwaitableAdapterRegistry.ADAPTERS` — `CopyOnWriteArrayList` for lock-free iteration during adapter lookup
+* `AwaitableAdapterRegistry.blockingExecutor` — `volatile` field
+* `AsyncStreamGenerator.current` — `volatile` field for producer/consumer visibility
+* `Flow.Publisher` adaptation — `AtomicReference<Subscription>` for thread-safe subscription management
+* Defer scopes — per-task `ArrayDeque`, no cross-thread sharing
+* `DELAY_SCHEDULER` — single daemon thread for non-blocking timer operations
+
+[[cross-language-comparison]]
+== Cross-Language Comparison
+
+The following table compares Groovy's async/await with the corresponding features in
+JavaScript and C#, for developers familiar with those languages.
+
+[cols="3,4,4,4"]
+|===
+| Feature | Groovy | JavaScript | C#
+
+| **Async declaration**
+| `async def foo() { }`
+| `async function foo() { }`
+| `async Task<T> Foo() { }`
+
+| **Await expression**
+| `await expr` or `await(expr)`
+| `await expr`
+| `await expr`
+
+| **Return type**
+| `Awaitable<T>`
+| `Promise<T>`
+| `Task<T>` / `ValueTask<T>`
+
+| **Async closure/lambda**
+| `async { -> body }`
+| `async () => body`
+| `async () => body`
+
+| **Async iteration**
+| `for await (x in src) { }`
+| `for await (x of src) { }`
+| `await foreach (x in src) { }`
+
+| **Async generator**
+| `yield return expr`
+| `yield expr` (in `async function*`)
+| `yield return expr` (in `IAsyncEnumerable<T>`)
+
+| **Deferred cleanup**
+| `defer { cleanup }`
+| _(none; use `try`/`finally`)_
+| _(none; use `try`/`finally` or `IAsyncDisposable`)_
+
+| **Wait all**
+| `Awaitable.all(a, b, c)`
+| `Promise.all([a, b, c])`
+| `Task.WhenAll(a, b, c)`
+
+| **Wait any**
+| `Awaitable.any(a, b, c)`
+| `Promise.any([a, b, c])`
+| `Task.WhenAny(a, b, c)`
+
+| **Wait all settled**
+| `Awaitable.allSettled(a, b, c)`
+| `Promise.allSettled([a, b, c])`
+| _(manual; no built-in equivalent)_
+
+| **Pre-computed result**
+| `Awaitable.of(value)`
+| `Promise.resolve(value)`
+| `Task.FromResult(value)`
+
+| **Pre-computed error**
+| `Awaitable.failed(error)`
+| `Promise.reject(error)`
+| `Task.FromException(ex)`
+
+| **Delay**
+| `Awaitable.delay(ms)`
+| `new Promise(r => setTimeout(r, ms))`
+| `Task.Delay(ms)`
+
+| **Cancellation**
+| `awaitable.cancel()`
+| `AbortController.abort()`
+| `CancellationToken`
+
+| **Executor/scheduler**
+| `Awaitable.setExecutor(exec)`
+| _(event loop; not configurable)_
+| `TaskScheduler`
+
+| **Third-party support**
+| `AwaitableAdapterRegistry` SPI
+| _(native `thenable` protocol)_
+| Custom awaiters via `GetAwaiter()`
+
+| **Annotation form**
+| `@Async`
+| _(none)_
+| _(none; attribute-driven async not supported)_
+
+| **Implementation**
+| Thread-per-task (virtual threads on JDK 21+)
+| Event loop + microtask queue
+| State machine (compiled IL transformation)
+|===
+
+[[summary]]
+== Summary
+
+[cols="2,5"]
+|===
+| Feature | Syntax
+
+| Async method
+| `async returnType methodName(params) { ... }`
+
+| Async closure
+| `def fn = async { params -> body }; await(fn(args))`
+
+| Async lambda
+| `def fn = async (params) -> { body }; await(fn(args))`
+
+| Await expression
+| `await expr` or `await(expr)`
+
+| For await
+| `for await (item in source) { ... }` or `for await (item : source) { ... }`
+
+| Yield return
+| `yield return expr` (inside async methods)
+
+| Defer
+| `defer { cleanup code }`
+
+| Parallel wait
+| `await Awaitable.all(a, b, c)`
+
+| Race
+| `await Awaitable.any(a, b)`
+
+| All settled
+| `await Awaitable.allSettled(a, b, c)`
+
+| Delay
+| `Awaitable.delay(millis)`
+
+| Pre-computed result
+| `Awaitable.of(value)` / `Awaitable.failed(error)`
+
+| Annotation form
+| `@Async def methodName() { ... }`
+|===
diff --git a/src/spec/test/AsyncAwaitSpecTest.groovy b/src/spec/test/AsyncAwaitSpecTest.groovy
new file mode 100644
index 0000000000..4b03993f85
--- /dev/null
+++ b/src/spec/test/AsyncAwaitSpecTest.groovy
@@ -0,0 +1,1097 @@
+/*
+ *  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.
+ */
+
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Spec tests for async/await documentation.
+ * <p>
+ * Tagged code snippets in this file are referenced by {@code core-async-await.adoc}
+ * via AsciiDoc {@code include::} directives.
+ */
+class AsyncAwaitSpecTest {
+
+    // =========================================================================
+    // 1. Basic async methods
+    // =========================================================================
+
+    @Test
+    void testAsyncMethodBasic() {
+        assertScript '''
+// tag::async_method_basic[]
+import groovy.concurrent.Awaitable
+
+class GreetingService {
+    async greet(String name) {
+        return "Hello, ${name}!"
+    }
+}
+
+def service = new GreetingService()
+def awaitable = service.greet("World")
+assert awaitable instanceof Awaitable
+assert awaitable.get() == "Hello, World!"
+// end::async_method_basic[]
+        '''
+    }
+
+    @Test
+    void testAsyncMethodTyped() {
+        assertScript '''
+// tag::async_method_typed[]
+class MathService {
+    async int square(int n) { n * n }
+    async String upper(String s) { s.toUpperCase() }
+}
+
+def svc = new MathService()
+assert svc.square(7).get() == 49
+assert svc.upper("groovy").get() == "GROOVY"
+// end::async_method_typed[]
+        '''
+    }
+
+    @Test
+    void testAsyncMethodVoid() {
+        assertScript '''
+// tag::async_method_void[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.atomic.AtomicReference
+
+class Logger {
+    AtomicReference<String> lastMessage = new AtomicReference<>()
+
+    async void log(String msg) {
+        lastMessage.set(msg)
+    }
+}
+
+def logger = new Logger()
+def a = logger.log("event occurred")
+assert a instanceof Awaitable
+a.get()
+assert logger.lastMessage.get() == "event occurred"
+// end::async_method_void[]
+        '''
+    }
+
+    @Test
+    void testAsyncMethodStatic() {
+        assertScript '''
+// tag::async_method_static[]
+class Util {
+    async static int add(int a, int b) { a + b }
+}
+
+assert Util.add(3, 4).get() == 7
+// end::async_method_static[]
+        '''
+    }
+
+    @Test
+    void testAsyncScript() {
+        assertScript '''
+// tag::async_script[]
+async multiply(int a, int b) { a * b }
+
+def result = multiply(6, 7)
+assert result.get() == 42
+// end::async_script[]
+        '''
+    }
+
+    // =========================================================================
+    // 2. await expression
+    // =========================================================================
+
+    @Test
+    void testAwaitBasic() {
+        assertScript '''
+// tag::await_basic[]
+class DataService {
+    async fetchData() { "database result" }
+
+    async process() {
+        def data = await fetchData()
+        return "Processed: ${data}"
+    }
+}
+
+def result = await(new DataService().process())
+assert result == "Processed: database result"
+// end::await_basic[]
+        '''
+    }
+
+    @Test
+    void testAwaitCompletableFuture() {
+        assertScript '''
+// tag::await_completable_future[]
+import java.util.concurrent.CompletableFuture
+
+class Service {
+    async compute() {
+        def value = await CompletableFuture.supplyAsync { 42 }
+        return value * 2
+    }
+}
+
+assert await(new Service().compute()) == 84
+// end::await_completable_future[]
+        '''
+    }
+
+    @Test
+    void testAwaitArithmetic() {
+        assertScript '''
+// tag::await_arithmetic[]
+import java.util.concurrent.CompletableFuture
+
+class Calculator {
+    async sum() {
+        def a = await(CompletableFuture.supplyAsync { 10 })
+        def b = await(CompletableFuture.supplyAsync { 20 })
+        return a + b
+    }
+}
+
+assert await(new Calculator().sum()) == 30
+// end::await_arithmetic[]
+        '''
+    }
+
+    @Test
+    void testAwaitParenthesized() {
+        assertScript '''
+// tag::await_parenthesized[]
+import java.util.concurrent.CompletableFuture
+
+async compute() {
+    // Both forms are equivalent:
+    def a = await CompletableFuture.supplyAsync { 10 }
+    def b = await(CompletableFuture.supplyAsync { 20 })
+    return a + b
+}
+
+assert await(compute()) == 30
+// end::await_parenthesized[]
+        '''
+    }
+
+    // =========================================================================
+    // 3. Async closures and lambdas
+    // =========================================================================
+
+    @Test
+    void testAsyncClosureBasic() {
+        assertScript '''
+// tag::async_closure_basic[]
+import groovy.concurrent.Awaitable
+
+// async { ... } creates a Closure that returns an Awaitable
+def asyncTask = async { 1 + 2 }
+assert asyncTask instanceof Closure
+
+// Explicit invocation is required to start execution
+def awaitable = asyncTask()
+assert awaitable instanceof Awaitable
+assert await(awaitable) == 3
+// end::async_closure_basic[]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithParams() {
+        assertScript '''
+// tag::async_closure_params[]
+def asyncSquare = async { int n -> n * n }
+
+// Call with argument, then await the result
+assert await(asyncSquare(5)) == 25
+assert await(asyncSquare(7)) == 49
+// end::async_closure_params[]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureMultipleParams() {
+        assertScript '''
+// tag::async_closure_multi_params[]
+def asyncConcat = async { a, b, sep -> "${a}${sep}${b}" }
+assert await(asyncConcat("hello", "world", " ")) == "hello world"
+// end::async_closure_multi_params[]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureInCollection() {
+        assertScript '''
+// tag::async_closure_collection[]
+def ops = [
+    add: async { a, b -> a + b },
+    mul: async { a, b -> a * b },
+    sub: async { a, b -> a - b }
+]
+
+assert await(ops.add(3, 4)) == 7
+assert await(ops.mul(3, 4)) == 12
+assert await(ops.sub(10, 4)) == 6
+// end::async_closure_collection[]
+        '''
+    }
+
+    @Test
+    void testAsyncLambda() {
+        assertScript '''
+// tag::async_lambda[]
+def asyncDouble = async (n) -> { n * 2 }
+assert await(asyncDouble(21)) == 42
+
+def asyncGreet = async (name) -> { "Hello, ${name}!" }
+assert await(asyncGreet("Groovy")) == "Hello, Groovy!"
+// end::async_lambda[]
+        '''
+    }
+
+    // =========================================================================
+    // 4. for await — async iteration
+    // =========================================================================
+
+    @Test
+    void testForAwaitBasic() {
+        assertScript '''
+// tag::for_await_basic[]
+class Collector {
+    async collectItems() {
+        def results = []
+        for await (item in [10, 20, 30]) {
+            results << item
+        }
+        return results
+    }
+}
+
+assert await(new Collector().collectItems()) == [10, 20, 30]
+// end::for_await_basic[]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithTransform() {
+        assertScript '''
+// tag::for_await_transform[]
+class Transformer {
+    async doubleAll() {
+        def results = []
+        for await (item in [1, 2, 3, 4]) {
+            results << item * 2
+        }
+        return results
+    }
+}
+
+assert await(new Transformer().doubleAll()) == [2, 4, 6, 8]
+// end::for_await_transform[]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithBreak() {
+        assertScript '''
+// tag::for_await_break[]
+class Finder {
+    async findFirstOver(int threshold) {
+        for await (item in [5, 10, 15, 20, 25]) {
+            if (item > threshold) return item
+        }
+        return -1
+    }
+}
+
+assert await(new Finder().findFirstOver(12)) == 15
+// end::for_await_break[]
+        '''
+    }
+
+    // =========================================================================
+    // 5. yield return — async generators
+    // =========================================================================
+
+    @Test
+    void testYieldReturnBasic() {
+        assertScript '''
+// tag::yield_return_basic[]
+import groovy.concurrent.AsyncStream
+
+class NumberGenerator {
+    async numbers() {
+        yield return 1
+        yield return 2
+        yield return 3
+    }
+}
+
+def stream = new NumberGenerator().numbers()
+assert stream instanceof AsyncStream
+
+def results = []
+for await (n in stream) {
+    results << n
+}
+assert results == [1, 2, 3]
+// end::yield_return_basic[]
+        '''
+    }
+
+    @Test
+    void testYieldReturnLoop() {
+        assertScript '''
+// tag::yield_return_loop[]
+class RangeGenerator {
+    async range(int start, int end) {
+        for (int i = start; i <= end; i++) {
+            yield return i
+        }
+    }
+}
+
+def results = []
+for await (n in new RangeGenerator().range(1, 5)) {
+    results << n
+}
+assert results == [1, 2, 3, 4, 5]
+// end::yield_return_loop[]
+        '''
+    }
+
+    @Test
+    void testYieldReturnFilter() {
+        assertScript '''
+// tag::yield_return_filter[]
+class FilteredGenerator {
+    async evenNumbers(int max) {
+        for (int i = 1; i <= max; i++) {
+            if (i % 2 == 0) {
+                yield return i
+            }
+        }
+    }
+}
+
+def results = []
+for await (n in new FilteredGenerator().evenNumbers(10)) {
+    results << n
+}
+assert results == [2, 4, 6, 8, 10]
+// end::yield_return_filter[]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithAwait() {
+        assertScript '''
+// tag::yield_return_with_await[]
+import java.util.concurrent.CompletableFuture
+
+class AsyncDataGenerator {
+    async fetchSequence() {
+        for (int i = 1; i <= 3; i++) {
+            def value = await CompletableFuture.supplyAsync { i * 10 }
+            yield return value
+        }
+    }
+}
+
+def results = []
+for await (v in new AsyncDataGenerator().fetchSequence()) {
+    results << v
+}
+assert results == [10, 20, 30]
+// end::yield_return_with_await[]
+        '''
+    }
+
+    // =========================================================================
+    // 6. defer — Go-style cleanup
+    // =========================================================================
+
+    @Test
+    void testDeferBasic() {
+        assertScript '''
+// tag::defer_basic[]
+class DeferExample {
+    async runWithDefer() {
+        def log = []
+        defer { log << "first deferred" }
+        defer { log << "second deferred" }
+        defer { log << "third deferred" }
+        log << "body executed"
+        return log
+    }
+}
+
+def result = await(new DeferExample().runWithDefer())
+// Body runs first; deferred blocks execute in LIFO order
+assert result == ["body executed", "third deferred", "second deferred", "first deferred"]
+// end::defer_basic[]
+        '''
+    }
+
+    @Test
+    void testDeferOnException() {
+        assertScript '''
+// tag::defer_exception[]
+class ResourceHandler {
+    static log = []
+
+    async processWithCleanup() {
+        defer { log << "cleanup done" }
+        throw new RuntimeException("something went wrong")
+    }
+}
+
+try {
+    await(new ResourceHandler().processWithCleanup())
+} catch (RuntimeException e) {
+    assert e.message == "something went wrong"
+}
+// Deferred blocks always execute, even on exception
+assert ResourceHandler.log == ["cleanup done"]
+// end::defer_exception[]
+        '''
+    }
+
+    @Test
+    void testDeferResourceCleanup() {
+        assertScript '''
+// tag::defer_resource_cleanup[]
+class ResourceManager {
+    static resources = []
+    static cleanupLog = []
+
+    async processResources() {
+        def r1 = "database-conn"
+        resources << r1
+        defer { resources.remove(r1); cleanupLog << "closed ${r1}" }
+
+        def r2 = "file-handle"
+        resources << r2
+        defer { resources.remove(r2); cleanupLog << "closed ${r2}" }
+
+        assert resources.size() == 2
+        return "done"
+    }
+}
+
+assert await(new ResourceManager().processResources()) == "done"
+// LIFO order: r2 closed first, then r1
+assert ResourceManager.cleanupLog == ["closed file-handle", "closed database-conn"]
+assert ResourceManager.resources.isEmpty()
+// end::defer_resource_cleanup[]
+        '''
+    }
+
+    // =========================================================================
+    // 7. Exception handling
+    // =========================================================================
+
+    @Test
+    void testExceptionTransparency() {
+        assertScript '''
+// tag::exception_transparency[]
+async fetchData() {
+    throw new java.io.IOException("disk failure")
+}
+
+async caller() {
+    try {
+        await fetchData()
+        assert false : "should not reach here"
+    } catch (java.io.IOException e) {
+        return "Recovered: ${e.message}"
+    }
+}
+
+assert await(caller()) == "Recovered: disk failure"
+// end::exception_transparency[]
+        '''
+    }
+
+    @Test
+    void testExceptionMultipleTasks() {
+        assertScript '''
+// tag::exception_multiple_tasks[]
+async riskyTask(boolean shouldFail) {
+    if (shouldFail) throw new IllegalStateException("task failed")
+    return "success"
+}
+
+async coordinator() {
+    def results = []
+    for (flag in [false, true, false]) {
+        try {
+            results << await(riskyTask(flag))
+        } catch (IllegalStateException e) {
+            results << "error: ${e.message}"
+        }
+    }
+    return results
+}
+
+assert await(coordinator()) == ["success", "error: task failed", "success"]
+// end::exception_multiple_tasks[]
+        '''
+    }
+
+    // =========================================================================
+    // 8. Utility methods — awaitAll, awaitAny, awaitAllSettled, delay
+    // =========================================================================
+
+    @Test
+    void testAwaitAll() {
+        assertScript '''
+// tag::await_all[]
+import groovy.concurrent.Awaitable
+
+async fetchUser() { "Alice" }
+async fetchOrder() { "Order#42" }
+async fetchBalance() { 100.0 }
+
+def results = await(Awaitable.all(fetchUser(), fetchOrder(), fetchBalance()))
+assert results == ["Alice", "Order#42", 100.0]
+// end::await_all[]
+        '''
+    }
+
+    @Test
+    void testAwaitAny() {
+        assertScript '''
+// tag::await_any[]
+import groovy.concurrent.Awaitable
+
+def fast = async { "fast result" }
+def slow = async {
+    await(Awaitable.delay(2000))
+    return "slow result"
+}
+
+def winner = await(Awaitable.any(fast(), slow()))
+assert winner == "fast result"
+// end::await_any[]
+        '''
+    }
+
+    @Test
+    void testAwaitAllSettled() {
+        assertScript '''
+// tag::await_all_settled[]
+import groovy.concurrent.Awaitable
+
+async caller() {
+    def a = Awaitable.of(1)
+    def b = Awaitable.failed(new IOException("network error"))
+    def c = Awaitable.of(3)
+    return await(Awaitable.allSettled(a, b, c))
+}
+
+def results = await(caller())
+assert results.size() == 3
+
+assert results[0].isSuccess() && results[0].value == 1
+assert results[1].isFailure() && results[1].error.message == "network error"
+assert results[2].isSuccess() && results[2].value == 3
+// end::await_all_settled[]
+        '''
+    }
+
+    @Test
+    void testDelay() {
+        assertScript '''
+// tag::delay_example[]
+import groovy.concurrent.Awaitable
+
+async delayedGreeting() {
+    await(Awaitable.delay(100))  // pause for 100 milliseconds
+    return "Hello after delay"
+}
+
+assert await(delayedGreeting()) == "Hello after delay"
+// end::delay_example[]
+        '''
+    }
+
+    // =========================================================================
+    // 9. Flow.Publisher integration
+    // =========================================================================
+
+    @Test
+    void testFlowPublisherAwait() {
+        assertScript '''
+// tag::flow_publisher_await[]
+import java.util.concurrent.SubmissionPublisher
+
+def publisher = new SubmissionPublisher<String>()
+Thread.start {
+    Thread.sleep(200)
+    publisher.submit("hello from publisher")
+    publisher.close()
+}
+
+def result = await(publisher)
+assert result == "hello from publisher"
+// end::flow_publisher_await[]
+        '''
+    }
+
+    @Test
+    void testFlowPublisherForAwait() {
+        assertScript '''
+// tag::flow_publisher_for_await[]
+import java.util.concurrent.SubmissionPublisher
+
+class StreamConsumer {
+    async consumeAll(SubmissionPublisher<Integer> pub) {
+        def results = []
+        for await (item in pub) {
+            results << item
+        }
+        return results
+    }
+}
+
+def publisher = new SubmissionPublisher<Integer>()
+def future = new StreamConsumer().consumeAll(publisher)
+Thread.start {
+    Thread.sleep(50)
+    (1..5).each { publisher.submit(it) }
+    publisher.close()
+}
+
+assert await(future) == [1, 2, 3, 4, 5]
+// end::flow_publisher_for_await[]
+        '''
+    }
+
+    // =========================================================================
+    // 10. @Async annotation
+    // =========================================================================
+
+    @Test
+    void testAsyncAnnotation() {
+        assertScript '''
+// tag::async_annotation[]
+import groovy.transform.Async
+import groovy.concurrent.Awaitable
+
+class Service {
+    @Async
+    def fetchData() {
+        return "data loaded"
+    }
+}
+
+def svc = new Service()
+def awaitable = svc.fetchData()
+assert awaitable instanceof Awaitable
+assert awaitable.get() == "data loaded"
+// end::async_annotation[]
+        '''
+    }
+
+    // =========================================================================
+    // 11. Custom executor
+    // =========================================================================
+
+    @Test
+    void testCustomExecutor() {
+        assertScript '''
+// tag::custom_executor[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.Executors
+
+class WorkService {
+    async work() { Thread.currentThread().name }
+}
+
+def customPool = Executors.newFixedThreadPool(2)
+try {
+    Awaitable.setExecutor(customPool)
+
+    def threadName = await(new WorkService().work())
+    assert threadName.contains("pool")
+} finally {
+    Awaitable.setExecutor(null)  // restore default
+    customPool.shutdown()
+}
+// end::custom_executor[]
+        '''
+    }
+
+    // =========================================================================
+    // 12. Comprehensive pattern — parallel web scraping
+    // =========================================================================
+
+    @Test
+    void testParallelPattern() {
+        assertScript '''
+// tag::parallel_pattern[]
+import groovy.concurrent.Awaitable
+
+async fetchPage(String url) {
+    await(Awaitable.delay(10))  // simulate network I/O
+    return "Content of ${url}"
+}
+
+// Launch three tasks in parallel
+def pages = await(Awaitable.all(
+    fetchPage("https://example.com/page1"),
+    fetchPage("https://example.com/page2"),
+    fetchPage("https://example.com/page3")
+))
+
+assert pages.size() == 3
+assert pages.every { it.startsWith("Content of") }
+// end::parallel_pattern[]
+        '''
+    }
+
+    // =========================================================================
+    // 13. Common patterns — retry, timeout, cancellation
+    // =========================================================================
+
+    @Test
+    void testRetryPattern() {
+        assertScript '''
+// tag::retry_pattern[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.atomic.AtomicInteger
+
+class RetryDemo {
+    final AtomicInteger attempts = new AtomicInteger(0)
+
+    async fetchData() {
+        if (attempts.incrementAndGet() < 3) throw new IOException("transient error")
+        return "data"
+    }
+
+    async fetchWithRetry(int maxRetries) {
+        for (int attempt = 1; attempt <= maxRetries; attempt++) {
+            try {
+                return await fetchData()
+            } catch (Exception e) {
+                if (attempt == maxRetries) throw e
+                await(Awaitable.delay(10 * attempt))  // exponential backoff
+            }
+        }
+    }
+}
+
+def demo = new RetryDemo()
+assert await(demo.fetchWithRetry(5)) == "data"
+assert demo.attempts.get() == 3
+// end::retry_pattern[]
+        '''
+    }
+
+    @Test
+    void testTimeoutPattern() {
+        assertScript '''
+// tag::timeout_pattern[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.TimeoutException
+
+async longRunningTask() {
+    await(Awaitable.delay(5000))
+    return "completed"
+}
+
+async withTimeout(Awaitable task, long timeoutMs) {
+    def timeout = Awaitable.delay(timeoutMs).then { throw new TimeoutException("timed out") }
+    return await Awaitable.any(task, timeout)
+}
+
+try {
+    await withTimeout(longRunningTask(), 50)
+    assert false : "should have timed out"
+} catch (TimeoutException e) {
+    assert e.message == "timed out"
+}
+// end::timeout_pattern[]
+        '''
+    }
+
+    @Test
+    void testCancellation() {
+        assertScript '''
+// tag::cancellation[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.CancellationException
+
+async longRunningTask() {
+    await(Awaitable.delay(10_000))
+    return "done"
+}
+
+def task = longRunningTask()
+task.cancel()
+
+try {
+    await(task)
+    assert false : "should have thrown"
+} catch (CancellationException e) {
+    assert task.isCancelled()
+}
+// end::cancellation[]
+        '''
+    }
+
+    // =========================================================================
+    // 14. Best practices — Awaitable.of, async caching, static factory
+    // =========================================================================
+
+    @Test
+    void testAwaitableOf() {
+        assertScript '''
+// tag::awaitable_of[]
+import groovy.concurrent.Awaitable
+
+// No thread spawned — result is immediately available
+Awaitable<Integer> addAsync(int a, int b) {
+    return Awaitable.of(a + b)
+}
+
+assert await(addAsync(2, 3)) == 5
+
+// Pre-computed error result
+Awaitable<String> failFast() {
+    return Awaitable.failed(new IllegalStateException("not ready"))
+}
+
+try {
+    await failFast()
+    assert false
+} catch (IllegalStateException e) {
+    assert e.message == "not ready"
+}
+// end::awaitable_of[]
+        '''
+    }
+
+    @Test
+    void testAsyncCaching() {
+        assertScript '''
+// tag::async_caching[]
+import groovy.concurrent.Awaitable
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicInteger
+
+class CacheDemo {
+    final AtomicInteger fetchCount = new AtomicInteger(0)
+    final cache = new ConcurrentHashMap<Integer, Awaitable>()
+
+    async fetchPersonAsync(int id) {
+        fetchCount.incrementAndGet()
+        await(Awaitable.delay(10))
+        return "Person-${id}"
+    }
+
+    def getOrFetch(int id) {
+        cache.computeIfAbsent(id, { key -> fetchPersonAsync(key) })
+    }
+}
+
+def demo = new CacheDemo()
+
+// First call triggers actual fetch
+def p1 = await(demo.getOrFetch(42))
+// Second call returns cached Awaitable (no new fetch)
+def p2 = await(demo.getOrFetch(42))
+
+assert p1 == "Person-42"
+assert p2 == "Person-42"
+assert demo.fetchCount.get() == 1  // fetched only once
+// end::async_caching[]
+        '''
+    }
+
+    @Test
+    void testStaticFactoryPattern() {
+        assertScript '''
+// tag::static_factory[]
+import groovy.concurrent.Awaitable
+
+class RemoteService {
+    final String connectionId
+    private RemoteService(String connectionId) { this.connectionId = connectionId }
+
+    static async create() {
+        def connId = await connectToRemote()
+        return new RemoteService(connId)
+    }
+
+    private static async connectToRemote() {
+        await(Awaitable.delay(10))  // simulate I/O
+        return "conn-" + System.nanoTime()
+    }
+}
+
+def service = await(RemoteService.create())
+assert service.connectionId.startsWith("conn-")
+// end::static_factory[]
+        '''
+    }
+
+    // =========================================================================
+    // 15. Best practices — sync-over-async, prefer await, fire-and-forget
+    // =========================================================================
+
+    @Test
+    void testSyncOverAsync() {
+        assertScript '''
+// tag::sync_over_async[]
+import groovy.concurrent.Awaitable
+
+class ConsumerService {
+    // BAD: blocking sync-over-async
+    // def result = computeAsync(5).get()
+
+    // GOOD: propagate await through the call chain
+    async computeAsync(int n) {
+        await(Awaitable.delay(10))
+        return n * 2
+    }
+
+    async doSomething() {
+        def result = await computeAsync(5)
+        return result + 1
+    }
+}
+
+def svc = new ConsumerService()
+assert await(svc.doSomething()) == 11
+// end::sync_over_async[]
+        '''
+    }
+
+    @Test
+    void testPreferAwaitOverThen() {
+        assertScript '''
+// tag::prefer_await_over_then[]
+import groovy.concurrent.Awaitable
+
+class PipelineDemo {
+    async step1() { await(Awaitable.delay(5)); return 10 }
+    async step2(int n) { await(Awaitable.delay(5)); return n * 2 }
+    async step3(int n) { await(Awaitable.delay(5)); return n + 3 }
+
+    // More readable: sequential awaits
+    async pipeline() {
+        def a = await step1()
+        def b = await step2(a)
+        return await step3(b)
+    }
+}
+
+def demo = new PipelineDemo()
+assert await(demo.pipeline()) == 23
+// end::prefer_await_over_then[]
+        '''
+    }
+
+    @Test
+    void testFireAndForget() {
+        assertScript '''
+// tag::fire_and_forget[]
+import groovy.concurrent.Awaitable
+
+def errors = Collections.synchronizedList([])
+
+async backgroundOperation() {
+    await(Awaitable.delay(10))
+    throw new RuntimeException("background error")
+}
+
+// GOOD: capture for tracking
+def task = backgroundOperation()
+Thread.sleep(100)  // wait for completion
+assert task.isCompletedExceptionally()
+// end::fire_and_forget[]
+        '''
+    }
+
+    @Test
+    void testForAwaitColonNotation() {
+        assertScript '''
+// tag::for_await_colon[]
+class ColonNotationDemo {
+    async generateNumbers() {
+        for (int i = 1; i <= 3; i++) {
+            yield return i
+        }
+    }
+}
+
+def demo = new ColonNotationDemo()
+def results = []
+// for await supports both `in` and `:` notation
+for await (n : demo.generateNumbers()) {
+    results << n
+}
+assert results == [1, 2, 3]
+// end::for_await_colon[]
+        '''
+    }
+
+    // =========================================================================
+    // 16. Custom adapter registration
+    // =========================================================================
+
+    @Test
+    void testAdapterRegistration() {
+        assertScript '''
+// tag::adapter_registration[]
+import groovy.concurrent.AwaitableAdapterRegistry
+import groovy.concurrent.AwaitableAdapter
+import groovy.concurrent.Awaitable
+
+// A simple custom async type (simulating a third-party framework)
+class CustomFuture<T> {
+    private final T value
+    CustomFuture(T value) { this.value = value }
+    T blockingGet() { return value }
+}
+
+// Register adapter for the custom type
+def handle = AwaitableAdapterRegistry.register(new AwaitableAdapter() {
+    boolean supportsAwaitable(Class<?> type) { CustomFuture.isAssignableFrom(type) }
+    Awaitable toAwaitable(Object source) { Awaitable.of(((CustomFuture) source).blockingGet()) }
+})
+
+try {
+    def future = new CustomFuture<String>("hello from adapter")
+    assert await(future) == "hello from adapter"
+} finally {
+    handle.close()  // deregister when done
+}
+// end::adapter_registration[]
+        '''
+    }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy
new file mode 100644
index 0000000000..acbbce06ed
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncAwaitSyntaxTest.groovy
@@ -0,0 +1,2109 @@
+/*
+ *  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
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Comprehensive syntax-level tests for the {@code async}/{@code await}/{@code for await}
+ * language features. Every test uses {@code assertScript} to exercise the actual
+ * Groovy parser and AST transformation pipeline, ensuring the grammar, AstBuilder,
+ * and runtime all work end-to-end from the developer's perspective.
+ *
+ * <p>Organized by feature area:
+ * <ol>
+ *   <li>{@code async} modifier – method declarations</li>
+ *   <li>{@code await} expression – in various statement positions</li>
+ *   <li>{@code for await} – async iteration</li>
+ *   <li>Combined patterns – real-world usage scenarios</li>
+ *   <li>Backward compatibility – {@code async}/{@code await} as identifiers</li>
+ *   <li>Error cases – syntax and semantic rejections</li>
+ * </ol>
+ */
+class AsyncAwaitSyntaxTest {
+
+    // =====================================================================
+    // 1. 'async' modifier — method declarations
+    // =====================================================================
+
+    @Test
+    void testAsyncDefMethodReturnsAwaitable() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                async greet() { "hello" }
+            }
+            def r = new Svc().greet()
+            assert r instanceof Awaitable // its default implementation is `org.apache.groovy.runtime.async.GroovyPromise`
+            assert r.get() == "hello"
+        '''
+    }
+
+    @Test
+    void testAsyncTypedReturnMethod() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                async String upper(String s) { s.toUpperCase() }
+            }
+            assert new Svc().upper("abc").get() == "ABC"
+        '''
+    }
+
+    @Test
+    void testAsyncIntReturnMethod() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                async int square(int n) { n * n }
+            }
+            assert new Svc().square(7).get() == 49
+        '''
+    }
+
+    @Test
+    void testAsyncVoidMethodReturnsAwaitable() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.atomic.AtomicReference
+
+            class Svc {
+                AtomicReference<String> ref = new AtomicReference<>()
+                async void store(String v) { ref.set(v) }
+            }
+            def svc = new Svc()
+            def a = svc.store("done")
+            assert a instanceof Awaitable
+            a.get()
+            assert svc.ref.get() == "done"
+        '''
+    }
+
+    @Test
+    void testAsyncStaticMethod() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Util {
+                async static add(int a, int b) { a + b }
+            }
+            assert Util.add(3, 4).get() == 7
+        '''
+    }
+
+    @Test
+    void testAsyncDefInScript() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async multiply(int a, int b) { a * b }
+
+            def r = multiply(6, 7)
+            assert r instanceof Awaitable
+            assert r.get() == 42
+        '''
+    }
+
+    @Test
+    void testAsyncMethodWithMultipleParams() {
+        assertScript '''
+            class Svc {
+                async concat(String a, String b, String sep) {
+                    return a + sep + b
+                }
+            }
+            assert new Svc().concat("foo", "bar", "-").get() == "foo-bar"
+        '''
+    }
+
+    @Test
+    void testAsyncMethodWithDefaultParams() {
+        assertScript '''
+            class Svc {
+                async greet(String name, String greeting = "Hello") {
+                    "${greeting}, ${name}!"
+                }
+            }
+            def svc = new Svc()
+            assert svc.greet("World").get() == "Hello, World!"
+            assert svc.greet("World", "Hi").get() == "Hi, World!"
+        '''
+    }
+
+    @Test
+    void testAsyncMethodWithVarargs() {
+        assertScript '''
+            class Svc {
+                async sum(int... nums) {
+                    nums.sum()
+                }
+            }
+            assert new Svc().sum(1, 2, 3, 4).get() == 10
+        '''
+    }
+
+    @Test
+    void testAsyncMethodWithClosureParam() {
+        assertScript '''
+            class Svc {
+                async transform(String s, Closure fn) {
+                    fn(s)
+                }
+            }
+            assert new Svc().transform("hello") { it.reverse() }.get() == "olleh"
+        '''
+    }
+
+    @Test
+    void testAsyncMethodRunsAsynchronously() {
+        assertScript '''
+            class Svc {
+                async threadName() { Thread.currentThread().name }
+            }
+            def name = new Svc().threadName().get()
+            assert name != Thread.currentThread().name
+        '''
+    }
+
+    // =====================================================================
+    // 2. 'await' expression — in various statement positions
+    // =====================================================================
+
+    @Test
+    void testAwaitCompletableFuture() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async fetch() {
+                    def v = await CompletableFuture.supplyAsync { 42 }
+                    return v
+                }
+            }
+            assert new Svc().fetch().get() == 42
+        '''
+    }
+
+    @Test
+    void testAwaitAwaitableObject() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                async inner() { "from-inner" }
+                async outer() {
+                    def v = await inner()
+                    return "got: ${v}"
+                }
+            }
+            assert new Svc().outer().get() == "got: from-inner"
+        '''
+    }
+
+    @Test
+    void testAwaitWithParenthesizedExpression() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async compute() {
+                    def v = await CompletableFuture.supplyAsync { 99 }
+                    return v
+                }
+            }
+            assert new Svc().compute().get() == 99
+        '''
+    }
+
+    @Test
+    void testAwaitInAssignment() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async work() {
+                    String result
+                    result = await CompletableFuture.supplyAsync { "assigned" }
+                    return result
+                }
+            }
+            assert new Svc().work().get() == "assigned"
+        '''
+    }
+
+    @Test
+    void testAwaitInArithmeticExpression() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async calc() {
+                    def a = await CompletableFuture.supplyAsync { 10 }
+                    def b = await CompletableFuture.supplyAsync { 20 }
+                    return a + b + 5
+                }
+            }
+            assert new Svc().calc().get() == 35
+        '''
+    }
+
+    @Test
+    void testMultipleSequentialAwaits() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async pipeline() {
+                    def a = await CompletableFuture.supplyAsync { "step1" }
+                    def b = await CompletableFuture.supplyAsync { "${a}->step2" }
+                    def c = await CompletableFuture.supplyAsync { "${b}->step3" }
+                    return c
+                }
+            }
+            assert new Svc().pipeline().get() == "step1->step2->step3"
+        '''
+    }
+
+    @Test
+    void testAwaitInReturnStatement() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async direct() {
+                    return await CompletableFuture.supplyAsync { "direct-return" }
+                }
+            }
+            assert new Svc().direct().get() == "direct-return"
+        '''
+    }
+
+    @Test
+    void testAwaitInIfCondition() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async decide(boolean flag) {
+                    if (flag) {
+                        return await CompletableFuture.supplyAsync { "yes" }
+                    } else {
+                        return await CompletableFuture.supplyAsync { "no" }
+                    }
+                }
+            }
+            def svc = new Svc()
+            assert svc.decide(true).get() == "yes"
+            assert svc.decide(false).get() == "no"
+        '''
+    }
+
+    @Test
+    void testAwaitInTernaryExpression() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async ternary(boolean flag) {
+                    def v = flag ? (await CompletableFuture.supplyAsync { "T" })
+                                 : (await CompletableFuture.supplyAsync { "F" })
+                    return v
+                }
+            }
+            def svc = new Svc()
+            assert svc.ternary(true).get() == "T"
+            assert svc.ternary(false).get() == "F"
+        '''
+    }
+
+    @Test
+    void testAwaitInWhileLoop() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async countdown() {
+                    def results = []
+                    int n = 3
+                    while (n > 0) {
+                        def v = await CompletableFuture.supplyAsync { n }
+                        results << v
+                        n--
+                    }
+                    return results
+                }
+            }
+            assert new Svc().countdown().get() == [3, 2, 1]
+        '''
+    }
+
+    @Test
+    void testAwaitInForLoop() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async processItems() {
+                    def results = []
+                    for (int i = 0; i < 4; i++) {
+                        def v = await CompletableFuture.supplyAsync { i * 10 }
+                        results << v
+                    }
+                    return results
+                }
+            }
+            assert new Svc().processItems().get() == [0, 10, 20, 30]
+        '''
+    }
+
+    @Test
+    void testAwaitInForEachLoop() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async doubleAll() {
+                    def results = []
+                    for (x in [1, 2, 3]) {
+                        def v = await CompletableFuture.supplyAsync { x * 2 }
+                        results << v
+                    }
+                    return results
+                }
+            }
+            assert new Svc().doubleAll().get() == [2, 4, 6]
+        '''
+    }
+
+    @Test
+    void testAwaitInTryCatch() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async safe() {
+                    try {
+                        return await CompletableFuture.supplyAsync {
+                            throw new RuntimeException("boom")
+                        }
+                    } catch (RuntimeException e) {
+                        return "caught: ${e.message}"
+                    }
+                }
+            }
+            assert new Svc().safe().get() == "caught: boom"
+        '''
+    }
+
+    @Test
+    void testAwaitInTryFinally() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+            import java.util.concurrent.atomic.AtomicBoolean
+
+            class Svc {
+                AtomicBoolean cleaned = new AtomicBoolean(false)
+                async withCleanup() {
+                    try {
+                        return await CompletableFuture.supplyAsync { "result" }
+                    } finally {
+                        cleaned.set(true)
+                    }
+                }
+            }
+            def svc = new Svc()
+            assert svc.withCleanup().get() == "result"
+            assert svc.cleaned.get()
+        '''
+    }
+
+    @Test
+    void testAwaitInSwitchExpression() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async route(int code) {
+                    switch (code) {
+                        case 1: return await CompletableFuture.supplyAsync { "one" }
+                        case 2: return await CompletableFuture.supplyAsync { "two" }
+                        default: return await CompletableFuture.supplyAsync { "other" }
+                    }
+                }
+            }
+            def svc = new Svc()
+            assert svc.route(1).get() == "one"
+            assert svc.route(2).get() == "two"
+            assert svc.route(9).get() == "other"
+        '''
+    }
+
+    @Test
+    void testAwaitInStringInterpolation() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async greet() {
+                    def name = await CompletableFuture.supplyAsync { "World" }
+                    return "Hello, ${name}!"
+                }
+            }
+            assert new Svc().greet().get() == "Hello, World!"
+        '''
+    }
+
+    @Test
+    void testAwaitInCollectionLiteral() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async buildList() {
+                    def a = await CompletableFuture.supplyAsync { "x" }
+                    def b = await CompletableFuture.supplyAsync { "y" }
+                    return [a, b, "z"]
+                }
+            }
+            assert new Svc().buildList().get() == ["x", "y", "z"]
+        '''
+    }
+
+    @Test
+    void testAwaitInMapLiteral() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async buildMap() {
+                    def name = await CompletableFuture.supplyAsync { "Alice" }
+                    def age = await CompletableFuture.supplyAsync { 30 }
+                    return [name: name, age: age]
+                }
+            }
+            assert new Svc().buildMap().get() == [name: "Alice", age: 30]
+        '''
+    }
+
+    @Test
+    void testAwaitAsMethodArgument() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                String format(String s) { "[${s}]" }
+                async process() {
+                    def v = await CompletableFuture.supplyAsync { "data" }
+                    return format(v)
+                }
+            }
+            assert new Svc().process().get() == "[data]"
+        '''
+    }
+
+    @Test
+    void testNestedAwaitExpressions() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async nested() {
+                    // await of (await result used to create another future)
+                    def inner = await CompletableFuture.supplyAsync { 5 }
+                    def outer = await CompletableFuture.supplyAsync { inner * 10 }
+                    return outer
+                }
+            }
+            assert new Svc().nested().get() == 50
+        '''
+    }
+
+    @Test
+    void testAwaitInClosureInsideAsyncMethod() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async transform() {
+                    def items = [1, 2, 3]
+                    // Note: await inside collect closure — this works because
+                    // the closure executes synchronously within the async method
+                    def results = items.collect { n ->
+                        await CompletableFuture.supplyAsync { n * 100 }
+                    }
+                    return results
+                }
+            }
+            assert new Svc().transform().get() == [100, 200, 300]
+        '''
+    }
+
+    @Test
+    void testAwaitNullResult() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async fetchNull() {
+                    def v = await CompletableFuture.supplyAsync { null }
+                    return v
+                }
+            }
+            assert new Svc().fetchNull().get() == null
+        '''
+    }
+
+    @Test
+    void testAwaitBooleanResult() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async check() {
+                    boolean ok = await CompletableFuture.supplyAsync { true }
+                    return ok
+                }
+            }
+            assert new Svc().check().get() == true
+        '''
+    }
+
+    @Test
+    void testAwaitChainedMethodCallOnResult() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async process() {
+                    def s = await CompletableFuture.supplyAsync { "hello world" }
+                    return s.toUpperCase().split(" ").toList()
+                }
+            }
+            assert new Svc().process().get() == ["HELLO", "WORLD"]
+        '''
+    }
+
+    // =====================================================================
+    // 3. 'for await' — async iteration
+    // =====================================================================
+
+    @Test
+    void testForAwaitBasicIteration() {
+        assertScript '''
+            class Svc {
+                async collect() {
+                    def results = []
+                    for await (item in [10, 20, 30]) {
+                        results << item
+                    }
+                    return results
+                }
+            }
+            assert new Svc().collect().get() == [10, 20, 30]
+        '''
+    }
+
+    @Test
+    void testForAwaitBasicIteration_2() {
+        assertScript '''
+            class Svc {
+                async collect() {
+                    def results = []
+                    for await (item : [10, 20, 30]) {
+                        results << item
+                    }
+                    return results
+                }
+            }
+            assert new Svc().collect().get() == [10, 20, 30]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithTransformation() {
+        assertScript '''
+            class Svc {
+                async doubleAll() {
+                    def results = []
+                    for await (item in [1, 2, 3, 4]) {
+                        results << item * 2
+                    }
+                    return results
+                }
+            }
+            assert new Svc().doubleAll().get() == [2, 4, 6, 8]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithBreak() {
+        assertScript '''
+            class Svc {
+                async findFirst() {
+                    for await (item in [5, 10, 15, 20, 25]) {
+                        if (item > 12) return item
+                    }
+                    return -1
+                }
+            }
+            assert new Svc().findFirst().get() == 15
+        '''
+    }
+
+    @Test
+    void testForAwaitWithContinue() {
+        assertScript '''
+            class Svc {
+                async skipOdds() {
+                    def results = []
+                    for await (item in [1, 2, 3, 4, 5, 6]) {
+                        if (item % 2 != 0) continue
+                        results << item
+                    }
+                    return results
+                }
+            }
+            assert new Svc().skipOdds().get() == [2, 4, 6]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithTypedVariable() {
+        assertScript '''
+            class Svc {
+                async typed() {
+                    def results = []
+                    for await (String s in ["hello", "world"]) {
+                        results << s.toUpperCase()
+                    }
+                    return results
+                }
+            }
+            assert new Svc().typed().get() == ["HELLO", "WORLD"]
+        '''
+    }
+
+    @Test
+    void testForAwaitEmptySource() {
+        assertScript '''
+            class Svc {
+                async empty() {
+                    def results = []
+                    for await (item in []) {
+                        results << item
+                    }
+                    return results
+                }
+            }
+            assert new Svc().empty().get() == []
+        '''
+    }
+
+    @Test
+    void testForAwaitSingleElement() {
+        assertScript '''
+            class Svc {
+                async single() {
+                    def results = []
+                    for await (item in ["only"]) {
+                        results << item
+                    }
+                    return results
+                }
+            }
+            assert new Svc().single().get() == ["only"]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithIndex() {
+        assertScript '''
+            class Svc {
+                async indexed() {
+                    def results = []
+                    int idx = 0
+                    for await (item in ["a", "b", "c"]) {
+                        results << "${idx}:${item}"
+                        idx++
+                    }
+                    return results
+                }
+            }
+            assert new Svc().indexed().get() == ["0:a", "1:b", "2:c"]
+        '''
+    }
+
+    @Test
+    void testForAwaitNestedInRegularFor() {
+        assertScript '''
+            class Svc {
+                async nested() {
+                    def results = []
+                    for (prefix in ["A", "B"]) {
+                        for await (n in [1, 2]) {
+                            results << "${prefix}${n}"
+                        }
+                    }
+                    return results
+                }
+            }
+            assert new Svc().nested().get() == ["A1", "A2", "B1", "B2"]
+        '''
+    }
+
+    @Test
+    void testRegularForNestedInForAwait() {
+        assertScript '''
+            class Svc {
+                async nested() {
+                    def results = []
+                    for await (base in [10, 20]) {
+                        for (int i = 0; i < 3; i++) {
+                            results << base + i
+                        }
+                    }
+                    return results
+                }
+            }
+            assert new Svc().nested().get() == [10, 11, 12, 20, 21, 22]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithAwaitInsideBody() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async process() {
+                    def results = []
+                    for await (item in [1, 2, 3]) {
+                        def doubled = await CompletableFuture.supplyAsync { item * 2 }
+                        results << doubled
+                    }
+                    return results
+                }
+            }
+            assert new Svc().process().get() == [2, 4, 6]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithAsyncStream() {
+        assertScript '''
+            import groovy.concurrent.AsyncStream
+            import groovy.concurrent.Awaitable
+
+            class CountStream implements AsyncStream<Integer> {
+                private int current = 0
+                private final int max
+                CountStream(int max) { this.max = max }
+
+                Awaitable<Boolean> moveNext() {
+                    current++
+                    return Awaitable.of(current <= max)
+                }
+                Integer getCurrent() { current }
+            }
+
+            class Svc {
+                async sum() {
+                    int total = 0
+                    for await (n in new CountStream(5)) {
+                        total += n
+                    }
+                    return total
+                }
+            }
+            assert new Svc().sum().get() == 15
+        '''
+    }
+
+    @Test
+    void testMultipleForAwaitSequential() {
+        assertScript '''
+            class Svc {
+                async multi() {
+                    def r1 = []
+                    for await (x in [1, 2]) { r1 << x }
+                    def r2 = []
+                    for await (y in [3, 4]) { r2 << y }
+                    return [r1, r2]
+                }
+            }
+            assert new Svc().multi().get() == [[1, 2], [3, 4]]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithAnnotation() {
+        assertScript '''
+            import groovy.transform.Async
+
+            class Svc {
+                @Async
+                def collectItems() {
+                    def results = []
+                    for await (item in [100, 200, 300]) {
+                        results << item
+                    }
+                    return results
+                }
+            }
+            assert new Svc().collectItems().get() == [100, 200, 300]
+        '''
+    }
+
+    @Test
+    void testForAwaitInScript() {
+        assertScript '''
+            import groovy.transform.Async
+            import groovy.concurrent.Awaitable
+
+            @Async
+            def gatherItems() {
+                def results = []
+                for await (item in ["x", "y", "z"]) {
+                    results << item
+                }
+                return results
+            }
+
+            assert gatherItems().get() == ["x", "y", "z"]
+        '''
+    }
+
+    @Test
+    void testForAwaitWithTryCatch() {
+        assertScript '''
+            class Svc {
+                async safeIterate() {
+                    def results = []
+                    try {
+                        for await (item in [1, 2, 3]) {
+                            results << item
+                        }
+                    } catch (Exception e) {
+                        results << "error"
+                    }
+                    return results
+                }
+            }
+            assert new Svc().safeIterate().get() == [1, 2, 3]
+        '''
+    }
+
+    // =====================================================================
+    // 4. Combined patterns — real-world usage scenarios
+    // =====================================================================
+
+    @Test
+    void testAsyncServiceCallingAsyncService() {
+        assertScript '''
+            class UserService {
+                async findUser(long id) {
+                    return [id: id, name: "User${id}"]
+                }
+            }
+
+            class OrderService {
+                async findOrders(long userId) {
+                    return (1..3).collect { [orderId: it, userId: userId] }
+                }
+            }
+
+            class AggregatorService {
+                def userService = new UserService()
+                def orderService = new OrderService()
+
+                async getUserWithOrders(long id) {
+                    def user = await userService.findUser(id)
+                    def orders = await orderService.findOrders(id)
+                    return [user: user, orders: orders]
+                }
+            }
+
+            def agg = new AggregatorService()
+            def result = agg.getUserWithOrders(42).get()
+            assert result.user.name == "User42"
+            assert result.orders.size() == 3
+            assert result.orders[0].userId == 42
+        '''
+    }
+
+    @Test
+    void testAsyncWithExceptionPropagation() {
+        assertScript '''
+            import java.util.concurrent.ExecutionException
+
+            class Svc {
+                async fail() {
+                    throw new IllegalArgumentException("bad input")
+                }
+            }
+            try {
+                new Svc().fail().get()
+                assert false : "expected exception"
+            } catch (ExecutionException e) {
+                assert e.cause instanceof IllegalArgumentException
+                assert e.cause.message == "bad input"
+            }
+        '''
+    }
+
+    @Test
+    void testAwaitPropagatesExceptionToCallerAsync() {
+        assertScript '''
+            class Svc {
+                async failInner() {
+                    throw new IllegalStateException("inner-fail")
+                }
+                async caller() {
+                    try {
+                        await failInner()
+                        return "should not reach"
+                    } catch (IllegalStateException e) {
+                        return "recovered: ${e.message}"
+                    }
+                }
+            }
+            assert new Svc().caller().get() == "recovered: inner-fail"
+        '''
+    }
+
+    @Test
+    void testAsyncAwaitWithCollectionProcessing() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async processAll(List<Integer> items) {
+                    def results = []
+                    for (item in items) {
+                        def r = await CompletableFuture.supplyAsync { item * item }
+                        results << r
+                    }
+                    return results
+                }
+            }
+            assert new Svc().processAll([1, 2, 3, 4, 5]).get() == [1, 4, 9, 16, 25]
+        '''
+    }
+
+    @Test
+    void testAsyncAwaitProducerConsumer() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Producer {
+                async produce(int n) { n * 10 }
+            }
+            class Consumer {
+                def producer = new Producer()
+                async consume() {
+                    def sum = 0
+                    for (i in 1..5) {
+                        sum += await producer.produce(i)
+                    }
+                    return sum
+                }
+            }
+            // 10 + 20 + 30 + 40 + 50 = 150
+            assert new Consumer().consume().get() == 150
+        '''
+    }
+
+    @Test
+    void testAsyncAwaitParallelThenGather() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async step(int n) { n * 2 }
+
+                async parallelGather() {
+                    // Launch multiple async tasks
+                    def f1 = step(1)
+                    def f2 = step(2)
+                    def f3 = step(3)
+                    // Await all results
+                    return [await f1, await f2, await f3]
+                }
+            }
+            assert new Svc().parallelGather().get() == [2, 4, 6]
+        '''
+    }
+
+    @Test
+    void testAsyncAwaitChainedTransformation() {
+        assertScript '''
+            class Pipeline {
+                async step1(String input) { input.toUpperCase() }
+                async step2(String input) { input.reverse() }
+                async step3(String input) { "[${input}]" }
+
+                async run(String input) {
+                    def r1 = await step1(input)
+                    def r2 = await step2(r1)
+                    return await step3(r2)
+                }
+            }
+            assert new Pipeline().run("abc").get() == "[CBA]"
+        '''
+    }
+
+    @Test
+    void testAsyncMixedAnnotationAndKeyword() {
+        assertScript '''
+            import groovy.transform.Async
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                @Async
+                def fromAnnotation() { "annotated" }
+
+                async fromKeyword() { "keyword" }
+
+                async combine() {
+                    def a = await fromAnnotation()
+                    def b = await fromKeyword()
+                    return "${a}+${b}"
+                }
+            }
+            assert new Svc().combine().get() == "annotated+keyword"
+        '''
+    }
+
+    @Test
+    void testAsyncWithConditionalAwait() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async fetchIf(boolean shouldFetch) {
+                    if (shouldFetch) {
+                        return await CompletableFuture.supplyAsync { "fetched" }
+                    }
+                    return "cached"
+                }
+            }
+            def svc = new Svc()
+            assert svc.fetchIf(true).get() == "fetched"
+            assert svc.fetchIf(false).get() == "cached"
+        '''
+    }
+
+    @Test
+    void testAsyncRecursion() {
+        assertScript '''
+            class Svc {
+                async factorial(int n) {
+                    if (n <= 1) return 1
+                    def sub = await factorial(n - 1)
+                    return n * sub
+                }
+            }
+            assert new Svc().factorial(5).get() == 120
+        '''
+    }
+
+    @Test
+    void testAsyncWithRetry() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+            import java.util.concurrent.atomic.AtomicInteger
+
+            class Svc {
+                AtomicInteger attempts = new AtomicInteger(0)
+
+                async unreliable() {
+                    if (attempts.incrementAndGet() < 3) {
+                        throw new RuntimeException("not yet")
+                    }
+                    return "success"
+                }
+
+                async withRetry(int maxRetries) {
+                    for (int i = 0; i < maxRetries; i++) {
+                        try {
+                            return await unreliable()
+                        } catch (RuntimeException e) {
+                            if (i == maxRetries - 1) throw e
+                        }
+                    }
+                }
+            }
+            assert new Svc().withRetry(5).get() == "success"
+        '''
+    }
+
+    @Test
+    void testAsyncWithTimeout() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.TimeUnit
+            import java.util.concurrent.TimeoutException
+
+            class Svc {
+                async slow() {
+                    Thread.sleep(2000)
+                    return "done"
+                }
+            }
+            def a = new Svc().slow()
+            try {
+                a.get(50, TimeUnit.MILLISECONDS)
+                assert false : "expected timeout"
+            } catch (TimeoutException e) {
+                // expected — cancel to avoid leaving the task running
+                a.cancel()
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncWithAwaitableOf() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                async useOf() {
+                    def a = await Awaitable.of(42)
+                    return a + 8
+                }
+            }
+            assert new Svc().useOf().get() == 50
+        '''
+    }
+
+    @Test
+    void testAsyncWithAwaitableThen() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                async inner() { 10 }
+                async chained() {
+                    def a = inner()  // returns Awaitable<Integer>
+                    def b = a.then { it * 3 }
+                    return await b
+                }
+            }
+            assert new Svc().chained().get() == 30
+        '''
+    }
+
+    @Test
+    void testAsyncWithAwaitableThenCompose() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                async step1() { 5 }
+                async step2(int v) { v * 4 }
+                async composed() {
+                    def a = step1()
+                    def b = a.thenCompose { v -> step2(v) }
+                    return await b
+                }
+            }
+            assert new Svc().composed().get() == 20
+        '''
+    }
+
+    @Test
+    void testAsyncWithAwaitableExceptionally() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class Svc {
+                async failing() {
+                    throw new RuntimeException("oops")
+                }
+                async recovered() {
+                    def a = failing().exceptionally { "fallback" }
+                    return await a
+                }
+            }
+            assert new Svc().recovered().get() == "fallback"
+        '''
+    }
+
+    @Test
+    void testConcurrentAsyncMethodsWithSyntax() {
+        assertScript '''
+            import java.util.concurrent.ConcurrentHashMap
+
+            class Svc {
+                def results = new ConcurrentHashMap()
+                async store(String key, String value) {
+                    results.put(key, value)
+                    return value
+                }
+            }
+            def svc = new Svc()
+            def futures = (1..10).collect { i -> svc.store("k${i}", "v${i}") }
+            futures.each { it.get() }
+            assert svc.results.size() == 10
+            assert svc.results["k5"] == "v5"
+        '''
+    }
+
+    @Test
+    void testScriptLevelAsyncAwait() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            async fetchData(String key) {
+                return "data-for-${key}"
+            }
+
+            async main() {
+                def r1 = await fetchData("alpha")
+                def r2 = await fetchData("beta")
+                return [r1, r2]
+            }
+
+            def result = main().get()
+            assert result[0] == "data-for-alpha"
+            assert result[1] == "data-for-beta"
+        '''
+    }
+
+    @Test
+    void testScriptLevelForAwait() {
+        assertScript '''
+            async items() {
+                def results = []
+                for await (x in [1, 2, 3]) {
+                    results << x * 10
+                }
+                return results
+            }
+            assert items().get() == [10, 20, 30]
+        '''
+    }
+
+    @Test
+    void testAsyncWithCustomExecutor() {
+        assertScript '''
+            import java.util.concurrent.Executors
+
+            class Svc {
+                static executor = Executors.newFixedThreadPool(2)
+                async threadInfo() {
+                    return Thread.currentThread().name
+                }
+            }
+
+            groovy.concurrent.Awaitable.setExecutor(Svc.executor)
+            try {
+                def name = new Svc().threadInfo().get()
+                assert name.contains("pool")
+            } finally {
+                groovy.concurrent.Awaitable.setExecutor(null)
+                Svc.executor.shutdown()
+            }
+        '''
+    }
+
+    @Test
+    void testForAwaitCombinedWithAwaitInSameMethod() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async complexFlow() {
+                    def prefix = await CompletableFuture.supplyAsync { "item" }
+                    def results = []
+                    for await (n in [1, 2, 3]) {
+                        results << "${prefix}-${n}"
+                    }
+                    def suffix = await CompletableFuture.supplyAsync { "-done" }
+                    return results.join(",") + suffix
+                }
+            }
+            assert new Svc().complexFlow().get() == "item-1,item-2,item-3-done"
+        '''
+    }
+
+    @Test
+    void testAsyncMethodWithGStringReturn() {
+        assertScript '''
+            class Svc {
+                async template(String name, int age) {
+                    return "Name: ${name}, Age: ${age}"
+                }
+            }
+            assert new Svc().template("Alice", 25).get() == "Name: Alice, Age: 25"
+        '''
+    }
+
+    @Test
+    void testAsyncMethodReturningCollection() {
+        assertScript '''
+            class Svc {
+                async getList() { [1, 2, 3] }
+                async getMap() { [a: 1, b: 2] }
+                async getSet() { [1, 2, 3] as Set }
+
+                async all() {
+                    def list = await getList()
+                    def map = await getMap()
+                    def set = await getSet()
+                    return [list: list, map: map, setSize: set.size()]
+                }
+            }
+            def r = new Svc().all().get()
+            assert r.list == [1, 2, 3]
+            assert r.map == [a: 1, b: 2]
+            assert r.setSize == 3
+        '''
+    }
+
+    @Test
+    void testAsyncWithPropertyAccess() {
+        assertScript '''
+            class User {
+                String name
+                int age
+            }
+
+            class Svc {
+                async getUser() {
+                    new User(name: "Bob", age: 40)
+                }
+                async getUserName() {
+                    def user = await getUser()
+                    return user.name
+                }
+            }
+            assert new Svc().getUserName().get() == "Bob"
+        '''
+    }
+
+    @Test
+    void testAsyncWithSpreadOperator() {
+        assertScript '''
+            class Svc {
+                async getItems() { [[v: 1], [v: 2], [v: 3]] }
+                async getValues() {
+                    def items = await getItems()
+                    return items*.v
+                }
+            }
+            assert new Svc().getValues().get() == [1, 2, 3]
+        '''
+    }
+
+    @Test
+    void testAsyncWithElvisOperator() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Svc {
+                async fetchOrDefault() {
+                    def v = await CompletableFuture.supplyAsync { null }
+                    return v ?: "default"
+                }
+            }
+            assert new Svc().fetchOrDefault().get() == "default"
+        '''
+    }
+
+    @Test
+    void testAsyncWithSafeNavigation() {
+        assertScript '''
+            class Svc {
+                async nullResult() { null }
+                async safe() {
+                    def r = await nullResult()
+                    return r?.toString() ?: "was-null"
+                }
+            }
+            assert new Svc().safe().get() == "was-null"
+        '''
+    }
+
+    // =====================================================================
+    // 5. Backward compatibility — async/await as identifiers
+    // =====================================================================
+
+    @Test
+    void testAsyncAsLocalVariable() {
+        assertScript '''
+            def async = "value"
+            assert async == "value"
+        '''
+    }
+
+    @Test
+    void testAwaitAsLocalVariable() {
+        assertScript '''
+            def await = 123
+            assert await == 123
+        '''
+    }
+
+    @Test
+    void testAsyncAsMethodNameInClass() {
+        assertScript '''
+            class Legacy {
+                def async() { "legacy-async" }
+                def await() { "legacy-await" }
+            }
+            def obj = new Legacy()
+            assert obj.async() == "legacy-async"
+            assert obj.await() == "legacy-await"
+        '''
+    }
+
+    @Test
+    void testAsyncAsFieldName() {
+        assertScript '''
+            class Config {
+                boolean async = true
+                String await = "ready"
+            }
+            def c = new Config()
+            assert c.async == true
+            assert c.await == "ready"
+        '''
+    }
+
+    @Test
+    void testAsyncAsMapKey() {
+        assertScript '''
+            def map = [async: true, await: false]
+            assert map.async == true
+            assert map.await == false
+        '''
+    }
+
+    @Test
+    void testAsyncAsParameterName() {
+        assertScript '''
+            def process(boolean async) { async ? "yes" : "no" }
+            assert process(true) == "yes"
+        '''
+    }
+
+    @Test
+    void testAsyncAndAwaitAsLabels() {
+        assertScript '''
+            def result = null
+            async:
+            for (i in [1, 2, 3]) {
+                await:
+                for (j in [10, 20]) {
+                    if (j == 20) {
+                        result = i * j
+                        break async
+                    }
+                }
+            }
+            assert result == 20
+        '''
+    }
+
+    // =====================================================================
+    // 6. Error cases — syntax and semantic rejections
+    // =====================================================================
+
+    @Test
+    void testAsyncOnAbstractMethodFails() {
+        shouldFail '''
+            abstract class Svc {
+                @groovy.transform.Async
+                abstract def compute()
+            }
+        '''
+    }
+
+    @Test
+    void testAsyncOnAwaitableReturnTypeFails() {
+        shouldFail '''
+            import groovy.concurrent.Awaitable
+            class Svc {
+                @groovy.transform.Async
+                Awaitable<String> compute() { Awaitable.of("x") }
+            }
+        '''
+    }
+
+    @Test
+    void testForAwaitWithTraditionalForSyntaxFails() {
+        shouldFail '''
+            class Svc {
+                @groovy.transform.Async
+                def work() {
+                    for await (int i = 0; i < 10; i++) {
+                        println i
+                    }
+                }
+            }
+        '''
+    }
+
+    // =====================================================================
+    // 7. 'yield return' — async generator (C#-style async streams)
+    // =====================================================================
+
+    @Test
+    void testYieldReturnBasic() {
+        assertScript '''
+            import groovy.concurrent.AsyncStream
+
+            class Gen {
+                async numbers() {
+                    yield return 1
+                    yield return 2
+                    yield return 3
+                }
+            }
+            def stream = new Gen().numbers()
+            assert stream instanceof AsyncStream
+            def results = []
+            for await (n in stream) {
+                results << n
+            }
+            assert results == [1, 2, 3]
+        '''
+    }
+
+    @Test
+    void testYieldReturnInForLoop() {
+        assertScript '''
+            class Gen {
+                async range(int start, int end) {
+                    for (int i = start; i <= end; i++) {
+                        yield return i
+                    }
+                }
+            }
+            def results = []
+            for await (n in new Gen().range(1, 5)) {
+                results << n
+            }
+            assert results == [1, 2, 3, 4, 5]
+        '''
+    }
+
+    @Test
+    void testYieldReturnInWhileLoop() {
+        assertScript '''
+            class Gen {
+                async countdown(int n) {
+                    while (n > 0) {
+                        yield return n
+                        n--
+                    }
+                }
+            }
+            def results = []
+            for await (v in new Gen().countdown(4)) {
+                results << v
+            }
+            assert results == [4, 3, 2, 1]
+        '''
+    }
+
+    @Test
+    void testYieldReturnInForEach() {
+        assertScript '''
+            class Gen {
+                async transform(List items) {
+                    for (item in items) {
+                        yield return item.toString().toUpperCase()
+                    }
+                }
+            }
+            def results = []
+            for await (s in new Gen().transform(["hello", "world"])) {
+                results << s
+            }
+            assert results == ["HELLO", "WORLD"]
+        '''
+    }
+
+    @Test
+    void testYieldReturnStrings() {
+        assertScript '''
+            class Gen {
+                async greetings() {
+                    yield return "hello"
+                    yield return "world"
+                    yield return "!"
+                }
+            }
+            def results = []
+            for await (s in new Gen().greetings()) {
+                results << s
+            }
+            assert results == ["hello", "world", "!"]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithGString() {
+        assertScript '''
+            class Gen {
+                async labeled(List items) {
+                    int idx = 0
+                    for (item in items) {
+                        yield return "${idx}: ${item}"
+                        idx++
+                    }
+                }
+            }
+            def results = []
+            for await (s in new Gen().labeled(["a", "b", "c"])) {
+                results << s
+            }
+            assert results == ["0: a", "1: b", "2: c"]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithFilter() {
+        assertScript '''
+            class Gen {
+                async evens(int max) {
+                    for (int i = 1; i <= max; i++) {
+                        if (i % 2 == 0) {
+                            yield return i
+                        }
+                    }
+                }
+            }
+            def results = []
+            for await (n in new Gen().evens(10)) {
+                results << n
+            }
+            assert results == [2, 4, 6, 8, 10]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithParameters() {
+        assertScript '''
+            class Gen {
+                async multiply(List<Integer> items, int factor) {
+                    for (item in items) {
+                        yield return item * factor
+                    }
+                }
+            }
+            def results = []
+            for await (n in new Gen().multiply([1, 2, 3], 10)) {
+                results << n
+            }
+            assert results == [10, 20, 30]
+        '''
+    }
+
+    @Test
+    void testYieldReturnEmpty() {
+        assertScript '''
+            class Gen {
+                async empty() {
+                    // A generator must have at least one yield return
+                    // to be detected as a generator. An empty body with
+                    // no yield returns becomes a regular async method.
+                    if (false) yield return "unreachable"
+                }
+            }
+            def results = []
+            for await (item in new Gen().empty()) {
+                results << item
+            }
+            assert results == []
+        '''
+    }
+
+    @Test
+    void testYieldReturnSingleElement() {
+        assertScript '''
+            class Gen {
+                async single() {
+                    yield return "only"
+                }
+            }
+            def results = []
+            for await (item in new Gen().single()) {
+                results << item
+            }
+            assert results == ["only"]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithConditionalLogic() {
+        assertScript '''
+            class Gen {
+                async fizzBuzz(int n) {
+                    for (int i = 1; i <= n; i++) {
+                        if (i % 15 == 0) yield return "FizzBuzz"
+                        else if (i % 3 == 0) yield return "Fizz"
+                        else if (i % 5 == 0) yield return "Buzz"
+                        else yield return i.toString()
+                    }
+                }
+            }
+            def results = []
+            for await (s in new Gen().fizzBuzz(15)) {
+                results << s
+            }
+            assert results[0] == "1"
+            assert results[2] == "Fizz"
+            assert results[4] == "Buzz"
+            assert results[14] == "FizzBuzz"
+            assert results.size() == 15
+        '''
+    }
+
+    @Test
+    void testYieldReturnConsumerBreaksEarly() {
+        assertScript '''
+            class Gen {
+                async infinite() {
+                    int i = 0
+                    while (true) {
+                        yield return i++
+                    }
+                }
+            }
+            // Consumer only takes first 5
+            def results = []
+            def stream = new Gen().infinite()
+            for await (n in stream) {
+                results << n
+                if (results.size() >= 5) break
+            }
+            assert results == [0, 1, 2, 3, 4]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithAnnotation() {
+        assertScript '''
+            import groovy.transform.Async
+            import groovy.concurrent.AsyncStream
+
+            class Gen {
+                @Async
+                def items() {
+                    yield return "x"
+                    yield return "y"
+                }
+            }
+            def stream = new Gen().items()
+            assert stream instanceof AsyncStream
+            def results = []
+            for await (s in stream) {
+                results << s
+            }
+            assert results == ["x", "y"]
+        '''
+    }
+
+    @Test
+    void testYieldReturnInScript() {
+        assertScript '''
+            import groovy.concurrent.AsyncStream
+
+            async fibonacci(int count) {
+                int a = 0, b = 1
+                for (int i = 0; i < count; i++) {
+                    yield return a
+                    int temp = a + b
+                    a = b
+                    b = temp
+                }
+            }
+
+            def results = []
+            for await (n in fibonacci(8)) {
+                results << n
+            }
+            assert results == [0, 1, 1, 2, 3, 5, 8, 13]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithAwaitInsideBody() {
+        assertScript '''
+            import java.util.concurrent.CompletableFuture
+
+            class Gen {
+                async asyncItems() {
+                    for (i in 1..3) {
+                        def v = await CompletableFuture.supplyAsync { i * 100 }
+                        yield return v
+                    }
+                }
+            }
+            def results = []
+            for await (n in new Gen().asyncItems()) {
+                results << n
+            }
+            assert results == [100, 200, 300]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithTryCatch() {
+        assertScript '''
+            class Gen {
+                async safeItems() {
+                    for (i in 1..5) {
+                        try {
+                            if (i == 3) throw new RuntimeException("skip")
+                            yield return i
+                        } catch (RuntimeException e) {
+                            yield return -1
+                        }
+                    }
+                }
+            }
+            def results = []
+            for await (n in new Gen().safeItems()) {
+                results << n
+            }
+            assert results == [1, 2, -1, 4, 5]
+        '''
+    }
+
+    @Test
+    void testYieldReturnExceptionPropagation() {
+        assertScript '''
+            class Gen {
+                async failing() {
+                    yield return 1
+                    yield return 2
+                    throw new IllegalStateException("generator failed")
+                }
+            }
+            def results = []
+            try {
+                for await (n in new Gen().failing()) {
+                    results << n
+                }
+                assert false : "expected exception"
+            } catch (IllegalStateException e) {
+                assert e.message == "generator failed"
+            }
+            assert results == [1, 2]
+        '''
+    }
+
+    @Test
+    void testYieldReturnNullValues() {
+        assertScript '''
+            class Gen {
+                async withNulls() {
+                    yield return "a"
+                    yield return null
+                    yield return "b"
+                }
+            }
+            def results = []
+            for await (item in new Gen().withNulls()) {
+                results << item
+            }
+            assert results == ["a", null, "b"]
+        '''
+    }
+
+    @Test
+    void testYieldReturnMixedTypes() {
+        assertScript '''
+            class Gen {
+                async mixed() {
+                    yield return 42
+                    yield return "hello"
+                    yield return [1, 2, 3]
+                    yield return true
+                }
+            }
+            def results = []
+            for await (item in new Gen().mixed()) {
+                results << item
+            }
+            assert results[0] == 42
+            assert results[1] == "hello"
+            assert results[2] == [1, 2, 3]
+            assert results[3] == true
+        '''
+    }
+
+    @Test
+    void testYieldReturnForAwaitAccumulation() {
+        assertScript '''
+            class Gen {
+                async items() {
+                    for (i in 1..5) {
+                        yield return i
+                    }
+                }
+            }
+            // Sum using for-await
+            async sumStream() {
+                int total = 0
+                for await (n in new Gen().items()) {
+                    total += n
+                }
+                return total
+            }
+            assert sumStream().get() == 15
+        '''
+    }
+
+    @Test
+    void testYieldReturnChainedGenerators() {
+        assertScript '''
+            class Gen {
+                async source() {
+                    yield return 1
+                    yield return 2
+                    yield return 3
+                }
+
+                async doubled() {
+                    for await (n in source()) {
+                        yield return n * 2
+                    }
+                }
+            }
+            def results = []
+            for await (n in new Gen().doubled()) {
+                results << n
+            }
+            assert results == [2, 4, 6]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithForAwaitTransformPipeline() {
+        assertScript '''
+            class Pipeline {
+                async numbers(int n) {
+                    for (int i = 1; i <= n; i++) {
+                        yield return i
+                    }
+                }
+
+                async squared(source) {
+                    for await (n in source) {
+                        yield return n * n
+                    }
+                }
+
+                async asStrings(source) {
+                    for await (n in source) {
+                        yield return "val=${n}"
+                    }
+                }
+            }
+            def p = new Pipeline()
+            def results = []
+            for await (s in p.asStrings(p.squared(p.numbers(4)))) {
+                results << s
+            }
+            assert results == ["val=1", "val=4", "val=9", "val=16"]
+        '''
+    }
+
+    @Test
+    void testYieldReturnStaticMethod() {
+        assertScript '''
+            class Gen {
+                async static staticRange(int n) {
+                    for (int i = 0; i < n; i++) {
+                        yield return i
+                    }
+                }
+            }
+            def results = []
+            for await (n in Gen.staticRange(4)) {
+                results << n
+            }
+            assert results == [0, 1, 2, 3]
+        '''
+    }
+
+    @Test
+    void testYieldReturnWithMapEntries() {
+        assertScript '''
+            class Gen {
+                async entries(Map map) {
+                    for (entry in map.entrySet()) {
+                        yield return [key: entry.key, value: entry.value]
+                    }
+                }
+            }
+            def results = []
+            for await (e in new Gen().entries([a: 1, b: 2, c: 3])) {
+                results << e
+            }
+            assert results.size() == 3
+            assert results.find { it.key == "b" }.value == 2
+        '''
+    }
+
+    @Test
+    void testYieldReturnLargeSequence() {
+        assertScript '''
+            class Gen {
+                async sequence(int count) {
+                    for (int i = 0; i < count; i++) {
+                        yield return i
+                    }
+                }
+            }
+            int sum = 0
+            int count = 0
+            for await (n in new Gen().sequence(1000)) {
+                sum += n
+                count++
+            }
+            assert count == 1000
+            assert sum == (999 * 1000) / 2
+        '''
+    }
+
+    @Test
+    void testYieldReturnOutsideAsyncMethodFails() {
+        assertScript '''
+            // yield return outside async method should fail at runtime
+            try {
+                org.apache.groovy.runtime.async.AsyncSupport.yieldReturn("oops")
+                assert false : "expected exception"
+            } catch (IllegalStateException e) {
+                assert e.message.contains("yield return")
+            }
+        '''
+    }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncBestPracticesTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncBestPracticesTest.groovy
new file mode 100644
index 0000000000..471981300c
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncBestPracticesTest.groovy
@@ -0,0 +1,1470 @@
+/*
+ *  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.Awaitable
+import groovy.test.GroovyAssert
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+
+class AsyncBestPracticesTest {
+
+    @AfterEach
+    void resetExecutor() {
+        Awaitable.setExecutor(null)
+    }
+
+    @Test
+    void testAsyncAllTheWay() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            // Async should be viral: callers should await, not block
+            async fetchData() { "data" }
+            async processData() {
+                def data = await fetchData()
+                "processed: $data"
+            }
+            async handleRequest() {
+                def result = await processData()
+                "response: $result"
+            }
+
+            assert await(handleRequest()) == "response: processed: data"
+        '''
+    }
+
+    @Test
+    void testAvoidAsyncVoidPattern() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.atomic.AtomicBoolean
+
+            // Correct: async method returns Awaitable, so caller can await and catch errors
+            class OrderService {
+                AtomicBoolean saved = new AtomicBoolean(false)
+                async processOrder(String orderId) {
+                    saved.set(true)
+                    "order-$orderId"
+                }
+            }
+
+            def svc = new OrderService()
+            def result = await svc.processOrder("123")
+            assert result == "order-123"
+            assert svc.saved.get()
+        '''
+    }
+
+    @Test
+    void testNeverBlockOnAsyncCode() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            // CORRECT: use await to suspend without blocking
+            async fetchUser(int id) { "user-$id" }
+
+            async correctWay() {
+                def user = await fetchUser(1)
+                assert user == "user-1"
+                user
+            }
+
+            assert await(correctWay()) == "user-1"
+        '''
+    }
+
+    @Test
+    void testAlwaysAwaitTasks() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.atomic.AtomicInteger
+
+            // Always await — never fire-and-forget
+            class BatchProcessor {
+                AtomicInteger count = new AtomicInteger(0)
+                async processSingle(String item) {
+                    count.incrementAndGet()
+                    "done-$item"
+                }
+
+                async processAll(List<String> items) {
+                    // Correct: await each or use Awaitable.all
+                    def tasks = items.collect { processSingle(it) }
+                    await Awaitable.all(*tasks)
+                }
+            }
+
+            def proc = new BatchProcessor()
+            await proc.processAll(["a", "b", "c"])
+            assert proc.count.get() == 3
+        '''
+    }
+
+    @Test
+    void testSequentialExecution() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.CopyOnWriteArrayList
+
+            class SequentialTest {
+                def steps = new CopyOnWriteArrayList<String>()
+
+                async step1() { steps.add("validate"); "valid" }
+                async step2(input) { steps.add("process"); "processed-$input" }
+                async step3(input) { steps.add("confirm"); "confirmed-$input" }
+
+                async processOrder() {
+                    def v = await step1()
+                    def p = await step2(v)
+                    await step3(p)
+                }
+            }
+
+            def t = new SequentialTest()
+            def result = await t.processOrder()
+            assert result == "confirmed-processed-valid"
+            assert t.steps == ["validate", "process", "confirm"]
+        '''
+    }
+
+    @Test
+    void testParallelExecution() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            // Parallel: independent operations started together
+            async fetchUser(int id)     { "user-$id" }
+            async fetchOrders(int id)   { ["order1", "order2"] }
+            async fetchStats(int id)    { [views: 100] }
+
+            async getDashboard(int userId) {
+                def userTask    = fetchUser(userId)
+                def ordersTask  = fetchOrders(userId)
+                def statsTask   = fetchStats(userId)
+
+                def results = await Awaitable.all(userTask, ordersTask, statsTask)
+                [user: results[0], orders: results[1], stats: results[2]]
+            }
+
+            def dashboard = await getDashboard(42)
+            assert dashboard.user == "user-42"
+            assert dashboard.orders == ["order1", "order2"]
+            assert dashboard.stats == [views: 100]
+        '''
+    }
+
+    @Test
+    void testCancellationPattern() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.CancellationException
+
+            // Long-running task that can be cancelled
+            async longRunning() {
+                await Awaitable.delay(10_000)
+                "done"
+            }
+
+            def task = longRunning()
+            // Cancel before it completes
+            task.cancel()
+
+            assert task.isCancelled()
+            assert task.isCompletedExceptionally()
+
+            try {
+                await(task)
+                assert false
+            } catch (CancellationException e) {
+                // expected — properly propagated
+            }
+        '''
+    }
+
+    @Test
+    void testExceptionHandlingSingleTask() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async riskyOperation() {
+                throw new IOException("db error")
+            }
+
+            async handleSafely() {
+                try {
+                    await riskyOperation()
+                } catch (IOException e) {
+                    "recovered: ${e.message}"
+                }
+            }
+
+            assert await(handleSafely()) == "recovered: db error"
+        '''
+    }
+
+    @Test
+    void testExceptionHandlingWithAllSettled() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            // allSettled captures all exceptions without short-circuiting
+            async ok()   { "success" }
+            async fail1() { throw new IOException("io") }
+            async fail2() { throw new ArithmeticException("math") }
+
+            def results = await Awaitable.allSettled(ok(), fail1(), fail2())
+
+            def errors = results.findAll { it.isFailure() }
+            assert errors.size() == 2
+            assert errors[0].error instanceof IOException
+            assert errors[1].error instanceof ArithmeticException
+
+            def successes = results.findAll { it.isSuccess() }
+            assert successes.size() == 1
+            assert successes[0].value == "success"
+        '''
+    }
+
+    @Test
+    void testCachedAsyncResult() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.ConcurrentHashMap
+
+            // ValueTask-like pattern: return cached results synchronously
+            class UserCache {
+                def cache = new ConcurrentHashMap<Integer, String>()
+                int fetchCount = 0
+
+                def getUser(int id) {
+                    def cached = cache.get(id)
+                    if (cached != null) {
+                        return Awaitable.of(cached)
+                    }
+                    return fetchAndCache(id)
+                }
+
+                async fetchAndCache(int id) {
+                    fetchCount++
+                    def user = "user-$id"
+                    cache.put(id, user)
+                    user
+                }
+            }
+
+            def svc = new UserCache()
+            assert await(svc.getUser(1)) == "user-1"
+            assert await(svc.getUser(1)) == "user-1"  // from cache
+            assert svc.fetchCount == 1
+        '''
+    }
+
+    @Test
+    void testAsyncStreamWithPagination() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.AsyncStream
+
+            // Async stream pattern: process large datasets without loading all into memory
+            async getPage(int pageNum) {
+                if (pageNum >= 3) return []
+                (1..5).collect { "item-${pageNum * 5 + it}" }
+            }
+
+            async processAllItems() {
+                def allItems = []
+                int page = 0
+                while (true) {
+                    def items = await getPage(page)
+                    if (items.isEmpty()) break
+                    allItems.addAll(items)
+                    page++
+                }
+                allItems
+            }
+
+            def items = await processAllItems()
+            assert items.size() == 15
+            assert items[0] == "item-1"
+            assert items[-1] == "item-15"
+        '''
+    }
+
+    @Test
+    void testAsyncInitializationPattern() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.ConcurrentHashMap
+
+            class AsyncService {
+                private final ConcurrentHashMap<String, String> state = new ConcurrentHashMap<>()
+
+                async getConnection() {
+                    def c = state.get("conn")
+                    if (c != null) return c
+                    def newConn = await initConnection()
+                    state.putIfAbsent("conn", newConn)
+                    state.get("conn")
+                }
+
+                async initConnection() {
+                    await Awaitable.delay(10)
+                    "db-conn-open"
+                }
+            }
+
+            def svc = new AsyncService()
+            def results = await Awaitable.all(
+                svc.getConnection(),
+                svc.getConnection()
+            )
+            assert results.every { it == "db-conn-open" }
+        '''
+    }
+
+    @Test
+    void testTimeoutWithAny() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            // Race operation against timeout
+            async slowOp() {
+                await Awaitable.delay(10_000)
+                "result"
+            }
+
+            def task = slowOp()
+            def timeout = Awaitable.delay(50)
+
+            def winner = await Awaitable.any(task, timeout)
+            // Timeout wins (null from delay)
+            assert winner == null
+            task.cancel()
+        '''
+    }
+
+    @Test
+    void testRetryPattern() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.atomic.AtomicInteger
+
+            class RetryService {
+                def attempts = new AtomicInteger(0)
+
+                async unreliable() {
+                    if (attempts.incrementAndGet() < 3) {
+                        throw new IOException("transient error")
+                    }
+                    "success"
+                }
+
+                async withRetry(int maxRetries) {
+                    for (int i = 0; i <= maxRetries; i++) {
+                        try {
+                            return await unreliable()
+                        } catch (IOException e) {
+                            if (i == maxRetries) throw e
+                            await Awaitable.delay(10)
+                        }
+                    }
+                }
+            }
+
+            def svc = new RetryService()
+            assert await(svc.withRetry(3)) == "success"
+            assert svc.attempts.get() == 3
+        '''
+    }
+
+    // =========================================================================
+    // 1. Asynchrony is viral — propagate await through the call chain
+    // =========================================================================
+
+    /**
+     * Demonstrates that once a method is async, callers should also use
+     * {@code await} rather than blocking with {@code .get()}.
+     */
+    @Test
+    void testAsynchronyIsViral_AwaitPropagation() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class DependencyService {
+                async callDependency() { return 42 }
+            }
+
+            class ConsumerService {
+                private final DependencyService dep = new DependencyService()
+
+                // GOOD: async all the way through
+                async doSomething() {
+                    def result = await dep.callDependency()
+                    return result + 1
+                }
+            }
+
+            def service = new ConsumerService()
+            assert await(service.doSomething()) == 43
+        '''
+    }
+
+    @Test
+    void testAsynchronyIsViral_MultipleLayersOfAwait() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class DataService {
+                async fetchFromDb() {
+                    await Awaitable.delay(10)
+                    return "db-data"
+                }
+            }
+
+            class BusinessLogic {
+                private final DataService dataService = new DataService()
+                async process() {
+                    def raw = await dataService.fetchFromDb()
+                    return raw.toUpperCase()
+                }
+            }
+
+            class Controller {
+                private final BusinessLogic logic = new BusinessLogic()
+                async handleRequest() {
+                    def result = await logic.process()
+                    return "Response: ${result}"
+                }
+            }
+
+            def controller = new Controller()
+            assert await(controller.handleRequest()) == "Response: DB-DATA"
+        '''
+    }
+
+    // =========================================================================
+    // 2. Prefer Awaitable.of() for pre-computed data
+    // =========================================================================
+
+    /**
+     * When the result is already known, use {@code Awaitable.of()} to avoid
+     * spawning a thread. This is the equivalent of C#'s {@code Task.FromResult}.
+     */
+    @Test
+    void testPreComputedResult_AwaitableOf() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class MyLibrary {
+                static Awaitable<Integer> addAsync(int a, int b) {
+                    return Awaitable.of(a + b)
+                }
+            }
+
+            // No thread spawned — result is immediately available
+            assert await(MyLibrary.addAsync(3, 4)) == 7
+        '''
+    }
+
+    @Test
+    void testPreComputedResult_AwaitableOfVsAsyncClosure() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            // GOOD: Awaitable.of() — no thread allocation
+            def cachedValue = Awaitable.of("cached")
+            assert await(cachedValue) == "cached"
+            assert cachedValue.isDone()
+
+            // Also fine for truly async work: async closure
+            def computed = async { Thread.sleep(10); "computed" }
+            assert await(computed()) == "computed"
+        '''
+    }
+
+    @Test
+    void testPreComputedResult_FailedAwaitable() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def failed = Awaitable.failed(new IllegalArgumentException("bad input"))
+            assert failed.isDone()
+            assert failed.isCompletedExceptionally()
+
+            try {
+                await(failed)
+                assert false : "should have thrown"
+            } catch (IllegalArgumentException e) {
+                assert e.message == "bad input"
+            }
+        '''
+    }
+
+    // =========================================================================
+    // 3. Avoid sync-over-async — never block with .get()
+    // =========================================================================
+
+    /**
+     * Demonstrates the correct pattern: use {@code await} instead of the
+     * blocking {@code .get()} method on {@code Awaitable}.
+     */
+    @Test
+    void testAvoidSyncOverAsync_UseAwait() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class AsyncService {
+                async fetchData() {
+                    return "async-data"
+                }
+            }
+
+            def service = new AsyncService()
+
+            // GOOD: use await to get the result
+            def result = await(service.fetchData())
+            assert result == "async-data"
+        '''
+    }
+
+    @Test
+    void testAvoidSyncOverAsync_NestedAwait() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async fetchLevel1() {
+                return await(fetchLevel2())
+            }
+
+            async fetchLevel2() {
+                return await(fetchLevel3())
+            }
+
+            async fetchLevel3() {
+                return 42
+            }
+
+            // GOOD: await propagates cleanly through the chain
+            assert await(fetchLevel1()) == 42
+        '''
+    }
+
+    // =========================================================================
+    // 4. Prefer await over then/thenCompose
+    // =========================================================================
+
+    /**
+     * Use {@code await} instead of chaining with {@code .then()} or
+     * {@code .thenCompose()} for cleaner, more readable code.
+     */
+    @Test
+    void testPreferAwaitOverThen() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async callDependency() { return 10 }
+
+            // GOOD: await with sequential logic is clearer than chaining
+            async doSomething() {
+                def result = await callDependency()
+                return result + 1
+            }
+
+            assert await(doSomething()) == 11
+        '''
+    }
+
+    @Test
+    void testPreferAwaitOverThenCompose_MultiStep() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async step1() { return 1 }
+            async step2(int n) { return n * 2 }
+            async step3(int n) { return n + 10 }
+
+            // GOOD: sequential awaits read like synchronous code
+            async pipeline() {
+                def a = await step1()
+                def b = await step2(a)
+                def c = await step3(b)
+                return c
+            }
+
+            assert await(pipeline()) == 12
+        '''
+    }
+
+    // =========================================================================
+    // 5. Prefer async/await over directly returning Awaitable
+    // =========================================================================
+
+    /**
+     * Wrapping in {@code async} provides better exception handling and
+     * debugging compared to directly returning an {@code Awaitable}.
+     */
+    @Test
+    void testPreferAsyncOverDirectReturn() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async callDependency() { return "data" }
+
+            // GOOD: async/await wraps exceptions and provides better stack traces
+            async doSomething() {
+                return await callDependency()
+            }
+
+            assert await(doSomething()) == "data"
+        '''
+    }
+
+    @Test
+    void testPreferAsyncOverDirectReturn_ExceptionWrapping() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async riskyOperation() {
+                throw new IOException("disk failure")
+            }
+
+            // GOOD: exceptions in async methods are captured in the Awaitable
+            async safeWrapper() {
+                try {
+                    return await riskyOperation()
+                } catch (IOException e) {
+                    return "fallback: ${e.message}"
+                }
+            }
+
+            assert await(safeWrapper()) == "fallback: disk failure"
+        '''
+    }
+
+    // =========================================================================
+    // 6. Cancelling uncancellable operations with a timeout
+    // =========================================================================
+
+    /**
+     * Use {@code awaitAny} with {@code delay} to implement timeout-based
+     * cancellation for operations that don't support cancellation natively.
+     * This is the Groovy equivalent of C#'s {@code Task.WhenAny + Task.Delay}.
+     */
+    @Test
+    void testTimeoutWithAwaitAny() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.Awaitable
+
+            async slowOperation() {
+                await Awaitable.delay(5000)
+                return "completed"
+            }
+
+            async withTimeout(Awaitable task, long timeoutMs) {
+                def timeout = Awaitable.delay(timeoutMs)
+                def winner = await(Awaitable.any(task, timeout))
+                if (winner == null) {
+                    throw new java.util.concurrent.TimeoutException("operation timed out")
+                }
+                return winner
+            }
+
+            def task = slowOperation()
+            try {
+                await(withTimeout(task, 50))
+                assert false : "should have timed out"
+            } catch (java.util.concurrent.TimeoutException e) {
+                assert e.message == "operation timed out"
+            } finally {
+                task.cancel()
+            }
+        '''
+    }
+
+    @Test
+    void testTimeoutWithAwaitAny_SuccessBeforeTimeout() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.Awaitable
+
+            async fastOperation() {
+                await Awaitable.delay(10)
+                return "fast-result"
+            }
+
+            def task = fastOperation()
+            def timeout = Awaitable.delay(2000)
+            def winner = await(Awaitable.any(task, timeout))
+            assert winner == "fast-result"
+        '''
+    }
+
+    // =========================================================================
+    // 7. Timer callback patterns
+    // =========================================================================
+
+    /**
+     * Demonstrates safe async work in scheduled/timer contexts.
+     * Instead of fire-and-forget, capture the Awaitable for error tracking.
+     */
+    @Test
+    void testTimerCallback_SafeAsyncExecution() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.atomic.AtomicInteger
+            import java.util.concurrent.CountDownLatch
+
+            def pingCount = new AtomicInteger(0)
+            def latch = new CountDownLatch(3)
+
+            // Use async closure that captures local variables
+            def doAsyncPing = { AtomicInteger pc, CountDownLatch lt ->
+                def task = async {
+                    await Awaitable.delay(5)
+                    pc.incrementAndGet()
+                    lt.countDown()
+                    return "pong"
+                }
+                task()
+            }
+
+            // Schedule 3 pings using a timer
+            def timer = new Timer(true)
+            timer.schedule(new TimerTask() {
+                void run() {
+                    doAsyncPing(pingCount, latch)
+                }
+            }, 0, 20)
+
+            latch.await(5, java.util.concurrent.TimeUnit.SECONDS)
+            timer.cancel()
+            assert pingCount.get() >= 3
+        '''
+    }
+
+    @Test
+    void testTimerCallback_PeriodicAsyncWork() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.CountDownLatch
+
+            def results = Collections.synchronizedList([])
+            def latch = new CountDownLatch(3)
+
+            // Use async closures that capture the local variables
+            3.times { i ->
+                def myResults = results
+                def myLatch = latch
+                def task = async {
+                    await Awaitable.delay(5)
+                    myResults.add("item-${i}")
+                    myLatch.countDown()
+                    return "done"
+                }
+                task()
+            }
+
+            latch.await(5, java.util.concurrent.TimeUnit.SECONDS)
+            assert results.size() == 3
+            assert results.sort() == ["item-0", "item-1", "item-2"]
+        '''
+    }
+
+    // =========================================================================
+    // 8. Async lazy caching with ConcurrentHashMap
+    // =========================================================================
+
+    /**
+     * Demonstrates async lazy caching pattern using {@code ConcurrentHashMap}
+     * with {@code Awaitable} values. This avoids thread-pool starvation that
+     * would occur with synchronous {@code .get()} in the cache factory.
+     */
+    @Test
+    void testAsyncLazyCaching() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.ConcurrentHashMap
+            import java.util.concurrent.atomic.AtomicInteger
+
+            class PersonCache {
+                final AtomicInteger fetchCount = new AtomicInteger(0)
+                final ConcurrentHashMap<Integer, Awaitable> cache = new ConcurrentHashMap<>()
+
+                Awaitable fetchPerson(int id) {
+                    def counter = fetchCount
+                    def fetch = async {
+                        counter.incrementAndGet()
+                        await Awaitable.delay(10)
+                        return "Person-${id}"
+                    }
+                    return fetch()
+                }
+            }
+
+            def pc = new PersonCache()
+
+            // First access: triggers fetch
+            def result1 = await(pc.cache.computeIfAbsent(1, { id -> pc.fetchPerson(id) }))
+            assert result1 == "Person-1"
+            assert pc.fetchCount.get() == 1
+
+            // Second access: cache hit, no new fetch
+            def result2 = await(pc.cache.computeIfAbsent(1, { id -> pc.fetchPerson(id) }))
+            assert result2 == "Person-1"
+            assert pc.fetchCount.get() == 1
+
+            // Different key: new fetch
+            def result3 = await(pc.cache.computeIfAbsent(2, { id -> pc.fetchPerson(id) }))
+            assert result3 == "Person-2"
+            assert pc.fetchCount.get() == 2
+        '''
+    }
+
+    @Test
+    void testAsyncLazyCaching_ConcurrentAccess() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.ConcurrentHashMap
+            import java.util.concurrent.atomic.AtomicInteger
+            import java.util.concurrent.CountDownLatch
+
+            class DataCache {
+                final AtomicInteger fetchCount = new AtomicInteger(0)
+                final ConcurrentHashMap<Integer, Awaitable> cache = new ConcurrentHashMap<>()
+
+                Awaitable fetchData(int id) {
+                    def counter = fetchCount
+                    def fetch = async {
+                        counter.incrementAndGet()
+                        await Awaitable.delay(50)
+                        return "Data-${id}"
+                    }
+                    return fetch()
+                }
+            }
+
+            def dc = new DataCache()
+            def latch = new CountDownLatch(10)
+            def results = Collections.synchronizedList([])
+
+            // Simulate 10 concurrent requests for the same key
+            10.times {
+                def myCache = dc
+                def myResults = results
+                def myLatch = latch
+                def task = async {
+                    def value = await(myCache.cache.computeIfAbsent(1, { id -> myCache.fetchData(id) }))
+                    myResults.add(value)
+                    myLatch.countDown()
+                }
+                task()
+            }
+
+            latch.await(5, java.util.concurrent.TimeUnit.SECONDS)
+            assert results.size() == 10
+            assert results.every { it == "Data-1" }
+            // computeIfAbsent ensures at most 1 fetch for the same key
+            assert dc.fetchCount.get() == 1
+        '''
+    }
+
+    // =========================================================================
+    // 9. Static factory pattern for async construction
+    // =========================================================================
+
+    /**
+     * When a class requires asynchronous initialization, use a static factory
+     * method instead of blocking in the constructor.
+     */
+    @Test
+    void testStaticFactoryForAsyncConstruction() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class RemoteService {
+                final String connectionId
+
+                private RemoteService(String connectionId) {
+                    this.connectionId = connectionId
+                }
+
+                // GOOD: static async factory
+                static async create() {
+                    await Awaitable.delay(10)
+                    def connId = "conn-${System.nanoTime()}"
+                    return new RemoteService(connId)
+                }
+
+                async publish(String message) {
+                    return "Published '${message}' on ${connectionId}"
+                }
+            }
+
+            def service = await(RemoteService.create())
+            assert service.connectionId.startsWith("conn-")
+            def publishResult = await(service.publish("hello"))
+            assert publishResult.contains("Published 'hello'")
+        '''
+    }
+
+    @Test
+    void testStaticFactoryForAsyncConstruction_WithDependency() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.Awaitable
+
+            class Database {
+                final String url
+                private Database(String url) { this.url = url }
+
+                static async connect(String url) {
+                    await Awaitable.delay(10)
+                    return new Database(url)
+                }
+
+                async query(String sql) {
+                    await Awaitable.delay(5)
+                    return ["row1", "row2"]
+                }
+            }
+
+            class Repository {
+                final Database db
+                private Repository(Database db) { this.db = db }
+
+                static async create(String dbUrl) {
+                    def db = await Database.connect(dbUrl)
+                    return new Repository(db)
+                }
+
+                async findAll() {
+                    return await db.query("SELECT * FROM items")
+                }
+            }
+
+            def repo = await(Repository.create("jdbc:h2:mem:test"))
+            assert await(repo.findAll()) == ["row1", "row2"]
+        '''
+    }
+
+    // =========================================================================
+    // 10. Long-running work with dedicated threads
+    // =========================================================================
+
+    /**
+     * For long-running blocking work, use a dedicated thread instead of
+     * occupying a thread-pool thread indefinitely.
+     */
+    @Test
+    void testLongRunningWork_DedicatedThread() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.LinkedBlockingQueue
+            import java.util.concurrent.CountDownLatch
+
+            class QueueProcessor {
+                private final LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>()
+                private final List<String> processed = Collections.synchronizedList([])
+                private volatile boolean running = true
+
+                void startProcessing() {
+                    // GOOD: dedicated daemon thread for long-running work
+                    def t = new Thread({
+                        while (running) {
+                            def item = queue.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS)
+                            if (item != null) processed.add(item.toUpperCase())
+                        }
+                    })
+                    t.daemon = true
+                    t.name = "queue-processor"
+                    t.start()
+                }
+
+                void enqueue(String message) { queue.add(message) }
+
+                void stop() { running = false }
+
+                List<String> getProcessed() { return processed }
+            }
+
+            def processor = new QueueProcessor()
+            processor.startProcessing()
+
+            // Enqueue items
+            3.times { i -> processor.enqueue("msg-${i}") }
+            Thread.sleep(300)
+
+            processor.stop()
+            assert processor.processed.sort() == ["MSG-0", "MSG-1", "MSG-2"]
+        '''
+    }
+
+    // =========================================================================
+    // 11. Fire-and-forget safely
+    // =========================================================================
+
+    /**
+     * Always capture the {@code Awaitable} returned by async methods.
+     * Uncaptured awaitables may lose exceptions silently.
+     */
+    @Test
+    void testFireAndForget_CaptureAwaitable() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.CountDownLatch
+            import java.util.concurrent.atomic.AtomicBoolean
+
+            def completed = new AtomicBoolean(false)
+            def latch = new CountDownLatch(1)
+            def myCompleted = completed
+            def myLatch = latch
+
+            // Use async closure to capture local variables
+            def task = async {
+                await Awaitable.delay(10)
+                myCompleted.set(true)
+                myLatch.countDown()
+                return "done"
+            }
+
+            // GOOD: capture the Awaitable for tracking
+            def awaitable = task()
+
+            latch.await(5, java.util.concurrent.TimeUnit.SECONDS)
+            assert completed.get()
+            assert await(awaitable) == "done"
+        '''
+    }
+
+    @Test
+    void testFireAndForget_ExceptionTracking() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.Awaitable
+
+            class BackgroundService {
+                async failingBackground() {
+                    await Awaitable.delay(10)
+                    throw new RuntimeException("background failure")
+                }
+            }
+
+            def service = new BackgroundService()
+
+            // GOOD: capture so we can detect the failure
+            def task = service.failingBackground()
+
+            // Wait for completion, then check the error
+            Thread.sleep(200)
+            assert task.isCompletedExceptionally()
+
+            try {
+                await(task)
+                assert false : "should have thrown"
+            } catch (RuntimeException e) {
+                assert e.message == "background failure"
+            }
+        '''
+    }
+
+    // =========================================================================
+    // 12. Parallel execution with awaitAll
+    // =========================================================================
+
+    /**
+     * Use {@code awaitAll} to run independent operations concurrently, then
+     * collect all results. This is the equivalent of C#'s {@code Task.WhenAll}.
+     */
+    @Test
+    void testParallelExecution_AwaitAll() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async fetchUser() {
+                await Awaitable.delay(10)
+                return "User-1"
+            }
+
+            async fetchOrders() {
+                await Awaitable.delay(10)
+                return "Orders-5"
+            }
+
+            async fetchSettings() {
+                await Awaitable.delay(10)
+                return "Theme-Dark"
+            }
+
+            // GOOD: run all three concurrently
+            def userTask = fetchUser()
+            def ordersTask = fetchOrders()
+            def settingsTask = fetchSettings()
+
+            def results = await(Awaitable.all(userTask, ordersTask, settingsTask))
+            assert results == ["User-1", "Orders-5", "Theme-Dark"]
+        '''
+    }
+
+    @Test
+    void testParallelExecution_AwaitAllSettled() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.AwaitResult
+
+            async successOp() {
+                await Awaitable.delay(10)
+                return "ok"
+            }
+
+            async failOp() {
+                await Awaitable.delay(10)
+                throw new RuntimeException("fail")
+            }
+
+            def results = await(Awaitable.allSettled(successOp(), failOp()))
+            assert results.size() == 2
+            assert results[0].isSuccess()
+            assert results[0].value == "ok"
+            assert results[1].isFailure()
+            assert results[1].error.message == "fail"
+        '''
+    }
+
+    // =========================================================================
+    // 13. Cancellation propagation
+    // =========================================================================
+
+    /**
+     * Always propagate cancellation through the async call chain.
+     */
+    @Test
+    void testCancellationPropagation() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async longRunning() {
+                await Awaitable.delay(10000)
+                return "done"
+            }
+
+            def task = longRunning()
+            Thread.sleep(50)
+            task.cancel()
+
+            assert task.isCancelled()
+            assert task.isCompletedExceptionally()
+        '''
+    }
+
+    @Test
+    void testCancellationPropagation_WithCleanup() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.atomic.AtomicBoolean
+
+            class CancelCleanupDemo {
+                final AtomicBoolean cleanedUp = new AtomicBoolean(false)
+
+                async longRunningWithCleanup() {
+                    defer { cleanedUp.set(true) }
+                    await Awaitable.delay(10000)
+                    return "done"
+                }
+            }
+
+            def demo = new CancelCleanupDemo()
+            def task = demo.longRunningWithCleanup()
+            Thread.sleep(100)
+            task.cancel()
+            Thread.sleep(100)
+
+            assert task.isCancelled()
+        '''
+    }
+
+    // =========================================================================
+    // 14. Exception handling in async chains
+    // =========================================================================
+
+    /**
+     * Use try/catch in async methods for clean exception handling, similar
+     * to synchronous code.
+     */
+    @Test
+    void testExceptionHandling_TryCatchInAsync() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async riskyFetch(boolean shouldFail) {
+                await Awaitable.delay(10)
+                if (shouldFail) throw new IOException("network error")
+                return "data"
+            }
+
+            async resilientFetch() {
+                try {
+                    return await riskyFetch(true)
+                } catch (IOException e) {
+                    return "fallback"
+                }
+            }
+
+            assert await(resilientFetch()) == "fallback"
+            assert await(riskyFetch(false)) == "data"
+        '''
+    }
+
+    @Test
+    void testExceptionHandling_ExceptionallyRecovery() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async failingOp() {
+                throw new RuntimeException("oops")
+            }
+
+            // Use Awaitable.exceptionally() for inline recovery
+            def recovered = failingOp().exceptionally { ex -> "recovered from: ${ex.message}" }
+            assert await(recovered) == "recovered from: oops"
+        '''
+    }
+
+    // =========================================================================
+    // 15. Mixing async and synchronous code
+    // =========================================================================
+
+    /**
+     * Demonstrates the recommended pattern of keeping synchronous computation
+     * synchronous and only using async for truly asynchronous operations.
+     */
+    @Test
+    void testMixingSyncAndAsync() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.Awaitable
+
+            // Synchronous computation — no need for async
+            int add(int a, int b) { a + b }
+
+            // Truly asynchronous I/O simulation
+            async fetchRemoteValue() {
+                await Awaitable.delay(10)
+                return 100
+            }
+
+            async compute() {
+                def remote = await fetchRemoteValue()
+                // GOOD: synchronous work stays synchronous
+                def local = add(remote, 50)
+                return local
+            }
+
+            assert await(compute()) == 150
+        '''
+    }
+
+    @Test
+    void testMixingSyncAndAsync_ConditionalAsync() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.Awaitable
+
+            async getValueAsync(boolean useCache) {
+                if (useCache) {
+                    // No async operation needed — return pre-computed value
+                    return 42
+                }
+                // Truly async path
+                await Awaitable.delay(10)
+                return 42
+            }
+
+            // Both paths return the same result
+            assert await(getValueAsync(true)) == 42
+            assert await(getValueAsync(false)) == 42
+        '''
+    }
+
+    // =========================================================================
+    // 16. Structured concurrency patterns
+    // =========================================================================
+
+    /**
+     * Demonstrates structured concurrency: start multiple tasks, await them
+     * all, and aggregate results.
+     */
+    @Test
+    void testStructuredConcurrency_FanOutFanIn() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async processChunk(int chunkId, List<Integer> data) {
+                await Awaitable.delay(10)
+                return data.sum() * chunkId
+            }
+
+            async processAll(List<Integer> data) {
+                def chunks = data.collate(3)
+                def tasks = chunks.withIndex().collect { chunk, idx ->
+                    processChunk(idx + 1, chunk)
+                }
+                def results = await(Awaitable.all(*tasks))
+                return results.sum()
+            }
+
+            def data = [1, 2, 3, 4, 5, 6, 7, 8, 9]
+            def total = await(processAll(data))
+            // chunks: [1,2,3]=6, [4,5,6]=15, [7,8,9]=24
+            // results: 6*1=6, 15*2=30, 24*3=72
+            assert total == 108
+        '''
+    }
+
+    @Test
+    void testStructuredConcurrency_RetryPattern() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.atomic.AtomicInteger
+
+            class RetryDemo {
+                final AtomicInteger attempts = new AtomicInteger(0)
+
+                async unreliableOp() {
+                    await Awaitable.delay(5)
+                    if (attempts.incrementAndGet() < 3) {
+                        throw new IOException("transient failure")
+                    }
+                    return "success"
+                }
+            }
+
+            async withRetry(int maxRetries, Closure<groovy.concurrent.Awaitable> op) {
+                for (int i = 0; i <= maxRetries; i++) {
+                    try {
+                        return await op()
+                    } catch (IOException e) {
+                        if (i == maxRetries) throw e
+                        await Awaitable.delay(10)
+                    }
+                }
+            }
+
+            def demo = new RetryDemo()
+            def result = await(withRetry(3, { demo.unreliableOp() }))
+            assert result == "success"
+            assert demo.attempts.get() == 3
+        '''
+    }
+
+    // =========================================================================
+    // 17. Async generators with for-await
+    // =========================================================================
+
+    /**
+     * Use {@code for await} to consume asynchronous data streams, similar to
+     * C#'s {@code await foreach} or JavaScript's {@code for await...of}.
+     */
+    @Test
+    void testAsyncGenerator_ForAwaitConsumption() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async generatePages() {
+                def results = []
+                for await (page in asyncStream()) {
+                    results << page
+                }
+                return results
+            }
+
+            async Iterable asyncStream() {
+                yield return "page-1"
+                await Awaitable.delay(10)
+                yield return "page-2"
+                await Awaitable.delay(10)
+                yield return "page-3"
+            }
+
+            assert await(generatePages()) == ["page-1", "page-2", "page-3"]
+        '''
+    }
+
+    // =========================================================================
+    // 18. Defer for cleanup (Go-style)
+    // =========================================================================
+
+    /**
+     * Use {@code defer} for guaranteed cleanup, analogous to Go's defer.
+     * Defer actions execute in LIFO order when the method returns.
+     */
+    @Test
+    void testDeferCleanup() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            class DeferDemo {
+                final List log = Collections.synchronizedList([])
+
+                async processWithCleanup() {
+                    log << "start"
+                    defer { log << "cleanup-2" }
+                    defer { log << "cleanup-1" }
+                    await Awaitable.delay(10)
+                    log << "work-done"
+                    return "result"
+                }
+            }
+
+            def demo = new DeferDemo()
+            assert await(demo.processWithCleanup()) == "result"
+            assert demo.log == ["start", "work-done", "cleanup-1", "cleanup-2"]
+        '''
+    }
+
+    @Test
+    void testDeferCleanup_EvenOnException() {
+        assertScript '''
+            class DeferExceptionDemo {
+                final List log = Collections.synchronizedList([])
+
+                async riskyWork() {
+                    defer { log << "cleaned-up" }
+                    throw new RuntimeException("fail")
+                }
+            }
+
+            def demo = new DeferExceptionDemo()
+            try {
+                await(demo.riskyWork())
+            } catch (RuntimeException e) {
+                assert e.message == "fail"
+            }
+
+            Thread.sleep(100)
+            assert demo.log.contains("cleaned-up")
+        '''
+    }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncClosureLambdaTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncClosureLambdaTest.groovy
new file mode 100644
index 0000000000..31529a994e
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncClosureLambdaTest.groovy
@@ -0,0 +1,701 @@
+/*
+ *  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 async closures and async lambda expressions.
+ * <p>
+ * Syntax:
+ * <ul>
+ *   <li>{@code async { body }} — returns Closure&lt;Awaitable&gt; that must be called with () to execute</li>
+ *   <li>{@code async { x, y -> body }} — creates async closure, returns Closure&lt;Awaitable&gt;</li>
+ *   <li>{@code async (x, y) -> { body }} — creates async lambda, returns Closure&lt;Awaitable&gt;</li>
+ *   <li>Both support {@code yield return} for AsyncStream generation</li>
+ * </ul>
+ *
+ * @since 6.0.0
+ */
+final class AsyncClosureLambdaTest {
+
+    // =========================================================================
+    // 1. Async Closure — no params (returns Closure that must be called)
+    // =========================================================================
+
+    @Test
+    void testAsyncClosureImmediateExecution() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def result = async { 42 }
+            assert result instanceof Closure
+            assert result() instanceof Awaitable
+            assert await(result()) == 42
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithExplicitEmptyParams() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def result = async { -> "hello" }
+            assert result instanceof Closure
+            assert await(result()) == "hello"
+        '''
+    }
+
+    @Test
+    void testAsyncClosureRunsOnSeparateThread() {
+        assertScript '''
+            def mainThread = Thread.currentThread().name
+            def awaitable = async { Thread.currentThread().name }
+            def asyncThread = await(awaitable())
+            assert asyncThread != mainThread
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithComputation() {
+        assertScript '''
+            def awaitable = async {
+                def sum = 0
+                for (i in 1..100) { sum += i }
+                sum
+            }
+            assert await(awaitable()) == 5050
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithAwaitInside() {
+        assertScript '''
+            async fetchValue() { return 10 }
+
+            def result = async {
+                def v = await(fetchValue())
+                v * 2
+            }
+            assert await(result()) == 20
+        '''
+    }
+
+    @Test
+    void testMultipleAsyncClosuresParallel() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def a = async { Thread.sleep(50); 1 }
+            def b = async { Thread.sleep(50); 2 }
+            def c = async { Thread.sleep(50); 3 }
+
+            def results = await(Awaitable.all(a(), b(), c()))
+            assert results == [1, 2, 3]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureAssignedToVariable() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def task = async { 100 + 200 }
+            // Can use the Awaitable later
+            assert task instanceof Closure
+            def val = await(task())
+            assert val == 300
+        '''
+    }
+
+    @Test
+    void testAsyncClosureReturnsNull() {
+        assertScript '''
+            def result = async { null }
+            assert await(result()) == null
+        '''
+    }
+
+    // =========================================================================
+    // 2. Async Closure — with parameters (deferred execution)
+    // =========================================================================
+
+    @Test
+    void testAsyncClosureWithParams() {
+        assertScript '''
+            def asyncAdd = async { x, y -> x + y }
+            def result = asyncAdd(10, 20)
+            assert await(result) == 30
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithSingleParam() {
+        assertScript '''
+            def asyncDouble = async { n -> n * 2 }
+            assert await(asyncDouble(21)) == 42
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithParamsCalledMultipleTimes() {
+        assertScript '''
+            def asyncSquare = async { n -> n * n }
+            def r1 = await(asyncSquare(3))
+            def r2 = await(asyncSquare(5))
+            def r3 = await(asyncSquare(7))
+            assert r1 == 9
+            assert r2 == 25
+            assert r3 == 49
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithParamsIsClosure() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def asyncFn = async { x -> x.toUpperCase() }
+            assert asyncFn instanceof Closure
+            def result = asyncFn("hello")
+            assert result instanceof Awaitable
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithParamsAndStringOps() {
+        assertScript '''
+            def asyncConcat = async { a, b, sep -> "${a}${sep}${b}" }
+            assert await(asyncConcat("hello", "world", " ")) == "hello world"
+        '''
+    }
+
+    @Test
+    void testAsyncClosurePassedAsArgument() {
+        assertScript '''
+            def applyAsync(fn, x) {
+                return await(fn(x))
+            }
+
+            def asyncInc = async { n -> n + 1 }
+            assert applyAsync(asyncInc, 41) == 42
+        '''
+    }
+
+    @Test
+    void testAsyncClosureStoredInCollection() {
+        assertScript '''
+            def ops = [
+                add: async { a, b -> a + b },
+                mul: async { a, b -> a * b },
+                sub: async { a, b -> a - b }
+            ]
+
+            assert await(ops.add(3, 4)) == 7
+            assert await(ops.mul(3, 4)) == 12
+            assert await(ops.sub(10, 4)) == 6
+        '''
+    }
+
+    // =========================================================================
+    // 3. Async Lambda Expression
+    // =========================================================================
+
+    @Test
+    void testAsyncLambdaWithSingleParam() {
+        assertScript '''
+            def asyncDouble = async (n) -> { n * 2 }
+            assert await(asyncDouble(21)) == 42
+        '''
+    }
+
+    @Test
+    void testAsyncLambdaWithMultipleParams() {
+        assertScript '''
+            def asyncAdd = async (a, b) -> { a + b }
+            assert await(asyncAdd(10, 32)) == 42
+        '''
+    }
+
+    @Test
+    void testAsyncLambdaWithSingleExpression() {
+        assertScript '''
+            def asyncInc = async (n) -> n + 1
+            assert await(asyncInc(41)) == 42
+        '''
+    }
+
+    @Test
+    void testAsyncLambdaIsClosure() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def fn = async (x) -> { x * x }
+            assert fn instanceof Closure
+            assert fn(5) instanceof Awaitable
+        '''
+    }
+
+    @Test
+    void testAsyncLambdaCalledMultipleTimes() {
+        assertScript '''
+            def factorial = async (n) -> {
+                def result = 1
+                for (i in 2..n) { if (n >= 2) result *= i }
+                result
+            }
+
+            assert await(factorial(5)) == 120
+            assert await(factorial(6)) == 720
+            assert await(factorial(10)) == 3628800
+        '''
+    }
+
+    // =========================================================================
+    // 4. Async Closure with yield return (AsyncStream generator)
+    // =========================================================================
+
+    @Test
+    void testAsyncClosureGeneratorImmediate() {
+        assertScript '''
+            import groovy.concurrent.AsyncStream
+
+            def gen = async {
+                yield return 1
+                yield return 2
+                yield return 3
+            }
+            assert gen instanceof Closure : "Expected Closure but got ${gen.getClass().name}"
+            def stream = gen()
+            assert stream instanceof AsyncStream
+
+            def items = []
+            while (await(stream.moveNext())) {
+                items << stream.current
+            }
+            assert items == [1, 2, 3]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureGeneratorWithForAwait() {
+        assertScript '''
+            async collect() {
+                def gen = async {
+                    yield return "a"
+                    yield return "b"
+                    yield return "c"
+                }
+                def items = []
+                for await (item in gen()) {
+                    items << item
+                }
+                return items
+            }
+
+            assert await(collect()) == ["a", "b", "c"]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureGeneratorWithParams() {
+        assertScript '''
+            async collect() {
+                def range = async { start, end ->
+                    for (i in start..end) {
+                        yield return i
+                    }
+                }
+                def items = []
+                for await (item in range(1, 5)) {
+                    items << item
+                }
+                return items
+            }
+
+            assert await(collect()) == [1, 2, 3, 4, 5]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureGeneratorWithParamsCalledMultipleTimes() {
+        assertScript '''
+            async test() {
+                def repeat = async { value, times ->
+                    for (i in 1..times) {
+                        yield return value
+                    }
+                }
+                def r1 = []
+                for await (item in repeat("x", 3)) { r1 << item }
+                def r2 = []
+                for await (item in repeat("y", 2)) { r2 << item }
+                return [r1, r2]
+            }
+
+            def result = await(test())
+            assert result[0] == ["x", "x", "x"]
+            assert result[1] == ["y", "y"]
+        '''
+    }
+
+    // =========================================================================
+    // 5. Async Lambda with yield return
+    // =========================================================================
+
+    @Test
+    void testAsyncLambdaGeneratorWithForAwait() {
+        assertScript '''
+            async collect() {
+                def squares = async (n) -> {
+                    for (i in 1..n) {
+                        yield return i * i
+                    }
+                }
+                def items = []
+                for await (item in squares(4)) {
+                    items << item
+                }
+                return items
+            }
+
+            assert await(collect()) == [1, 4, 9, 16]
+        '''
+    }
+
+    // =========================================================================
+    // 6. Exception handling in async closures/lambdas
+    // =========================================================================
+
+    @Test
+    void testAsyncClosureExceptionTransparency() {
+        assertScript '''
+            async caller() {
+                def task = async { throw new java.io.IOException("async closure error") }
+                try {
+                    await task()
+                    assert false
+                } catch (java.io.IOException e) {
+                    return e.message
+                }
+            }
+
+            assert await(caller()) == "async closure error"
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithParamsExceptionTransparency() {
+        assertScript '''
+            async caller() {
+                def asyncDiv = async { a, b ->
+                    if (b == 0) throw new ArithmeticException("division by zero")
+                    a / b
+                }
+                try {
+                    await asyncDiv(10, 0)
+                    assert false
+                } catch (ArithmeticException e) {
+                    return e.message
+                }
+            }
+
+            assert await(caller()) == "division by zero"
+        '''
+    }
+
+    @Test
+    void testAsyncLambdaExceptionHandling() {
+        assertScript '''
+            async caller() {
+                def failOp = async (msg) -> { throw new IllegalStateException(msg) }
+                try {
+                    await failOp("lambda error")
+                    assert false
+                } catch (IllegalStateException e) {
+                    return e.message
+                }
+            }
+
+            assert await(caller()) == "lambda error"
+        '''
+    }
+
+    @Test
+    void testAsyncClosureGeneratorExceptionPropagation() {
+        assertScript '''
+            async caller() {
+                def gen = async {
+                    yield return 1
+                    throw new java.io.IOException("generator error")
+                }
+                def items = []
+                try {
+                    for await (item in gen()) {
+                        items << item
+                    }
+                    assert false
+                } catch (java.io.IOException e) {
+                    return "${items}:${e.message}"
+                }
+            }
+
+            assert await(caller()) == "[1]:generator error"
+        '''
+    }
+
+    // =========================================================================
+    // 7. Async closure/lambda used with await syntax
+    // =========================================================================
+
+    @Test
+    void testAwaitAsyncClosureInAsyncMethod() {
+        assertScript '''
+            async compute() {
+                def taskA = async { 10 }
+                def a = await taskA()
+                def taskB = async { 20 }
+                def b = await taskB()
+                return a + b
+            }
+
+            assert await(compute()) == 30
+        '''
+    }
+
+    @Test
+    void testAwaitAsyncClosureWithParamsInAsyncMethod() {
+        assertScript '''
+            async compute() {
+                def multiply = async { x, y -> x * y }
+                def a = await multiply(6, 7)
+                return a
+            }
+
+            assert await(compute()) == 42
+        '''
+    }
+
+    @Test
+    void testAsyncClosuresComposed() {
+        assertScript '''
+            async pipeline() {
+                def step1 = async { 10 }
+                def v1 = await step1()
+
+                def step2 = async { v1 * 2 }
+                def v2 = await step2()
+
+                def step3 = async { v2 + 1 }
+                return await step3()
+            }
+
+            assert await(pipeline()) == 21
+        '''
+    }
+
+    // =========================================================================
+    // 8. Async closure/lambda with complex types
+    // =========================================================================
+
+    @Test
+    void testAsyncClosureWithListResult() {
+        assertScript '''
+            def task = async { [1, 2, 3].collect { it * it } }
+            assert await(task()) == [1, 4, 9]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithMapResult() {
+        assertScript '''
+            def task = async { [name: "Groovy", version: 6] }
+            def result = await(task())
+            assert result.name == "Groovy"
+            assert result.version == 6
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithStringInterpolation() {
+        assertScript '''
+            def greet = async { name -> "Hello, ${name}!" }
+            assert await(greet("World")) == "Hello, World!"
+        '''
+    }
+
+    // =========================================================================
+    // 9. Async closure/lambda used with awaitAll/awaitAny
+    // =========================================================================
+
+    @Test
+    void testAwaitAllWithAsyncClosures() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def t1 = async { 1 }
+            def t2 = async { 2 }
+            def t3 = async { 3 }
+            def results = await(Awaitable.all(t1(), t2(), t3()))
+            assert results == [1, 2, 3]
+        '''
+    }
+
+    @Test
+    void testAwaitAnyWithAsyncClosures() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def slowTask = async { Thread.sleep(100); "slow" }
+            def fastTask = async { "fast" }
+            def result = await(Awaitable.any(slowTask(), fastTask()))
+            // "fast" should usually win, but either is valid
+            assert result == "fast" || result == "slow"
+        '''
+    }
+
+    @Test
+    void testAwaitAllSettledWithAsyncClosures() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def ok = async { 42 }
+            def fail = async { throw new RuntimeException("fail") }
+            def results = await(Awaitable.allSettled(ok(), fail()))
+            assert results[0].isSuccess()
+            assert results[0].value == 42
+            assert results[1].isFailure()
+            assert results[1].error instanceof RuntimeException
+        '''
+    }
+
+    // =========================================================================
+    // 10. Edge cases
+    // =========================================================================
+
+    @Test
+    void testAsyncClosureCapturingOuterVariable() {
+        assertScript '''
+            def x = 100
+            def task = async { x + 1 }
+            assert await(task()) == 101
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithParamCapturingOuter() {
+        assertScript '''
+            def base = 1000
+            def asyncAdd = async { n -> base + n }
+            assert await(asyncAdd(42)) == 1042
+        '''
+    }
+
+    @Test
+    void testNestedAsyncClosures() {
+        assertScript '''
+            def outer = async {
+                def inner = async { 42 }
+                await(inner())
+            }
+            assert await(outer()) == 42
+        '''
+    }
+
+    @Test
+    void testAsyncClosureInLoopBody() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def tasks = (1..5).collect { n ->
+                def t = async { n * n }
+                t()
+            }
+            def results = await(Awaitable.all(tasks as Object[]))
+            assert results == [1, 4, 9, 16, 25]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithMethodCallChaining() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def task = async { "hello world" }
+            assert task instanceof Closure
+            def chained = task().then { it.toUpperCase() }
+            assert await(chained) == "HELLO WORLD"
+        '''
+    }
+
+    @Test
+    void testAsyncClosureWithExceptionally() {
+        assertScript '''
+            def task = async { throw new RuntimeException("boom") }
+            def recovered = task().exceptionally { err -> "recovered: ${err.message}" }
+            assert await(recovered) == "recovered: boom"
+        '''
+    }
+
+    @Test
+    void testAsyncClosureGeneratorFactoryPattern() {
+        assertScript '''
+            async collect() {
+                // Factory that creates fibonacci async streams
+                def fibonacci = async { limit ->
+                    def a = 0, b = 1
+                    for (i in 0..<limit) {
+                        yield return a
+                        def temp = a + b
+                        a = b
+                        b = temp
+                    }
+                }
+                def items = []
+                for await (item in fibonacci(8)) {
+                    items << item
+                }
+                return items
+            }
+
+            assert await(collect()) == [0, 1, 1, 2, 3, 5, 8, 13]
+        '''
+    }
+
+    @Test
+    void testAsyncClosureCancellation() {
+        assertScript '''
+            import java.util.concurrent.CancellationException
+
+            def taskDef = async { Thread.sleep(100); 42 }
+            def task = taskDef()
+            task.cancel()
+            assert task.isCancelled()
+            assert task.isCompletedExceptionally()
+            try {
+                await(task)
+                assert false
+            } catch (CancellationException e) {
+                // expected
+            }
+        '''
+    }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/AsyncCoverageTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/AsyncCoverageTest.groovy
new file mode 100644
index 0000000000..81746fa1ed
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/AsyncCoverageTest.groovy
@@ -0,0 +1,2488 @@
+/*
+ *  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.AwaitResult
+import groovy.concurrent.Awaitable
+import groovy.concurrent.AwaitableAdapter
+import groovy.concurrent.AwaitableAdapterRegistry
+import org.apache.groovy.runtime.async.AsyncStreamGenerator
+import org.apache.groovy.runtime.async.AsyncSupport
+import org.apache.groovy.runtime.async.GroovyPromise
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+
+import java.util.concurrent.Callable
+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.Flow
+import java.util.concurrent.FutureTask
+import java.util.concurrent.SubmissionPublisher
+import java.util.concurrent.TimeUnit
+
+import static groovy.test.GroovyAssert.assertScript
+
+/**
+ * Comprehensive coverage tests for Groovy's async/await API surface.
+ * <p>
+ * Targets the untested public methods of {@code Awaitable},
+ * {@code AwaitResult}, {@code GroovyPromise}, {@code AsyncStream},
+ * and {@code AwaitableAdapterRegistry}, as well as combinator edge cases and chaining patterns.
+ */
+class AsyncCoverageTest {
+
+    @AfterEach
+    void cleanup() {
+        Awaitable.setExecutor(null)
+    }
+
+    // ================================================================
+    // Awaitable instance methods: then, thenCompose, exceptionally
+    // ================================================================
+
+    @Test
+    void testThenTransformsResult() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async def fetchValue() { 10 }
+
+            def doubled = fetchValue().then { it * 2 }
+            assert doubled instanceof Awaitable
+            assert await(doubled) == 20
+        '''
+    }
+
+    @Test
+    void testThenComposeFlattensNestedAwaitable() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            async def fetchId() { 42 }
+            async def fetchName(int id) { "user-${id}" }
+
+            def result = fetchId().thenCompose { id -> fetchName(id) }
+            assert result instanceof Awaitable
+            assert await(result) == "user-42"
+        '''
+    }
+
+    @Test
+    void testExceptionallyRecoversFromFailure() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def failed = Awaitable.failed(new IOException("network error"))
+            def recovered = failed.exceptionally { ex -> "fallback: ${ex.message}" }
+            assert await(recovered) == "fallback: network error"
+        '''
+    }
+
+    @Test
+    void testExceptionallyPassesThroughOnSuccess() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def ok = Awaitable.of(99)
+            def same = ok.exceptionally { ex -> -1 }
+            assert await(same) == 99
+        '''
+    }
+
+    // ================================================================
+    // Awaitable instance methods: isDone, cancel, isCancelled,
+    // isCompletedExceptionally, toCompletableFuture, get(timeout)
+    // ================================================================
+
+    @Test
+    void testIsDoneReflectsCompletionState() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def completed = Awaitable.of("done")
+            assert completed.isDone()
+
+            def pending = Awaitable.delay(5000)
+            assert !pending.isDone()
+            pending.cancel()
+        '''
+    }
+
+    @Test
+    void testCancelAndIsCancelled() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.CancellationException
+
+            def task = Awaitable.delay(10_000)
+            assert !task.isCancelled()
+            boolean cancelled = task.cancel()
+            assert cancelled
+            assert task.isCancelled()
+            assert task.isDone()
+            assert task.isCompletedExceptionally()
+
+            try {
+                await(task)
+                assert false : "Should have thrown"
+            } catch (CancellationException e) {
+                // expected
+            }
+        '''
+    }
+
+    @Test
+    void testIsCompletedExceptionally() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            def ok = Awaitable.of(1)
+            assert !ok.isCompletedExceptionally()
+
+            def bad = Awaitable.failed(new RuntimeException("oops"))
+            assert bad.isCompletedExceptionally()
+        '''
+    }
+
+    @Test
+    void testToCompletableFuture() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.CompletableFuture
+
+            async def compute() { 42 }
+
+            def awaitable = compute()
+            def cf = awaitable.toCompletableFuture()
+            assert cf instanceof CompletableFuture
+            assert cf.get() == 42
+        '''
+    }
+
+    @Test
+    void testGetWithTimeout() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.TimeUnit
+            import java.util.concurrent.TimeoutException
+
+            def quick = Awaitable.of("fast")
+            assert quick.get(1, TimeUnit.SECONDS) == "fast"
+
+            def slow = Awaitable.delay(10_000)
+            try {
+                slow.get(50, TimeUnit.MILLISECONDS)
+                assert false : "Should timeout"
+            } catch (TimeoutException e) {
+                // expected
+            } finally {
+                slow.cancel()
+            }
+        '''
+    }
+
+    // ================================================================
+    // Awaitable static: delay(long, TimeUnit), getExecutor,
+    // isVirtualThreadsAvailable
+    // ================================================================
+
+    @Test
+    void testDelayWithTimeUnit() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.TimeUnit
+
+            long start = System.currentTimeMillis()
+            await Awaitable.delay(100, TimeUnit.MILLISECONDS)
+            long elapsed = System.currentTimeMillis() - start
+            assert elapsed >= 80 : "delay should wait ~100ms"
+        '''
+    }
+
+    @Test
+    void testGetExecutorReturnsNonNull() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.Executor
+
+            def exec = Awaitable.getExecutor()
+            assert exec != null
+            assert exec instanceof Executor
+        '''
+    }
+
+    @Test
+    void testIsVirtualThreadsAvailable() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+
+            // Just verify it returns a boolean without throwing
+            def result = Awaitable.isVirtualThreadsAvailable()
+            assert result instanceof Boolean
+        '''
+    }
+
+    @Test
+    void testSetExecutorAndRestore() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import java.util.concurrent.Executors
+
+            class CustomExecTest {
+                async def getThreadName() {
+                    Thread.currentThread().name
+                }
+            }
+
+            def pool = Executors.newSingleThreadExecutor { r ->
+                new Thread(r, "custom-exec-thread")
+            }
+            try {
+                Awaitable.setExecutor(pool)
+                assert Awaitable.getExecutor() == pool
+
+                def name = await(new CustomExecTest().getThreadName())
+                assert name == "custom-exec-thread"
+            } finally {
+                Awaitable.setExecutor(null)
+                pool.shutdown()
+            }
+        '''
+    }
+
+    // ================================================================
+    // AwaitResult: success/failure factories, getOrElse, toString
+    // ================================================================
+
+    @Test
+    void testAwaitResultFactories() {
+        assertScript '''
+            import groovy.concurrent.AwaitResult
+
+            def s = AwaitResult.success("hello")
+            assert s.isSuccess()
+            assert !s.isFailure()
+            assert s.value == "hello"
+            assert s.toString().contains("Success")
+
+            def f = AwaitResult.failure(new IOException("boom"))
+            assert f.isFailure()
+            assert !f.isSuccess()
+            assert f.error instanceof IOException
+            assert f.error.message == "boom"
+            assert f.toString().contains("Failure")
+        '''
+    }
+
+    @Test
+    void testAwaitResultGetOrElse() {
+        assertScript '''
+            import groovy.concurrent.AwaitResult
+
+            def s = AwaitResult.success(42)
+            assert s.getOrElse { -1 } == 42
+
+            def f = AwaitResult.failure(new IOException("err"))
+            assert f.getOrElse { ex -> "recovered: ${ex.message}" } == "recovered: err"
+        '''
+    }
+
+    @Test
+    void testAwaitResultGetValueThrowsOnFailure() {
+        assertScript '''
+            import groovy.concurrent.AwaitResult
+
+            def f = AwaitResult.failure(new IOException("err"))
+            try {
+                f.value
+                assert false : "Should throw"
+            } catch (IllegalStateException e) {
+                assert e.message.contains("failed result")
+            }
+        '''
+    }
+
+    @Test
+    void testAwaitResultGetErrorThrowsOnSuccess() {
+        assertScript '''
+            import groovy.concurrent.AwaitResult
+
+            def s = AwaitResult.success("ok")
+            try {
+                s.error
+                assert false : "Should throw"
+            } catch (IllegalStateException e) {
+                assert e.message.contains("successful result")
+            }
+        '''
+    }
+
+    // ================================================================
+    // AwaitableAdapterRegistry: register, unregister, blockingExecutor
+    // ================================================================
+
+    @Test
+    void testAdapterRegistryRegisterAndUnregister() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.AwaitableAdapter
+            import groovy.concurrent.AwaitableAdapterRegistry
+
+            // Create a custom adapter for a custom type
+            class CustomResult { String data }
+
+            def adapter = new AwaitableAdapter() {
+                boolean supportsAwaitable(Class<?> type) {
+                    CustomResult.isAssignableFrom(type)
+                }
+                def <T> Awaitable<T> toAwaitable(Object source) {
+                    Awaitable.of(((CustomResult) source).data)
+                }
+            }
+
+            // Register and verify it works
+            def handle = AwaitableAdapterRegistry.register(adapter)
+            try {
+                def result = AwaitableAdapterRegistry.toAwaitable(new CustomResult(data: "hello"))
+                assert await(result) == "hello"
+            } finally {
+                handle.close()
+            }
+
+            // After close, the adapter should be removed
+            try {
+                AwaitableAdapterRegistry.toAwaitable(new CustomResult(data: "fail"))
+                assert false : "Should throw"
+            } catch (IllegalArgumentException e) {
+                // expected
+            }
+        '''
+    }
+
+    @Test
+    void testAdapterRegistryExplicitUnregister() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.AwaitableAdapter
+            import groovy.concurrent.AwaitableAdapterRegistry
+
+            def adapter = new AwaitableAdapter() {
+                boolean supportsAwaitable(Class<?> type) { false }
+                def <T> Awaitable<T> toAwaitable(Object source) { null }
+            }
+
+            AwaitableAdapterRegistry.register(adapter)
+            assert AwaitableAdapterRegistry.unregister(adapter) == true
+            assert AwaitableAdapterRegistry.unregister(adapter) == false
+        '''
+    }
+
+    @Test
+    void testAdapterRegistrySetBlockingExecutor() {
+        assertScript '''
+            import groovy.concurrent.Awaitable
+            import groovy.concurrent.AwaitableAdapterRegistry
+            import java.util.concurrent.Executors
+            import java.util.concurrent.FutureTask
+
+            def pool = Executors.newSingleThreadExecutor()
+            try {
+                // Set blocking executor so Future adaptation uses it
+                AwaitableAdapterRegistry.setBlockingExecutor(pool)
+
+                // Create a plain java.util.concurrent.Future
+                def future = new FutureTask<String>({ "from-blocking-future" })
+                pool.submit(future)
+
+                def aw = AwaitableAdapterRegistry.toAwaitable(future)
+                assert await(aw) == "from-blocking-future"
+            } finally {
+                AwaitableAdapterRegistry.setBlockingExecutor(null)
+                pool.shutdown()
+            }
+        '''
+    }
+
+    // ================================================================
+    // AsyncStream: empty()
+    // ================================================================
+
+    @Test
+    void testAsyncStreamEmpty() {
+        assertScript '''
+            import groovy.concurrent.AsyncStream
+
+            def stream = AsyncStream.empty()
+            assert await(stream.moveNext()) == false
... 8029 lines suppressed ...