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/29 10:47:09 UTC

(groovy) branch GROOVY-9381_3 updated (b0d03f346e -> 5618df2b46)

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


    omit b0d03f346e GROOVY-9381: Add native async/await support
     new 5618df2b46 GROOVY-9381: Add native async/await support

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   (b0d03f346e)
            \
             N -- N -- N   refs/heads/GROOVY-9381_3 (5618df2b46)

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:
 .../groovy/runtime/async/FlowPublisherAdapter.java | 43 ++++++++++++----
 .../groovy/runtime/async/AsyncDeferFlowTest.groovy | 60 ++++++++++++++++++++--
 2 files changed, 90 insertions(+), 13 deletions(-)


(groovy) 01/01: GROOVY-9381: Add native async/await support

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 5618df2b46d1f6ba444b6c349a21280f28178a18
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Mar 7 20:00:28 2026 +0900

    GROOVY-9381: Add native async/await support
---
 gradle/verification-metadata.xml                   |   16 +
 settings.gradle                                    |    2 +
 src/antlr/GroovyLexer.g4                           |    3 +
 src/antlr/GroovyParser.g4                          |   15 +-
 src/main/java/groovy/concurrent/AsyncChannel.java  |  267 +
 src/main/java/groovy/concurrent/AsyncContext.java  |  404 ++
 src/main/java/groovy/concurrent/AsyncScope.java    |  280 ++
 src/main/java/groovy/concurrent/AsyncStream.java   |  146 +
 src/main/java/groovy/concurrent/AwaitResult.java   |  202 +
 src/main/java/groovy/concurrent/Awaitable.java     |  598 +++
 .../java/groovy/concurrent/AwaitableAdapter.java   |   86 +
 .../concurrent/AwaitableAdapterRegistry.java       |  350 ++
 .../groovy/concurrent/ChannelClosedException.java  |   77 +
 src/main/java/groovy/transform/Async.java          |  134 +
 .../apache/groovy/parser/antlr4/AstBuilder.java    |  187 +-
 .../groovy/parser/antlr4/ModifierManager.java      |   22 +-
 .../groovy/runtime/async/AbstractAsyncStream.java  |  287 ++
 .../groovy/runtime/async/AsyncStreamGenerator.java |  279 ++
 .../apache/groovy/runtime/async/AsyncSupport.java  | 1135 +++++
 .../groovy/runtime/async/DefaultAsyncChannel.java  |  410 ++
 .../groovy/runtime/async/DefaultAsyncContext.java  |  345 ++
 .../groovy/runtime/async/DefaultAsyncScope.java    |  314 ++
 .../groovy/runtime/async/FlowPublisherAdapter.java |  415 ++
 .../apache/groovy/runtime/async/GroovyPromise.java |  264 +
 .../apache/groovy/runtime/async/ScopedLocal.java   |  525 ++
 .../java/org/codehaus/groovy/ast/ModifierNode.java |    2 +
 .../groovy/transform/AsyncASTTransformation.java   |  254 +
 .../groovy/transform/AsyncTransformHelper.java     |  427 ++
 .../services/groovy.concurrent.AwaitableAdapter    |   16 +
 src/spec/doc/core-async-await.adoc                 | 2185 ++++++++
 src/spec/test/AsyncAwaitSpecTest.groovy            | 2023 ++++++++
 .../groovy/runtime/async/AsyncApiTest.groovy       | 5195 ++++++++++++++++++++
 .../runtime/async/AsyncAwaitSyntaxTest.groovy      | 3125 ++++++++++++
 .../runtime/async/AsyncBestPracticesTest.groovy    | 1593 ++++++
 .../runtime/async/AsyncClosureLambdaTest.groovy    |  774 +++
 .../runtime/async/AsyncConcurrencyTest.groovy      |  540 ++
 .../runtime/async/AsyncContextChannelTest.groovy   | 2355 +++++++++
 .../groovy/runtime/async/AsyncDeferFlowTest.groovy |  858 ++++
 .../async/AsyncExceptionHandlingTest.groovy        | 1633 ++++++
 .../async/AsyncFrameworkIntegrationTest.groovy     |  196 +
 .../groovy/runtime/async/AsyncPatternsTest.groovy  | 1901 +++++++
 .../async/AsyncRuntimeEnhancementTest.groovy       |  257 +
 .../groovy/runtime/async/AsyncTransformTest.groovy | 1747 +++++++
 .../runtime/async/AsyncVirtualThreadTest.groovy    | 1166 +++++
 .../groovy/runtime/async/ScopedLocalTest.groovy    |  807 +++
 subprojects/groovy-binary/src/spec/doc/index.adoc  |    4 +
 subprojects/groovy-reactor/build.gradle            |   38 +
 .../groovy/reactor/ReactorAwaitableAdapter.java    |  162 +
 .../org/apache/groovy/reactor/package-info.java    |   43 +
 .../services/groovy.concurrent.AwaitableAdapter    |   16 +
 .../src/spec/doc/reactor-userguide.adoc            |  334 ++
 .../groovy/reactor/ReactorDocSpecTest.groovy       |  402 ++
 .../reactor/ReactorAwaitableAdapterTest.groovy     |  843 ++++
 subprojects/groovy-rxjava/build.gradle             |   38 +
 .../groovy/rxjava/RxJavaAwaitableAdapter.java      |  191 +
 .../org/apache/groovy/rxjava/package-info.java     |   45 +
 .../services/groovy.concurrent.AwaitableAdapter    |   16 +
 .../src/spec/doc/rxjava-userguide.adoc             |  320 ++
 .../apache/groovy/rxjava/RxJavaDocSpecTest.groovy  |  292 ++
 .../rxjava/RxJavaAwaitableAdapterTest.groovy       |  828 ++++
 versions.properties                                |    2 +
 61 files changed, 37387 insertions(+), 4 deletions(-)

diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 27a92efd74..67c0a40134 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -116,6 +116,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"/>
@@ -920,6 +921,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"/>
@@ -2550,6 +2561,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/settings.gradle b/settings.gradle
index 908f53e871..440e13ed0a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -69,6 +69,8 @@ def subprojects = [
         'groovy-macro',
         'groovy-macro-library',
         'groovy-nio',
+        'groovy-reactor',
+        'groovy-rxjava',
         'groovy-servlet',
         'groovy-sql',
         'groovy-swing',
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 550852286a..9f12e2b16f 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -134,6 +134,7 @@ modifier
           |   VOLATILE
           |   DEF
           |   VAR
+          |   ASYNC
           )
     ;
 
@@ -601,7 +602,7 @@ switchStatement
     ;
 
 loopStatement
-    :   annotationsOpt FOR LPAREN forControl RPAREN nls statement                                             #forStmtAlt
+    :   annotationsOpt FOR AWAIT? LPAREN forControl RPAREN nls statement                                      #forStmtAlt
     |   annotationsOpt WHILE expressionInPar nls statement                                                    #whileStmtAlt
     |   annotationsOpt DO nls statement nls WHILE expressionInPar                                             #doWhileStmtAlt
     ;
@@ -643,6 +644,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
@@ -779,6 +782,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 (COMMA nls expression)* RPAREN | expression)           #awaitExprAlt
+
     // qualified names, array expressions, method invocation, post inc/dec
     |   postfixExpression                                                                   #postfixExprAlt
 
@@ -1229,6 +1236,9 @@ identifier
     :   Identifier
     |   CapitalizedIdentifier
     |   AS
+    |   ASYNC
+    |   AWAIT
+    |   DEFER
     |   IN
     |   PERMITS
     |   RECORD
@@ -1247,12 +1257,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/AsyncChannel.java b/src/main/java/groovy/concurrent/AsyncChannel.java
new file mode 100644
index 0000000000..024c2967f1
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncChannel.java
@@ -0,0 +1,267 @@
+/*
+ *  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.DefaultAsyncChannel;
+
+/**
+ * An asynchronous channel for inter-task communication with optional buffering.
+ *
+ * <p>A channel coordinates producers and consumers without exposing explicit
+ * locks or shared mutable state.  It models the classic
+ * <a href="https://en.wikipedia.org/wiki/Communicating_sequential_processes">CSP</a>
+ * (Communicating Sequential Processes) paradigm popularized by Go's channels
+ * and Kotlin's {@code Channel} type.
+ *
+ * <h2>Obtaining Instances</h2>
+ * <p>Channels are created via the static factory methods on this interface,
+ * following the same pattern as {@link java.util.List#of()}:
+ * <pre>
+ * def unbuffered = AsyncChannel.create()       // capacity 0 (rendezvous)
+ * def buffered   = AsyncChannel.create(10)     // capacity 10
+ * </pre>
+ *
+ * <h2>Buffering Modes</h2>
+ * <ul>
+ *   <li><b>Unbuffered (rendezvous)</b> — created with a capacity of {@code 0}.
+ *       Each {@link #send(Object)} suspends until a matching {@link #receive()}
+ *       arrives, and vice versa.  This provides the strongest synchronization
+ *       guarantee between producer and consumer.</li>
+ *   <li><b>Buffered</b> — created with a positive capacity.  Values are
+ *       enqueued in an internal buffer until it fills, after which senders
+ *       suspend until a receiver drains at least one slot.</li>
+ * </ul>
+ *
+ * <h2>Composition with {@code await}</h2>
+ * <p>All channel operations return {@link Awaitable} values, composing
+ * naturally with Groovy's {@code await} keyword and combinators such as
+ * {@link Awaitable#any(Object...)}.
+ *
+ * <h2>Iteration with {@code for await}</h2>
+ * <p>Channels integrate with Groovy's {@code for await} syntax via the
+ * {@link #asStream()} method, which provides a read-only {@link AsyncStream}
+ * view that yields received values until the channel is closed and drained:
+ * <pre>
+ * def ch = AsyncChannel.create(2)
+ * ch.send('a')
+ * ch.send('b')
+ * ch.close()
+ * for await (item in ch) {
+ *     println item   // prints 'a', then 'b'
+ * }
+ * </pre>
+ *
+ * <h2>Null Rejection</h2>
+ * <p>{@code null} payloads are rejected to maintain a clear distinction
+ * between "no value yet" and an actual payload.  Sending {@code null}
+ * throws {@link NullPointerException} immediately.
+ *
+ * <h2>Close Semantics</h2>
+ * <p>Closing a channel is an idempotent, one-way operation:
+ * <ul>
+ *   <li>Values already buffered remain receivable.</li>
+ *   <li>Pending senders fail immediately with {@link ChannelClosedException}.</li>
+ *   <li>After all buffered values are drained, subsequent receivers fail
+ *       with {@link ChannelClosedException}.</li>
+ * </ul>
+ *
+ * <h2>Thread Safety</h2>
+ * <p>All methods on {@code AsyncChannel} are safe for concurrent use by
+ * multiple threads.  Values are delivered in FIFO (first-in, first-out)
+ * order with respect to the order in which sends and receives are enqueued.
+ *
+ * @param <T> the payload type carried through the channel
+ * @see #create()
+ * @see #create(int)
+ * @see ChannelClosedException
+ * @see AsyncStream
+ * @since 6.0.0
+ */
+public interface AsyncChannel<T> {
+
+    // ---- Static Factory Methods -----------------------------------------
+
+    /**
+     * Creates an unbuffered (rendezvous) channel.
+     *
+     * <p>An unbuffered channel has a capacity of {@code 0}: each
+     * {@link #send(Object)} suspends until a matching {@link #receive()}
+     * arrives, providing the strongest producer–consumer synchronization
+     * guarantee.
+     *
+     * <p>This is the channel equivalent of Go's {@code make(chan T)} and
+     * Kotlin's {@code Channel(RENDEZVOUS)}.
+     *
+     * <p><b>Example:</b>
+     * <pre>
+     * def ch = AsyncChannel.&lt;String&gt;create()
+     * Awaitable.go { ch.send('hello') }
+     * def msg = await ch.receive()
+     * </pre>
+     *
+     * @param <T> the payload type
+     * @return a new unbuffered {@link AsyncChannel} instance
+     * @since 6.0.0
+     */
+    static <T> AsyncChannel<T> create() {
+        return new DefaultAsyncChannel<>();
+    }
+
+    /**
+     * Creates a channel with the specified buffer capacity.
+     *
+     * <p>Values sent to a buffered channel are enqueued in an internal
+     * buffer.  Senders complete immediately as long as buffer space is
+     * available; they suspend only when the buffer is full.
+     *
+     * <p>A capacity of {@code 0} is equivalent to calling {@link #create()},
+     * producing an unbuffered (rendezvous) channel.
+     *
+     * <p>This is the channel equivalent of C#'s
+     * {@code Channel.CreateBounded<T>(capacity)} and Go's
+     * {@code make(chan T, capacity)}.
+     *
+     * <p><b>Example:</b>
+     * <pre>
+     * def ch = AsyncChannel.&lt;Integer&gt;create(10)
+     * for (i in 1..10) { ch.send(i) }  // all complete immediately
+     * ch.close()
+     * </pre>
+     *
+     * @param capacity the maximum number of values the buffer can hold;
+     *                 must be {@code >= 0}.  A capacity of {@code 0}
+     *                 creates an unbuffered (rendezvous) channel.
+     * @param <T>      the payload type
+     * @return a new {@link AsyncChannel} instance with the given capacity
+     * @throws IllegalArgumentException if {@code capacity} is negative
+     * @since 6.0.0
+     */
+    static <T> AsyncChannel<T> create(int capacity) {
+        return new DefaultAsyncChannel<>(capacity);
+    }
+
+    /**
+     * Returns this channel's configured buffer capacity.
+     *
+     * <p>A capacity of {@code 0} indicates an unbuffered (rendezvous)
+     * channel.  A positive value indicates the maximum number of elements
+     * that can be buffered before senders must wait.
+     *
+     * @return the buffer capacity (always {@code >= 0})
+     */
+    int getCapacity();
+
+    /**
+     * Returns the number of elements currently held in the buffer.
+     *
+     * <p>This is a snapshot value and may be stale by the time the caller
+     * acts on it.  It is primarily useful for monitoring and diagnostics.
+     *
+     * @return the current number of buffered elements
+     */
+    int getBufferedSize();
+
+    /**
+     * Returns {@code true} if the channel has been closed.
+     *
+     * <p>A closed channel may still contain buffered values that can be
+     * received.  This method only indicates that no further sends will
+     * be accepted.
+     *
+     * @return {@code true} if the channel has been closed
+     */
+    boolean isClosed();
+
+    /**
+     * Sends a value through the channel asynchronously.
+     *
+     * <p>The returned {@link Awaitable} completes when the value has been
+     * either handed off to a waiting receiver or enqueued in the buffer.
+     * If the buffer is full (or the channel is unbuffered and no receiver
+     * is waiting), the awaitable suspends until space becomes available.
+     *
+     * <p>If the channel is already closed, the returned awaitable completes
+     * exceptionally with {@link ChannelClosedException}.
+     *
+     * @param value the value to send; must not be {@code null}
+     * @return an {@link Awaitable} that completes when the send succeeds
+     * @throws NullPointerException if {@code value} is {@code null}
+     */
+    Awaitable<Void> send(T value);
+
+    /**
+     * Receives the next value from the channel asynchronously.
+     *
+     * <p>Values are consumed in FIFO order.  If the channel is buffered,
+     * buffered values are consumed first; a waiting sender is then
+     * admitted to refill the freed buffer slot.  If the channel is
+     * unbuffered, the receiver pairs directly with a waiting sender.
+     *
+     * <p>If the channel is closed and all buffered values have been
+     * drained, the returned awaitable completes exceptionally with
+     * {@link ChannelClosedException}.
+     *
+     * @return an {@link Awaitable} that yields the next value
+     */
+    Awaitable<T> receive();
+
+    /**
+     * Closes the channel.
+     *
+     * <p>Closing is idempotent — calling {@code close()} on an already-closed
+     * channel is a safe no-op that returns {@code false}.  On the first
+     * invocation:
+     * <ol>
+     *   <li>Buffered values that match a waiting receiver are delivered.</li>
+     *   <li>Any remaining waiting receivers (after the buffer is exhausted)
+     *       are failed with {@link ChannelClosedException}.</li>
+     *   <li>All pending senders are failed with
+     *       {@link ChannelClosedException}.</li>
+     * </ol>
+     *
+     * @return {@code true} if this invocation transitioned the channel
+     *         from open to closed; {@code false} if it was already closed
+     */
+    boolean close();
+
+    /**
+     * Returns a read-only {@link AsyncStream} view of this channel.
+     *
+     * <p>The returned stream yields values received from the channel.
+     * When the channel is closed and all buffered values are drained, the
+     * stream signals completion ({@link AsyncStream#moveNext() moveNext()}
+     * returns {@code false}).
+     *
+     * <p>This enables natural {@code for await} iteration:
+     * <pre>
+     * for await (item in channel) {
+     *     process(item)
+     * }
+     * </pre>
+     *
+     * <p><b>Ownership:</b> the stream view does <em>not</em> own the
+     * channel — calling {@link AsyncStream#close() close()} on the stream
+     * is a no-op.  The producer is responsible for closing the channel when
+     * done sending.  This follows the Go convention where the sender, not
+     * the receiver, closes the channel.
+     *
+     * @return an {@link AsyncStream} view backed by this channel
+     */
+    AsyncStream<T> asStream();
+}
diff --git a/src/main/java/groovy/concurrent/AsyncContext.java b/src/main/java/groovy/concurrent/AsyncContext.java
new file mode 100644
index 0000000000..d3f6cf3abf
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncContext.java
@@ -0,0 +1,404 @@
+/*
+ *  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 groovy.lang.Closure;
+import org.apache.groovy.runtime.async.DefaultAsyncContext;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * A lightweight, thread-scoped context map for propagating execution-scoped
+ * state across asynchronous boundaries.
+ *
+ * <h2>Purpose</h2>
+ * <p>{@code AsyncContext} addresses the common production need for carrying
+ * request correlation IDs, tenant identifiers, security principals, trace
+ * metadata, and other small pieces of execution-scoped state across the
+ * thread hops introduced by {@code async}/{@code await}.  It is the Groovy
+ * counterpart of Go's {@code context.Context}, Kotlin's
+ * {@code CoroutineContext}, and C#'s {@code AsyncLocal&lt;T&gt;}.
+ *
+ * <h2>Propagation model</h2>
+ * <p>The current context is stored per thread.  The Groovy async runtime
+ * captures an immutable {@linkplain Snapshot snapshot} when a task or
+ * continuation is scheduled and restores that snapshot when it executes.
+ * This <em>copy-on-spawn</em> model provides two key guarantees:
+ * <ul>
+ *   <li><b>Inheritance</b> — child tasks inherit the parent's context as
+ *       it existed at spawn time.</li>
+ *   <li><b>Isolation</b> — child-side mutations remain invisible to the
+ *       parent and siblings, preventing accidental cross-task leaking.</li>
+ * </ul>
+ *
+ * <h2>Key semantics</h2>
+ * <p>Keys are normalized to non-{@code null} {@link String} values.
+ * Supplying {@code null} as a value is equivalent to removing the key.
+ *
+ * <h2>Thread safety</h2>
+ * <p>Each thread owns its own {@code AsyncContext} instance, managed by
+ * the runtime via {@link org.apache.groovy.runtime.async.ScopedLocal
+ * ScopedLocal}.  On JDK&nbsp;25+, this leverages {@code ScopedValue} for
+ * optimal virtual-thread performance; on earlier JDKs it falls back to
+ * {@code ThreadLocal}.</p>
+ *
+ * <p>Instance methods ({@link #put}, {@link #get}, {@link #remove}) are
+ * <em>not</em> synchronized — they are invoked only on the owning thread.
+ * Static methods ({@link #withSnapshot}, {@link #capture}) use a
+ * scope-based binding pattern, ensuring the previous context is always
+ * reinstated even if the action throws.  This design prevents stale
+ * context from leaking to thread-pool threads between task executions.</p>
+ *
+ * @see Snapshot
+ * @see AsyncScope
+ * @since 6.0.0
+ */
+public interface AsyncContext {
+
+    // ---- Instance methods ------------------------------------------------
+
+    /**
+     * Looks up a value in this context by key.
+     *
+     * @param key the context key; must not be {@code null}
+     * @return the stored value, or {@code null} if absent
+     * @throws NullPointerException if {@code key} is {@code null}
+     */
+    Object get(String key);
+
+    /**
+     * Returns the value for the given key, or the specified default if
+     * the key is not present.
+     * <p>
+     * This method mirrors {@link Map#getOrDefault(Object, Object)} and
+     * is useful when a fallback is needed without a separate
+     * {@link #containsKey(String)} check.
+     *
+     * @param key          the context key; must not be {@code null}
+     * @param defaultValue the value to return if the key is absent
+     * @return the stored value, or {@code defaultValue} if absent
+     * @throws NullPointerException if {@code key} is {@code null}
+     */
+    Object getOrDefault(String key, Object defaultValue);
+
+    /**
+     * If the specified key is not already associated with a value (or is
+     * mapped to {@code null}), attempts to compute its value using the
+     * given mapping function and enters it into this context.
+     * <p>
+     * This method mirrors {@link Map#computeIfAbsent(Object, Function)}
+     * and is useful for one-time, lazy initialization of context entries.
+     *
+     * @param key             the context key; must not be {@code null}
+     * @param mappingFunction the function to compute a value; must not
+     *                        be {@code null}
+     * @return the current (existing or computed) value associated with
+     *         the key, or {@code null} if the computed value is {@code null}
+     * @throws NullPointerException if {@code key} or
+     *         {@code mappingFunction} is {@code null}
+     */
+    Object computeIfAbsent(String key, Function<String, Object> mappingFunction);
+
+    /**
+     * Groovy subscript-read operator: {@code context['traceId']}.
+     *
+     * @param key the context key; must not be {@code null}
+     * @return the stored value, or {@code null} if absent
+     * @throws NullPointerException if {@code key} is {@code null}
+     */
+    Object getAt(String key);
+
+    /**
+     * Stores a value in this context.
+     * <p>
+     * A {@code null} value removes the key.
+     *
+     * @param key   the context key; must not be {@code null}
+     * @param value the value to store, or {@code null} to remove the key
+     * @return the previous value associated with the key, or {@code null}
+     * @throws NullPointerException if {@code key} is {@code null}
+     */
+    Object put(String key, Object value);
+
+    /**
+     * Groovy subscript-write operator: {@code context['traceId'] = 'abc-123'}.
+     *
+     * @param key   the context key; must not be {@code null}
+     * @param value the value to store, or {@code null} to remove the key
+     * @throws NullPointerException if {@code key} is {@code null}
+     */
+    void putAt(String key, Object value);
+
+    /**
+     * Removes a key from this context.
+     *
+     * @param key the context key; must not be {@code null}
+     * @return the removed value, or {@code null} if absent
+     * @throws NullPointerException if {@code key} is {@code null}
+     */
+    Object remove(String key);
+
+    /**
+     * Adds all entries from the supplied map to this context.
+     * <p>
+     * A {@code null} or empty map is silently ignored.
+     * {@code null} values remove the corresponding keys.
+     *
+     * @param entries the entries to add; may be {@code null}
+     */
+    void putAll(Map<?, ?> entries);
+
+    /**
+     * Removes all entries from this context.
+     */
+    void clear();
+
+    /**
+     * Returns {@code true} if the given key is present in this context.
+     *
+     * @param key the context key; must not be {@code null}
+     * @return {@code true} if the key is present
+     * @throws NullPointerException if {@code key} is {@code null}
+     */
+    boolean containsKey(String key);
+
+    /**
+     * Returns the number of entries in this context.
+     *
+     * @return the entry count
+     */
+    int size();
+
+    /**
+     * Returns {@code true} if this context contains no entries.
+     *
+     * @return {@code true} if empty
+     */
+    boolean isEmpty();
+
+    /**
+     * Returns an unmodifiable copy of this context's entries as a {@link Map}.
+     * <p>
+     * The returned map is a point-in-time snapshot: subsequent mutations
+     * to this context are not reflected in the returned map.
+     *
+     * @return an unmodifiable snapshot of the context entries
+     */
+    Map<String, Object> snapshot();
+
+    // ---- Static factory and utility methods ------------------------------
+
+    /**
+     * Returns the live {@code AsyncContext} associated with the current
+     * thread.
+     * <p>
+     * If no context has been explicitly bound, a fresh empty context is
+     * created on first access (lazy initialization).
+     *
+     * @return the current async context; never {@code null}
+     */
+    static AsyncContext current() {
+        return DefaultAsyncContext.current();
+    }
+
+    /**
+     * Captures an immutable {@link Snapshot} of the current thread's
+     * async context.
+     * <p>
+     * The snapshot is a frozen copy: mutations to the live context after
+     * this call do not affect the snapshot.
+     *
+     * @return the captured snapshot; never {@code null}
+     */
+    static Snapshot capture() {
+        return DefaultAsyncContext.capture();
+    }
+
+    /**
+     * Executes the supplier with the given snapshot installed as the
+     * current async context, restoring the previous context afterwards.
+     * <p>
+     * This method is the primary mechanism for context propagation across
+     * thread boundaries.  The Groovy async runtime uses it internally when
+     * resuming continuations and executing scheduled tasks.
+     *
+     * @param snapshot the snapshot to install; must not be {@code null}
+     * @param supplier the action to execute; must not be {@code null}
+     * @param <T>      the result type
+     * @return the supplier's result
+     * @throws NullPointerException if {@code snapshot} or {@code supplier}
+     *                              is {@code null}
+     */
+    static <T> T withSnapshot(Snapshot snapshot, Supplier<T> supplier) {
+        return DefaultAsyncContext.withSnapshot(snapshot, supplier);
+    }
+
+    /**
+     * Executes the runnable with the given snapshot installed as the
+     * current async context, restoring the previous context afterwards.
+     *
+     * @param snapshot the snapshot to install; must not be {@code null}
+     * @param action   the action to execute; must not be {@code null}
+     * @throws NullPointerException if {@code snapshot} or {@code action}
+     *                              is {@code null}
+     */
+    static void withSnapshot(Snapshot snapshot, Runnable action) {
+        DefaultAsyncContext.withSnapshot(snapshot, action);
+    }
+
+    /**
+     * Temporarily overlays the current async context with the supplied
+     * entries for the duration of the closure.
+     * <p>
+     * Any modifications performed inside the closure are scoped to that
+     * dynamic extent and are discarded when the closure returns or throws.
+     *
+     * @param entries the entries to overlay; may be {@code null}
+     * @param action  the closure to execute; must not be {@code null}
+     * @param <T>     the result type
+     * @return the closure's result
+     * @throws NullPointerException if {@code action} is {@code null}
+     */
+    static <T> T with(Map<?, ?> entries, Closure<T> action) {
+        return DefaultAsyncContext.with(entries, action);
+    }
+
+    /**
+     * Creates a new, empty {@code AsyncContext}.
+     *
+     * @return a new empty context
+     */
+    static AsyncContext create() {
+        return new DefaultAsyncContext();
+    }
+
+    /**
+     * Creates a new {@code AsyncContext} seeded with the supplied entries.
+     *
+     * @param initialValues initial context entries; may be {@code null}
+     * @return a new context with the given entries
+     */
+    static AsyncContext create(Map<?, ?> initialValues) {
+        return new DefaultAsyncContext(initialValues);
+    }
+
+    // ---- Snapshot --------------------------------------------------------
+
+    /**
+     * An immutable, point-in-time snapshot of an {@link AsyncContext},
+     * captured at task or continuation registration time and restored when
+     * the task executes.
+     * <p>
+     * Snapshots are the unit of context propagation across async
+     * boundaries.  They are lightweight — internally backed by an
+     * unmodifiable {@link Map} — and safe to share across threads.
+     *
+     * <h2>Usage</h2>
+     * <pre>{@code
+     * AsyncContext.Snapshot snap = AsyncContext.capture();
+     * executor.execute(() -> {
+     *     AsyncContext.withSnapshot(snap, () -> {
+     *         // context restored here
+     *     });
+     * });
+     * }</pre>
+     *
+     * @since 6.0.0
+     */
+    interface Snapshot {
+
+        /**
+         * Returns {@code true} if this snapshot contains the given key.
+         *
+         * @param key the context key; must not be {@code null}
+         * @return {@code true} if present
+         * @throws NullPointerException if {@code key} is {@code null}
+         */
+        boolean containsKey(String key);
+
+        /**
+         * Returns the captured value for the given key.
+         *
+         * @param key the context key; must not be {@code null}
+         * @return the captured value, or {@code null} if absent
+         * @throws NullPointerException if {@code key} is {@code null}
+         */
+        Object get(String key);
+
+        /**
+         * Returns the captured value for the given key, or the specified
+         * default if the key is not present.
+         *
+         * @param key          the context key; must not be {@code null}
+         * @param defaultValue the value to return if the key is absent
+         * @return the captured value, or {@code defaultValue} if absent
+         * @throws NullPointerException if {@code key} is {@code null}
+         */
+        Object getOrDefault(String key, Object defaultValue);
+
+        /**
+         * Returns {@code true} if this snapshot contains no entries.
+         *
+         * @return {@code true} if empty
+         */
+        boolean isEmpty();
+
+        /**
+         * Returns the number of entries in this snapshot.
+         *
+         * @return the entry count
+         */
+        int size();
+
+        /**
+         * Returns the captured entries as an unmodifiable {@link Map}.
+         *
+         * @return the captured entries; never {@code null}
+         */
+        Map<String, Object> asMap();
+
+        /**
+         * Creates a new snapshot by overlaying the supplied entries on top
+         * of this snapshot.
+         * <p>
+         * {@code null} values in the overlay remove the corresponding keys.
+         * A {@code null} or empty overlay returns {@code this} unchanged.
+         *
+         * @param entries the entries to merge; may be {@code null}
+         * @return the merged snapshot; never {@code null}
+         */
+        Snapshot with(Map<?, ?> entries);
+
+        /**
+         * Creates a snapshot from the given map.
+         * <p>
+         * The map is defensively copied; subsequent mutations to the
+         * original map do not affect the snapshot.
+         *
+         * @param values the context entries; must not be {@code null}
+         * @return an immutable snapshot; never {@code null}
+         * @throws NullPointerException if {@code values} is {@code null}
+         */
+        static Snapshot of(Map<String, Object> values) {
+            return DefaultAsyncContext.createSnapshot(values);
+        }
+    }
+}
diff --git a/src/main/java/groovy/concurrent/AsyncScope.java b/src/main/java/groovy/concurrent/AsyncScope.java
new file mode 100644
index 0000000000..48c5d45859
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncScope.java
@@ -0,0 +1,280 @@
+/*
+ *  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 groovy.lang.Closure;
+import groovy.transform.stc.ClosureParams;
+import groovy.transform.stc.SimpleType;
+import org.apache.groovy.runtime.async.AsyncSupport;
+import org.apache.groovy.runtime.async.DefaultAsyncScope;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.function.Supplier;
+
+/**
+ * A structured concurrency scope that ensures all child tasks complete
+ * (or are cancelled) before the scope exits.
+ *
+ * <h2>Design philosophy</h2>
+ * <p>{@code AsyncScope} provides a bounded lifetime for async tasks,
+ * following the <em>structured concurrency</em> model described in
+ * <a href="https://openjdk.org/jeps/453">JEP 453</a> and inspired by
+ * Kotlin's {@code coroutineScope}, Swift's {@code TaskGroup}, and Go's
+ * {@code errgroup.Group}.  Unlike fire-and-forget
+ * {@link AsyncSupport#executeAsync}, tasks launched within a scope are
+ * guaranteed to complete before the scope closes.  This prevents:</p>
+ * <ul>
+ *   <li>Orphaned tasks that outlive their logical parent</li>
+ *   <li>Resource leaks from uncollected async work</li>
+ *   <li>Silent failures from unobserved exceptions</li>
+ * </ul>
+ *
+ * <h2>Failure policy</h2>
+ * <p>By default, the scope uses a <b>fail-fast</b> policy: when any child
+ * task completes exceptionally, all sibling tasks are cancelled
+ * immediately.  The first failure becomes the primary exception;
+ * subsequent failures are added as
+ * {@linkplain Throwable#addSuppressed(Throwable) suppressed} exceptions.</p>
+ *
+ * <h2>Usage in Groovy</h2>
+ * <pre>{@code
+ * import groovy.concurrent.AsyncScope
+ * import groovy.concurrent.Awaitable
+ *
+ * def results = AsyncScope.withScope { scope ->
+ *     def userTask  = scope.async { fetchUser(id) }
+ *     def orderTask = scope.async { fetchOrders(id) }
+ *     return [user: await userTask, orders: await orderTask]
+ * }
+ * // Both tasks guaranteed complete here
+ * }</pre>
+ *
+ * <h2>Thread safety</h2>
+ * <p>All public methods of conforming implementations are thread-safe.
+ * A dedicated lock guards the child task list and the {@code closed}
+ * flag jointly, ensuring that {@link #async(Closure)} and
+ * {@link #close()} cannot race.</p>
+ *
+ * <h2>Thread-scoped state management</h2>
+ * <p>The {@link #withCurrent(AsyncScope, Supplier)} method uses
+ * {@link org.apache.groovy.runtime.async.ScopedLocal ScopedLocal} to
+ * manage the current scope binding.  On JDK&nbsp;25+, this leverages
+ * {@code ScopedValue} for optimal virtual-thread performance; on earlier
+ * JDKs it falls back to {@code ThreadLocal}.</p>
+ *
+ * @see Awaitable
+ * @see AsyncContext
+ * @see AsyncSupport
+ * @since 6.0.0
+ */
+public interface AsyncScope extends AutoCloseable {
+
+    // ---- Instance methods ------------------------------------------------
+
+    /**
+     * Launches a child task within this scope.
+     * <p>
+     * The task's lifetime is bound to the scope: when the scope is closed,
+     * all incomplete child tasks are cancelled.  The child inherits a
+     * snapshot of the current {@link AsyncContext}, but any child-side
+     * context mutations remain isolated from the parent and siblings.
+     * <p>
+     * Implementations must atomically check the closed state and register
+     * the child, preventing the race where a task is submitted to the
+     * executor but never joined by {@link #close()}.
+     *
+     * @param body the async body to execute; must not be {@code null}
+     * @param <T>  the result type
+     * @return an {@link Awaitable} representing the child task
+     * @throws NullPointerException  if {@code body} is {@code null}
+     * @throws IllegalStateException if the scope has already been closed
+     */
+    <T> Awaitable<T> async(Closure<T> body);
+
+    /**
+     * Launches a child task within this scope using a {@link Supplier},
+     * providing idiomatic Java interop.
+     * <p>
+     * Behaves identically to {@link #async(Closure)} in all respects —
+     * context propagation, scope binding, and fail-fast cancellation.
+     *
+     * @param supplier the async body to execute; must not be {@code null}
+     * @param <T>      the result type
+     * @return an {@link Awaitable} representing the child task
+     * @throws NullPointerException  if {@code supplier} is {@code null}
+     * @throws IllegalStateException if the scope has already been closed
+     */
+    <T> Awaitable<T> async(Supplier<T> supplier);
+
+    /**
+     * Returns the number of child tasks currently tracked by this scope.
+     * <p>
+     * Completed children may have been pruned, so this count reflects
+     * active or recently-completed tasks.
+     *
+     * @return the child task count
+     */
+    int getChildCount();
+
+    /**
+     * Cancels all child tasks.  Idempotent — safe to call multiple times.
+     * <p>
+     * Does <em>not</em> close the scope — the scope remains open so that
+     * {@link #close()} can still join all children and collect errors.
+     */
+    void cancelAll();
+
+    /**
+     * Waits for all child tasks to complete, then closes the scope.
+     * <p>
+     * If any child failed, the first failure is rethrown with subsequent
+     * failures as {@linkplain Throwable#addSuppressed(Throwable) suppressed}
+     * exceptions.  Cancelled tasks are silently ignored.
+     * <p>
+     * This method is idempotent: only the first invocation waits for
+     * children; subsequent calls are no-ops.
+     */
+    @Override
+    void close();
+
+    // ---- Static factory and utility methods ------------------------------
+
+    /**
+     * Returns the structured async scope currently bound to this thread,
+     * or {@code null} if execution is not inside
+     * {@link #withScope(Closure)} or a child launched from such a scope.
+     *
+     * @return the current scope, or {@code null}
+     */
+    static AsyncScope current() {
+        return DefaultAsyncScope.current();
+    }
+
+    /**
+     * Executes the supplier with the given scope installed as the current
+     * structured scope, restoring the previous binding afterwards.
+     *
+     * @param scope    the scope to install; may be {@code null}
+     * @param supplier the action to execute; must not be {@code null}
+     * @param <T>      the result type
+     * @return the supplier's result
+     * @throws NullPointerException if {@code supplier} is {@code null}
+     */
+    static <T> T withCurrent(AsyncScope scope, Supplier<T> supplier) {
+        return DefaultAsyncScope.withCurrent(scope, supplier);
+    }
+
+    /**
+     * Void overload of {@link #withCurrent(AsyncScope, Supplier)}.
+     *
+     * @param scope  the scope to install; may be {@code null}
+     * @param action the action to execute; must not be {@code null}
+     * @throws NullPointerException if {@code action} is {@code null}
+     */
+    static void withCurrent(AsyncScope scope, Runnable action) {
+        DefaultAsyncScope.withCurrent(scope, action);
+    }
+
+    /**
+     * Convenience method that creates a scope, executes the given closure
+     * within it, and ensures the scope is closed on exit.
+     * <p>
+     * The closure receives the {@code AsyncScope} as its argument and can
+     * launch child tasks via {@link #async(Closure)}.  The scope is
+     * automatically closed (and all children awaited) when the closure
+     * returns or throws.
+     *
+     * <pre>{@code
+     * def result = AsyncScope.withScope { scope ->
+     *     def a = scope.async { computeA() }
+     *     def b = scope.async { computeB() }
+     *     return [await a, await b]
+     * }
+     * }</pre>
+     *
+     * @param body the closure to execute within the scope;
+     *             must not be {@code null}
+     * @param <T>  the result type
+     * @return the closure's return value
+     * @throws NullPointerException if {@code body} is {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    static <T> T withScope(@ClosureParams(value = SimpleType.class, options = "groovy.concurrent.AsyncScope") Closure<T> body) {
+        return withScope(AsyncSupport.getExecutor(), body);
+    }
+
+    /**
+     * Convenience method that creates a scope with the given executor,
+     * executes the closure, and ensures the scope is closed on exit.
+     *
+     * @param executor the executor for child tasks;
+     *                 must not be {@code null}
+     * @param body     the closure to execute within the scope;
+     *                 must not be {@code null}
+     * @param <T>      the result type
+     * @return the closure's return value
+     * @throws NullPointerException if {@code executor} or {@code body}
+     *                              is {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    static <T> T withScope(Executor executor,
+            @ClosureParams(value = SimpleType.class, options = "groovy.concurrent.AsyncScope") Closure<T> body) {
+        Objects.requireNonNull(body, "body must not be null");
+        try (AsyncScope scope = AsyncScope.create(executor)) {
+            return withCurrent(scope, () -> body.call(scope));
+        }
+    }
+
+    /**
+     * Creates a new scope with the default async executor and fail-fast
+     * enabled.
+     *
+     * @return a new scope
+     */
+    static AsyncScope create() {
+        return new DefaultAsyncScope();
+    }
+
+    /**
+     * Creates a new scope with the given executor and fail-fast enabled.
+     *
+     * @param executor the executor for child tasks;
+     *                 must not be {@code null}
+     * @return a new scope
+     * @throws NullPointerException if {@code executor} is {@code null}
+     */
+    static AsyncScope create(Executor executor) {
+        return new DefaultAsyncScope(executor);
+    }
+
+    /**
+     * Creates a new scope with the given executor and failure policy.
+     *
+     * @param executor the executor for child tasks;
+     *                 must not be {@code null}
+     * @param failFast if {@code true}, cancel all siblings when any
+     *                 child fails
+     * @return a new scope
+     * @throws NullPointerException if {@code executor} is {@code null}
+     */
+    static AsyncScope create(Executor executor, boolean failFast) {
+        return new DefaultAsyncScope(executor, failFast);
+    }
+}
diff --git a/src/main/java/groovy/concurrent/AsyncStream.java b/src/main/java/groovy/concurrent/AsyncStream.java
new file mode 100644
index 0000000000..ce7017eefa
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AsyncStream.java
@@ -0,0 +1,146 @@
+/*
+ *  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 for producing and consuming
+ * values that arrive over time.
+ * <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>Viewing a {@link AsyncChannel} as a stream via {@link AsyncChannel#asStream()} —
+ *       each {@code moveNext()} call maps to a {@code receive()}, and
+ *       {@link ChannelClosedException} is translated to end-of-stream.
+ *       This conversion is registered automatically, so {@code for await (item in channel)}
+ *       works out of the box.</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> extends AutoCloseable {
+
+    /**
+     * 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();
+
+    /**
+     * Closes the stream and releases any associated resources.
+     * <p>
+     * The default implementation is a no-op.  Implementations that bridge to
+     * generators, publishers, or other resource-owning sources may override
+     * this to propagate cancellation upstream.  Compiler-generated
+     * {@code for await} loops invoke {@code close()} automatically from a
+     * {@code finally} block, including on early {@code break}, {@code return},
+     * and exceptional exit.
+     *
+     * @since 6.0.0
+     */
+    @Override
+    default void close() {
+    }
+
+    /**
+     * Converts the given source to an {@code AsyncStream}.
+     * <p>
+     * If the source is already an {@code AsyncStream}, it is returned as-is.
+     * Otherwise, the {@link AwaitableAdapterRegistry} is consulted to find a
+     * suitable adapter. Built-in adapters handle {@link Iterable} and
+     * {@link java.util.Iterator}; the auto-discovered {@code FlowPublisherAdapter}
+     * handles {@link java.util.concurrent.Flow.Publisher}; third-party frameworks
+     * can register additional adapters via the registry.
+     * <p>
+     * This is the recommended entry point for converting external collection or
+     * reactive types to {@code AsyncStream}:
+     * <pre>
+     * AsyncStream&lt;String&gt; stream = AsyncStream.from(myList)
+     * AsyncStream&lt;Integer&gt; stream2 = AsyncStream.from(myFlowPublisher)
+     * </pre>
+     *
+     * @param source the source object; must not be {@code null}
+     * @param <T>    the element type
+     * @return an async stream backed by the source
+     * @throws IllegalArgumentException if {@code source} is {@code null}
+     *         or no adapter supports the source type
+     * @see AwaitableAdapterRegistry#toAsyncStream(Object)
+     * @since 6.0.0
+     */
+    @SuppressWarnings("unchecked")
+    static <T> AsyncStream<T> from(Object source) {
+        return AwaitableAdapterRegistry.toAsyncStream(source);
+    }
+
+    /**
+     * Returns an empty {@code AsyncStream} that completes immediately.
+     */
+    @SuppressWarnings("unchecked")
+    static <T> AsyncStream<T> empty() {
+        return (AsyncStream<T>) EMPTY;
+    }
+
+    /**
+     * Cached awaitable for {@code moveNext()} returning {@code true}.
+     * Eliminates per-call allocation on the hot path.  Shared by all
+     * {@code AsyncStream} implementations (e.g. via
+     * {@link org.apache.groovy.runtime.async.AbstractAsyncStream AbstractAsyncStream}).
+     */
+    Awaitable<Boolean> MOVE_NEXT_TRUE = Awaitable.of(Boolean.TRUE);
+
+    /**
+     * Cached awaitable for {@code moveNext()} returning {@code false}.
+     * Eliminates per-call allocation on the stream-end path.
+     */
+    Awaitable<Boolean> MOVE_NEXT_FALSE = Awaitable.of(Boolean.FALSE);
+
+    /**
+     * Singleton empty stream instance.
+     * <p>
+     * This is an implementation detail backing {@link #empty()}.
+     * User code should call {@code AsyncStream.empty()} rather than
+     * referencing this field directly.
+     */
+    AsyncStream<Object> EMPTY = new AsyncStream<>() {
+        @Override public Awaitable<Boolean> moveNext() { return MOVE_NEXT_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..f7f77879b9
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitResult.java
@@ -0,0 +1,202 @@
+/*
+ *  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.  Returned by
+ * {@link Awaitable#allSettled(Object...) Awaitable.allSettled()} to
+ * report each individual task's result without short-circuiting on
+ * failure.
+ * <p>
+ * An {@code AwaitResult} is either a {@linkplain #isSuccess() success}
+ * carrying a value, or a {@linkplain #isFailure() failure} carrying a
+ * {@link Throwable}.
+ * <p>
+ * This type follows the <em>value object</em> pattern: two instances
+ * are {@linkplain #equals(Object) equal} if and only if they have
+ * the same success/failure state and carry equal values or errors.
+ * Immutability is enforced — all fields are final and no mutating
+ * methods are exposed.
+ * <p>
+ * Functional composition is supported via {@link #map(Function)},
+ * enabling transformation chains without unwrapping:
+ * <pre>
+ * AwaitResult&lt;Integer&gt; length = AwaitResult.success("hello").map(String::length)
+ * </pre>
+ * <p>
+ * Inspired by Kotlin's {@code Result}, Rust's {@code Result<T, E>},
+ * and C#'s pattern of structured success/error responses.
+ *
+ * @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.
+     *
+     * @param value the computation result (may be {@code null})
+     * @param <T>   the value type
+     * @return a success result wrapping the value
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> AwaitResult<T> success(Object value) {
+        return new AwaitResult<>((T) value, null, true);
+    }
+
+    /**
+     * Creates a failure result with the given exception.
+     *
+     * @param error the exception that caused the failure; must not be {@code null}
+     * @param <T>   the value type (never actually used, since the result is a failure)
+     * @return a failure result wrapping the exception
+     * @throws NullPointerException if {@code error} is {@code null}
+     */
+    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.
+     *
+     * @param fallback the function to apply to the error if this result
+     *                 is a failure; must not be {@code null}
+     * @return the value, or the fallback function's result
+     */
+    public T getOrElse(Function<Throwable, ? extends T> fallback) {
+        return success ? value : fallback.apply(error);
+    }
+
+    /**
+     * Transforms a successful result's value using the given function.
+     * If this result is a failure, the error is propagated unchanged.
+     * <p>
+     * This is the functor {@code map} operation, enabling value
+     * transformation without explicit unwrapping:
+     * <pre>
+     * AwaitResult&lt;String&gt;  name   = AwaitResult.success("Groovy")
+     * AwaitResult&lt;Integer&gt; length = name.map(String::length)
+     * assert length.value == 6
+     * </pre>
+     *
+     * @param fn  the mapping function; must not be {@code null}
+     * @param <U> the type of the mapped value
+     * @return a new success result with the mapped value, or the
+     *         original failure unchanged
+     * @throws NullPointerException if {@code fn} is {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    public <U> AwaitResult<U> map(Function<? super T, ? extends U> fn) {
+        Objects.requireNonNull(fn, "mapping function must not be null");
+        if (!success) {
+            return (AwaitResult<U>) this;
+        }
+        return AwaitResult.success(fn.apply(value));
+    }
+
+    /**
+     * Compares this result to another object for equality.
+     * <p>
+     * Two {@code AwaitResult} instances are equal if and only if they
+     * have the same success/failure state and carry
+     * {@linkplain Objects#equals(Object, Object) equal} values or errors.
+     *
+     * @param o the object to compare with
+     * @return {@code true} if the given object is an equal {@code AwaitResult}
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof AwaitResult<?> that)) return false;
+        if (success != that.success) return false;
+        return success
+                ? Objects.equals(value, that.value)
+                : Objects.equals(error, that.error);
+    }
+
+    /**
+     * Returns a hash code consistent with {@link #equals(Object)}.
+     *
+     * @return the hash code
+     */
+    @Override
+    public int hashCode() {
+        return success
+                ? Objects.hash(true, value)
+                : Objects.hash(false, error);
+    }
+
+    /**
+     * Returns a human-readable representation of this result:
+     * {@code AwaitResult.Success[value]} or {@code AwaitResult.Failure[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..c68e4c4da4
--- /dev/null
+++ b/src/main/java/groovy/concurrent/Awaitable.java
@@ -0,0 +1,598 @@
+/*
+ *  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 groovy.lang.Closure;
+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.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Core abstraction for asynchronous computations in Groovy.
+ * <p>
+ * {@code Awaitable} represents a computation that may not have completed
+ * yet.  It 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.
+ * <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)} — waits for all
+ *       tasks to complete, returning their results in order</li>
+ *   <li>{@link #any(Object...) Awaitable.any(a, b)} — returns the result
+ *       of the first task to complete</li>
+ *   <li>{@link #allSettled(Object...) Awaitable.allSettled(a, b)} — waits
+ *       for all tasks to settle (succeed or fail), returning an
+ *       {@link AwaitResult} list</li>
+ *   <li>{@link #delay(long) Awaitable.delay(ms)} — completes after a
+ *       non-blocking delay</li>
+ *   <li>{@link #orTimeoutMillis(Object, long) Awaitable.orTimeoutMillis(task, ms)} —
+ *       fails with {@link java.util.concurrent.TimeoutException} if the task
+ *       does not complete in time</li>
+ *   <li>{@link #completeOnTimeoutMillis(Object, Object, long)
+ *       Awaitable.completeOnTimeoutMillis(task, fallback, ms)} —
+ *       uses a fallback value when the timeout expires</li>
+ * </ul>
+ * <p>
+ * <b>Static factories and conversion:</b>
+ * <ul>
+ *   <li>{@link #from(Object) Awaitable.from(source)} — converts any supported
+ *       async type (CompletableFuture, CompletionStage, Future, Flow.Publisher, etc.)
+ *       to an {@code Awaitable}</li>
+ *   <li>{@link #of(Object) Awaitable.of(value)} — wraps an already-available
+ *       value in a completed {@code Awaitable}</li>
+ *   <li>{@link #failed(Throwable) Awaitable.failed(error)} — wraps an
+ *       exception in an immediately-failed {@code Awaitable}</li>
+ *   <li>{@link #go(Closure) Awaitable.go { ... }} — lightweight task spawn</li>
+ *   <li>{@link AsyncChannel#create() AsyncChannel.create()} / {@link AsyncChannel#create(int) AsyncChannel.create(n)} — buffered or unbuffered channels</li>
+ * </ul>
+ * <p>
+ * <b>Instance continuations</b> provide ergonomic composition without exposing
+ * raw {@link CompletableFuture} APIs:
+ * <ul>
+ *   <li>{@link #then(Function)} and {@link #thenCompose(Function)} for success chaining</li>
+ *   <li>{@link #thenAccept(Consumer)} for side-effecting continuations</li>
+ *   <li>{@link #exceptionally(Function)}, {@link #whenComplete(BiConsumer)},
+ *       and {@link #handle(BiFunction)} for failure/completion handling</li>
+ *   <li>{@link #orTimeout(long, TimeUnit)} and
+ *       {@link #completeOnTimeout(Object, long, TimeUnit)} for deadline composition</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.
+     *
+     * @return the computed result
+     * @throws InterruptedException if the calling thread is interrupted while waiting
+     * @throws ExecutionException if the computation completed exceptionally
+     */
+    T get() throws InterruptedException, ExecutionException;
+
+    /**
+     * Blocks until the computation completes or the timeout expires.
+     *
+     * @param timeout the maximum time to wait
+     * @param unit the time unit of the timeout argument
+     * @return the computed result
+     * @throws InterruptedException if the calling thread is interrupted while waiting
+     * @throws ExecutionException if the computation completed exceptionally
+     * @throws TimeoutException if the wait timed out
+     */
+    T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
+
+    /**
+     * Returns {@code true} if the computation has completed (normally,
+     * exceptionally, or via cancellation).
+     *
+     * @return {@code true} if complete
+     */
+    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}.
+     *
+     * @return {@code true} if the computation was successfully cancelled
+     */
+    boolean cancel();
+
+    /**
+     * Returns {@code true} if the computation was cancelled before completing normally.
+     *
+     * @return {@code true} if cancelled
+     */
+    boolean isCancelled();
+
+    /**
+     * Returns {@code true} if this computation completed exceptionally
+     * (including cancellation).
+     *
+     * @return {@code true} if completed with an error or 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.
+     *
+     * @param fn the mapping function
+     * @param <U> the type of the mapped result
+     * @return a new awaitable holding the mapped result
+     */
+    <U> Awaitable<U> then(Function<? super T, ? extends U> fn);
+
+    /**
+     * Returns a new {@code Awaitable} produced by applying the given async
+     * function to this awaitable's result, flattening the nested {@code Awaitable}.
+     * This is the monadic {@code flatMap} operation for awaitables.
+     *
+     * @param fn the async mapping function that returns an {@code Awaitable}
+     * @param <U> the type of the inner awaitable's result
+     * @return a new awaitable holding the inner result
+     */
+    <U> Awaitable<U> thenCompose(Function<? super T, ? extends Awaitable<U>> fn);
+
+    /**
+     * Returns a new {@code Awaitable} that, when this one completes normally,
+     * invokes the given consumer and completes with {@code null}.
+     * <p>
+     * Useful at API boundaries where you need to attach logging, metrics, or
+     * other side effects without blocking for the result.
+     *
+     * @param action the side-effecting consumer to invoke on success
+     * @return a new awaitable that completes after the action runs
+     * @since 6.0.0
+     */
+    default Awaitable<Void> thenAccept(Consumer<? super T> action) {
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        return GroovyPromise.of(toCompletableFuture().thenAccept(value ->
+                AsyncContext.withSnapshot(contextSnapshot, () -> {
+                    action.accept(value);
+                    return null;
+                })));
+    }
+
+    /**
+     * Returns a new {@code Awaitable} that, if this one completes exceptionally,
+     * applies the given function to the exception to produce a recovery value.
+     * The throwable passed to the function is deeply unwrapped to strip JDK
+     * wrapper layers.
+     *
+     * @param fn the recovery function
+     * @return a new awaitable that recovers from failures
+     */
+    Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn);
+
+    /**
+     * Returns a new {@code Awaitable} that invokes the given action when this
+     * computation completes, regardless of success or failure.
+     * <p>
+     * The supplied throwable is transparently unwrapped so handlers see the
+     * original failure rather than {@link ExecutionException} /
+     * {@link java.util.concurrent.CompletionException} wrappers.
+     *
+     * @param action the completion callback receiving the result or failure
+     * @return a new awaitable that completes with the original result
+     * @since 6.0.0
+     */
+    default Awaitable<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) {
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        return GroovyPromise.of(toCompletableFuture().whenComplete((value, error) ->
+                AsyncContext.withSnapshot(contextSnapshot, () -> {
+                    action.accept(value, error == null ? null : AsyncSupport.deepUnwrap(error));
+                    return null;
+                })));
+    }
+
+    /**
+     * Returns a new {@code Awaitable} that handles both the successful and the
+     * exceptional completion paths in a single continuation.
+     * <p>
+     * The supplied throwable is transparently unwrapped so the handler sees the
+     * original failure.  This provides a single place for success/failure
+     * projection, combining both paths in one callback.
+     *
+     * @param fn the handler receiving either the value or the failure
+     * @param <U> the projected result type
+     * @return a new awaitable holding the handler's result
+     * @since 6.0.0
+     */
+    default <U> Awaitable<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) {
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        return GroovyPromise.of(toCompletableFuture().handle((value, error) ->
+                AsyncContext.withSnapshot(contextSnapshot,
+                        () -> fn.apply(value, error == null ? null : AsyncSupport.deepUnwrap(error)))));
+    }
+
+    /**
+     * Returns a new {@code Awaitable} that fails with {@link TimeoutException}
+     * if this computation does not complete within the specified milliseconds.
+     * <p>
+     * Unlike {@link #get(long, TimeUnit)}, this is a non-blocking, composable
+     * timeout combinator: it returns another {@code Awaitable} that can itself
+     * be awaited, chained, or passed to {@link #all(Object...)} / {@link #any(Object...)}.
+     *
+     * @param timeoutMillis the timeout duration in milliseconds
+     * @return a new awaitable with timeout semantics
+     * @since 6.0.0
+     */
+    default Awaitable<T> orTimeoutMillis(long timeoutMillis) {
+        return Awaitable.orTimeout(this, timeoutMillis, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Returns a new {@code Awaitable} that fails with {@link TimeoutException}
+     * if this computation does not complete within the specified duration.
+     *
+     * @param duration the timeout duration
+     * @param unit the time unit
+     * @return a new awaitable with timeout semantics
+     * @since 6.0.0
+     */
+    default Awaitable<T> orTimeout(long duration, TimeUnit unit) {
+        return Awaitable.orTimeout(this, duration, unit);
+    }
+
+    /**
+     * Returns a new {@code Awaitable} that completes with the supplied fallback
+     * value if this computation does not finish before the timeout expires.
+     *
+     * @param fallback the value to use when the timeout expires
+     * @param timeoutMillis the timeout duration in milliseconds
+     * @return a new awaitable that yields either the original result or the fallback
+     * @since 6.0.0
+     */
+    default Awaitable<T> completeOnTimeoutMillis(T fallback, long timeoutMillis) {
+        return Awaitable.completeOnTimeout(this, fallback, timeoutMillis, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Returns a new {@code Awaitable} that completes with the supplied fallback
+     * value if this computation does not finish before the timeout expires.
+     *
+     * @param fallback the value to use when the timeout expires
+     * @param duration the timeout duration
+     * @param unit the time unit
+     * @return a new awaitable that yields either the original result or the fallback
+     * @since 6.0.0
+     */
+    default Awaitable<T> completeOnTimeout(T fallback, long duration, TimeUnit unit) {
+        return Awaitable.completeOnTimeout(this, fallback, duration, unit);
+    }
+
+    /**
+     * Converts this {@code Awaitable} to a JDK {@link CompletableFuture}
+     * for interoperability with APIs that require it.
+     *
+     * @return a {@code CompletableFuture} representing this computation
+     */
+    CompletableFuture<T> toCompletableFuture();
+
+    // ---- Static factories ----
+
+    /**
+     * Converts the given source to an {@code Awaitable}.
+     * <p>
+     * If the source is already an {@code Awaitable}, it is returned as-is.
+     * Otherwise, the {@link AwaitableAdapterRegistry} is consulted to find a
+     * suitable adapter. Built-in adapters handle {@link CompletableFuture},
+     * {@link java.util.concurrent.CompletionStage}, {@link java.util.concurrent.Future},
+     * and {@link java.util.concurrent.Flow.Publisher}; third-party frameworks
+     * can register additional adapters via the registry.
+     * <p>
+     * This is the recommended entry point for converting external async types
+     * to {@code Awaitable}:
+     * <pre>
+     * Awaitable&lt;String&gt; aw = Awaitable.from(someCompletableFuture)
+     * Awaitable&lt;Integer&gt; aw2 = Awaitable.from(someReactorMono)
+     * </pre>
+     *
+     * @param source the source object; must not be {@code null}
+     * @param <T>    the result type
+     * @return an awaitable backed by the source
+     * @throws IllegalArgumentException if {@code source} is {@code null}
+     *         or no adapter supports the source type
+     * @see AwaitableAdapterRegistry#toAwaitable(Object)
+     * @since 6.0.0
+     */
+    static <T> Awaitable<T> from(Object source) {
+        return AwaitableAdapterRegistry.toAwaitable(source);
+    }
+
+    /**
+     * Returns an already-completed {@code Awaitable} with the given value.
+     * Useful for returning a pre-computed result from an API that requires
+     * an {@code Awaitable} return type.
+     *
+     * @param value the result value (may be {@code null})
+     * @param <T>   the result type
+     * @return a completed awaitable
+     */
+    static <T> Awaitable<T> of(T value) {
+        return new GroovyPromise<>(CompletableFuture.completedFuture(value));
+    }
+
+    /**
+     * Returns an already-failed {@code Awaitable} with the given exception.
+     * <p>
+     * Useful for signaling a synchronous error from an API that returns
+     * {@code Awaitable}, without spawning a thread:
+     * <pre>
+     * if (id &lt; 0) return Awaitable.failed(new IllegalArgumentException("negative id"))
+     * </pre>
+     *
+     * @param error the exception to wrap; must not be {@code null}
+     * @param <T>   the nominal result type (never produced, since the awaitable is failed)
+     * @return an immediately-failed awaitable
+     * @throws NullPointerException if {@code error} is {@code null}
+     */
+    static <T> Awaitable<T> failed(Throwable error) {
+        return new GroovyPromise<>(CompletableFuture.failedFuture(error));
+    }
+
+    /**
+     * Spawns a lightweight task.
+     * <p>
+     * Inside an {@link AsyncScope}, the new task is automatically attached to
+     * the current structured scope — the scope will wait for it on close,
+     * and fail-fast cancellation applies.  Outside a scope it behaves like
+     * a detached async task.
+     * <p>
+     * The child task inherits a snapshot of the current
+     * {@link groovy.concurrent.AsyncContext AsyncContext}, ensuring that
+     * request IDs, tenant IDs, and other execution-scoped metadata
+     * propagate automatically.
+     *
+     * @param closure the task body
+     * @param <T>     the result type
+     * @return an awaitable representing the spawned task
+     * @see AsyncScope#async(groovy.lang.Closure)
+     * @since 6.0.0
+     */
+    static <T> Awaitable<T> go(Closure<T> closure) {
+        return AsyncSupport.go(closure);
+    }
+
+    // ---- Combinators ----
+
+    /**
+     * Returns an {@code Awaitable} that completes when all given sources
+     * complete successfully, with a list of their results in order.
+     * <p>
+     * Like JavaScript's {@code Promise.all()}, the combined awaitable fails as
+     * soon as the first source fails. Remaining sources are not cancelled
+     * automatically; cancel them explicitly if that is required by your workflow.
+     * <p>
+     * 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.
+     * <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>
+     * 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 ----
+
+    /**
+     * Returns an {@code Awaitable} that completes after the specified delay.
+     * Does not block any thread; the delay is handled by a scheduled executor.
+     * <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);
+    }
+
+    // ---- Timeout combinators ----
+
+    /**
+     * Adapts the given source to an {@code Awaitable} and applies a non-blocking
+     * fail-fast timeout.  Returns a new awaitable that fails with
+     * {@link TimeoutException} if the source does not complete before the
+     * deadline elapses.
+     * <p>
+     * The source may be a Groovy {@link Awaitable}, a JDK
+     * {@link CompletableFuture}/{@link java.util.concurrent.CompletionStage},
+     * or any type supported by {@link AwaitableAdapterRegistry}.  This provides
+     * a concise timeout combinator that returns another awaitable rather than
+     * requiring structural timeout blocks.
+     *
+     * @param source        the async source to time out
+     * @param timeoutMillis the timeout duration in milliseconds
+     * @param <T>           the result type
+     * @return a new awaitable with timeout semantics
+     * @since 6.0.0
+     */
+    static <T> Awaitable<T> orTimeoutMillis(Object source, long timeoutMillis) {
+        return AsyncSupport.orTimeout(source, timeoutMillis, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Adapts the given source and applies a non-blocking fail-fast timeout
+     * with explicit {@link TimeUnit}.
+     *
+     * @param source   the async source to time out
+     * @param duration the timeout duration
+     * @param unit     the time unit
+     * @param <T>      the result type
+     * @return a new awaitable with timeout semantics
+     * @since 6.0.0
+     */
+    static <T> Awaitable<T> orTimeout(Object source, long duration, TimeUnit unit) {
+        return AsyncSupport.orTimeout(source, duration, unit);
+    }
+
+    /**
+     * Adapts the given source and returns a new awaitable that yields the
+     * supplied fallback value if the timeout expires first.
+     *
+     * @param source        the async source to wait for
+     * @param fallback      the fallback value to use on timeout
+     * @param timeoutMillis the timeout duration in milliseconds
+     * @param <T>           the result type
+     * @return a new awaitable yielding either the original result or the fallback
+     * @since 6.0.0
+     */
+    static <T> Awaitable<T> completeOnTimeoutMillis(Object source, T fallback, long timeoutMillis) {
+        return AsyncSupport.completeOnTimeout(source, fallback, timeoutMillis, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Adapts the given source and returns a new awaitable that yields the
+     * supplied fallback value if the timeout expires first.
+     *
+     * @param source   the async source to wait for
+     * @param fallback the fallback value to use on timeout
+     * @param duration the timeout duration
+     * @param unit     the time unit
+     * @param <T>      the result type
+     * @return a new awaitable yielding either the original result or the fallback
+     * @since 6.0.0
+     */
+    static <T> Awaitable<T> completeOnTimeout(Object source, T fallback, long duration, TimeUnit unit) {
+        return AsyncSupport.completeOnTimeout(source, fallback, duration, unit);
+    }
+
+    // ---- Executor configuration ----
+
+    /**
+     * Returns the current executor used for {@code async} operations.
+     * <p>
+     * On JDK&nbsp;21+, the default is a virtual-thread-per-task executor.
+     * On JDK&nbsp;17–20, a bounded cached daemon thread pool is used
+     * (size controlled by the {@code groovy.async.parallelism} system property,
+     * default {@code 256}).
+     * <p>
+     * This method is thread-safe and may be called from any thread.
+     *
+     * @return the current executor, never {@code null}
+     * @see #setExecutor(Executor)
+     */
+    static Executor getExecutor() {
+        return AsyncSupport.getExecutor();
+    }
+
+    /**
+     * Sets the executor to use for {@code async} operations.
+     * <p>
+     * Pass {@code null} to reset to the default executor.  The change
+     * takes effect immediately for all subsequent {@code async} method
+     * invocations; tasks already in flight continue using the executor
+     * that launched them.
+     * <p>
+     * This method is thread-safe and may be called from any thread.
+     *
+     * @param executor the executor to use, or {@code null} to restore
+     *                 the default executor
+     * @see #getExecutor()
+     */
+    static void setExecutor(Executor executor) {
+        AsyncSupport.setExecutor(executor);
+    }
+
+    /**
+     * Returns {@code true} if the running JVM supports virtual threads (JDK&nbsp;21+).
+     * <p>
+     * When virtual threads are available, the default executor uses a
+     * virtual-thread-per-task strategy that scales to millions of
+     * concurrent tasks with minimal memory overhead.
+     *
+     * @return {@code true} if virtual threads are available
+     */
+    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..148440a595
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitableAdapter.java
@@ -0,0 +1,86 @@
+/*
+ *  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}.  Defaults to {@code false}; override if
+     * this adapter supports multi-value stream conversion (e.g., for Reactor
+     * {@code Flux} or RxJava {@code Observable}).
+     *
+     * @param type the source class to check
+     * @return {@code true} if this adapter can produce an {@code 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}.
+     *
+     * @param source the source object (e.g., a Reactor {@code Flux})
+     * @param <T>    the element type
+     * @return an async stream backed by the source
+     * @throws UnsupportedOperationException if not overridden (default)
+     */
+    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..7db66e9132
--- /dev/null
+++ b/src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java
@@ -0,0 +1,350 @@
+/*
+ *  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.Objects;
+import java.util.ServiceLoader;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+
+/**
+ * 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>
+ * </ul>
+ * <p>
+ * JDK {@link java.util.concurrent.Flow.Publisher} support is provided by
+ * the separately registered
+ * {@link org.apache.groovy.runtime.async.FlowPublisherAdapter}, which is
+ * auto-discovered via ServiceLoader.  Third-party frameworks (Reactor,
+ * RxJava, etc.) may register their own higher-priority adapters.
+ * <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<>();
+
+    /**
+     * Per-class adapter cache for {@link #toAwaitable(Object)}.
+     * Uses {@link ClassValue} for lock-free, GC-friendly per-class memoization
+     * that does not prevent class unloading — superior to
+     * {@code ConcurrentHashMap<Class<?>, V>} which holds strong references
+     * that can cause ClassLoader leaks in container environments.
+     * <p>
+     * Rebuilt via volatile reference swap when adapters are registered or
+     * removed (an extremely low-frequency operation).
+     */
+    private static volatile ClassValue<AwaitableAdapter> awaitableCache = buildAwaitableCache();
+
+    /**
+     * Per-class adapter cache for {@link #toAsyncStream(Object)}.
+     * Same caching strategy as {@link #awaitableCache}.
+     */
+    private static volatile ClassValue<AwaitableAdapter> streamCache = buildStreamCache();
+
+    /**
+     * 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.
+     * <p>
+     * Invalidates the per-class adapter caches so that subsequent lookups
+     * re-evaluate adapter priority order.
+     *
+     * @param adapter the adapter to register; must not be {@code null}
+     * @return an {@link AutoCloseable} that, when closed, removes this adapter
+     *         from the registry. Useful for test isolation.
+     */
+    public static AutoCloseable register(AwaitableAdapter adapter) {
+        Objects.requireNonNull(adapter, "adapter must not be null");
+        ADAPTERS.add(0, adapter);
+        invalidateCaches();
+        return () -> { ADAPTERS.remove(adapter); invalidateCaches(); };
+    }
+
+    /**
+     * Removes the given adapter from the registry.
+     * <p>
+     * Invalidates the per-class adapter caches so that subsequent lookups
+     * no longer consider the removed adapter.
+     *
+     * @param adapter the adapter to remove
+     * @return {@code true} if the adapter was found and removed
+     */
+    public static boolean unregister(AwaitableAdapter adapter) {
+        Objects.requireNonNull(adapter, "adapter must not be null");
+        boolean removed = ADAPTERS.remove(adapter);
+        if (removed) {
+            invalidateCaches();
+        }
+        return removed;
+    }
+
+    /**
+     * 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.
+     * <p>
+     * Uses a per-class {@link ClassValue} cache to avoid repeated linear
+     * scans of the adapter list on the hot path.  The first lookup for a
+     * given class performs a linear scan; subsequent lookups for the same
+     * class are O(1).
+     * <p>
+     * <b>Tip:</b> user code should generally prefer {@link Awaitable#from(Object)},
+     * which delegates to this method but is more discoverable from the
+     * {@code Awaitable} type itself.
+     *
+     * @param source the source object; must not be {@code null}
+     * @param <T>    the result type
+     * @return an awaitable backed by the source
+     * @throws IllegalArgumentException if {@code source} is {@code null}
+     *         or no adapter supports the source type
+     * @see Awaitable#from(Object)
+     */
+    @SuppressWarnings("unchecked")
+    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();
+        AwaitableAdapter adapter = awaitableCache.get(type);
+        if (adapter != null) {
+            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.
+     * {@link AsyncChannel} instances are handled as a built-in special case via
+     * {@link AsyncChannel#asStream()}, enabling transparent {@code for await}
+     * iteration over channels.
+     * <p>
+     * Uses a per-class {@link ClassValue} cache to avoid repeated linear
+     * scans of the adapter list on the hot path.
+     * <p>
+     * <b>Tip:</b> user code should generally prefer {@link AsyncStream#from(Object)},
+     * which delegates to this method but is more discoverable from the
+     * {@code AsyncStream} type itself.
+     *
+     * @param source the source object; must not be {@code null}
+     * @param <T>    the element type
+     * @return an async stream backed by the source
+     * @throws IllegalArgumentException if {@code source} is {@code null}
+     *         or no adapter supports the source type
+     * @see AsyncStream#from(Object)
+     */
+    @SuppressWarnings("unchecked")
+    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;
+        if (source instanceof AsyncChannel) return ((AsyncChannel<T>) source).asStream();
+        Class<?> type = source.getClass();
+        AwaitableAdapter adapter = streamCache.get(type);
+        if (adapter != null) {
+            return adapter.toAsyncStream(source);
+        }
+        throw new IllegalArgumentException(
+                "No AsyncStream adapter found for type: " + type.getName()
+                        + ". Register one via AwaitableAdapterRegistry.register() or ServiceLoader.");
+    }
+
+    // ---- Cache management ------------------------------------------------
+
+    private static ClassValue<AwaitableAdapter> buildAwaitableCache() {
+        return new ClassValue<>() {
+            @Override
+            protected AwaitableAdapter computeValue(Class<?> type) {
+                for (AwaitableAdapter adapter : ADAPTERS) {
+                    if (adapter.supportsAwaitable(type)) {
+                        return adapter;
+                    }
+                }
+                return null;
+            }
+        };
+    }
+
+    private static ClassValue<AwaitableAdapter> buildStreamCache() {
+        return new ClassValue<>() {
+            @Override
+            protected AwaitableAdapter computeValue(Class<?> type) {
+                for (AwaitableAdapter adapter : ADAPTERS) {
+                    if (adapter.supportsAsyncStream(type)) {
+                        return adapter;
+                    }
+                }
+                return null;
+            }
+        };
+    }
+
+    /**
+     * Rebuilds the per-class adapter caches after adapter registration
+     * changes.  Uses volatile reference swap — safe because
+     * {@link ClassValue} instances are immutable once constructed, and
+     * concurrent readers of the old cache see a consistent (if stale)
+     * snapshot until the new cache is published.
+     */
+    private static void invalidateCaches() {
+        awaitableCache = buildAwaitableCache();
+        streamCache = buildStreamCache();
+    }
+
+    /**
+     * Built-in adapter handling JDK {@link CompletableFuture}, {@link CompletionStage},
+     * {@link Future}, 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.
+     */
+    private static class BuiltInAdapter implements AwaitableAdapter {
+
+        @Override
+        public boolean supportsAwaitable(Class<?> type) {
+            return CompletionStage.class.isAssignableFrom(type)
+                    || Future.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 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);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> AsyncStream<T> toAsyncStream(Object source) {
+            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;
+                }
+            };
+        }
+
+        private static <T> void completeFrom(CompletableFuture<T> cf, Future<T> future) {
+            try {
+                cf.complete(future.get());
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                CancellationException ce = new CancellationException("Interrupted while adapting Future");
+                ce.initCause(e);
+                cf.completeExceptionally(ce);
+            } catch (ExecutionException e) {
+                cf.completeExceptionally(e.getCause() != null ? e.getCause() : e);
+            } catch (Exception e) {
+                cf.completeExceptionally(e);
+            }
+        }
+    }
+}
diff --git a/src/main/java/groovy/concurrent/ChannelClosedException.java b/src/main/java/groovy/concurrent/ChannelClosedException.java
new file mode 100644
index 0000000000..6a963b24af
--- /dev/null
+++ b/src/main/java/groovy/concurrent/ChannelClosedException.java
@@ -0,0 +1,77 @@
+/*
+ *  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;
+
+/**
+ * Thrown when an {@link AsyncChannel} operation is attempted after the channel
+ * has been closed.
+ * <p>
+ * This exception is raised in the following situations:
+ * <ul>
+ *   <li>{@link AsyncChannel#send(Object) send()} — the channel was closed before
+ *       or during the send attempt.  Pending senders that were waiting for buffer
+ *       space when the channel closed also receive this exception.</li>
+ *   <li>{@link AsyncChannel#receive() receive()} — the channel was closed and all
+ *       buffered values have been drained.  Note that values buffered before
+ *       closure are still delivered normally; only once the buffer is exhausted
+ *       does this exception appear.</li>
+ * </ul>
+ * <p>
+ * When used with {@code for await}, the loop infrastructure translates
+ * {@code ChannelClosedException} into a clean end-of-stream signal (i.e.,
+ * the loop exits normally rather than propagating the exception):
+ * <pre>
+ * def ch = AsyncChannel.create()
+ * // ... producer sends values, then calls ch.close()
+ * for await (item in ch) {
+ *     process(item)       // processes all buffered values
+ * }
+ * // loop exits cleanly after the channel is closed and drained
+ * </pre>
+ *
+ * @see AsyncChannel#send(Object)
+ * @see AsyncChannel#receive()
+ * @see AsyncChannel#close()
+ * @since 6.0.0
+ */
+public class ChannelClosedException extends IllegalStateException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Creates a {@code ChannelClosedException} with the specified detail message.
+     *
+     * @param message the detail message describing which operation failed
+     */
+    public ChannelClosedException(String message) {
+        super(message);
+    }
+
+    /**
+     * Creates a {@code ChannelClosedException} with the specified detail message
+     * and cause.
+     *
+     * @param message the detail message
+     * @param cause   the underlying cause (e.g., an {@link InterruptedException}
+     *                if the thread was interrupted while waiting on a channel operation)
+     */
+    public ChannelClosedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/main/java/groovy/transform/Async.java b/src/main/java/groovy/transform/Async.java
new file mode 100644
index 0000000000..ddfccbee4b
--- /dev/null
+++ b/src/main/java/groovy/transform/Async.java
@@ -0,0 +1,134 @@
+/*
+ *  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.
+ * <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
+ *     Map fetchProfile(long id) {
+ *         return [name: "User$id"]
+ *     }
+ *
+ *     {@code @}Async
+ *     List 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 an async type
+ *       ({@code Awaitable}, {@code AsyncStream}, {@code CompletableFuture},
+ *       {@code CompletionStage}, or {@code Future})</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 07331823f8..e49afa916b 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()));
@@ -475,6 +481,58 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         return forStatement;
     }
 
+    /**
+     * 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 = enhCtrl.type() != null ? this.visitType(enhCtrl.type()) : ClassHelper.dynamicType();
+        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);
+        Expression closeCall = AsyncTransformHelper.buildCloseStreamCall(varX(streamVar));
+        TryCatchStatement tryFinally = new TryCatchStatement(whileStmt, block(new ExpressionStatement(closeCall)));
+
+        return configureAST(block(streamDecl, tryFinally), ctx);
+    }
+
     @Override
     public Function<Statement, ForStatement> visitForControl(final ForControlContext ctx) {
         // e.g. `for (var e : x) { }` and `for (e in x) { }`
@@ -871,6 +929,40 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         return configureAST(this.visitYieldStatement(ctx.yieldStatement()), ctx);
     }
 
+    @Override
+    public ExpressionStatement visitYieldReturnStmtAlt(final YieldReturnStmtAltContext ctx) {
+        if (asyncContextDepth == 0) {
+            throw createParsingFailedException(
+                    "`yield return` can only be used inside an async method or async closure", 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) {
+        if (asyncContextDepth == 0) {
+            throw createParsingFailedException(
+                    "`defer` can only be used inside an async method or async closure", 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) {
@@ -1207,6 +1299,10 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
             throw createParsingFailedException("only sealed type declarations should have `permits` clause", ctx);
         }
 
+        if (modifierManager.containsAny(ASYNC)) {
+            throw createParsingFailedException("modifier `async` is not allowed for type declarations", modifierManager.get(ASYNC).get());
+        }
+
         int modifiers = modifierManager.getClassModifiersOpValue();
 
         boolean syntheticPublic = ((modifiers & Opcodes.ACC_SYNTHETIC) != 0);
@@ -1691,8 +1787,15 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         Parameter[] parameters = this.visitFormalParameters(ctx.formalParameters());
         ClassNode[] exceptions = this.visitQualifiedClassNameList(ctx.qualifiedClassNameList());
 
+        boolean isAsync = modifierManager.containsAny(ASYNC);
+        if (isAsync) asyncContextDepth++;
         anonymousInnerClassesDefinedInMethodStack.push(new LinkedList<>());
-        Statement code = this.visitMethodBody(ctx.methodBody());
+        Statement code;
+        try {
+            code = this.visitMethodBody(ctx.methodBody());
+        } finally {
+            if (isAsync) asyncContextDepth--;
+        }
         List<InnerClassNode> anonymousInnerClassList = anonymousInnerClassesDefinedInMethodStack.pop();
 
         MethodNode methodNode;
@@ -1705,6 +1808,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 (isAsync) {
+            methodNode.addAnnotation(new AnnotationNode(ClassHelper.make(groovy.transform.Async.class)));
+        }
+
         anonymousInnerClassList.forEach(e -> e.setEnclosingMethod(methodNode));
 
         methodNode.setGenericsTypes(this.visitTypeParameters(ctx.typeParameters()));
@@ -1929,6 +2038,10 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
                         asBoolean(ctx.modifiers()) ? this.visitModifiers(ctx.modifiers()) : Collections.emptyList()
                 );
 
+        if (modifierManager.containsAny(ASYNC)) {
+            throw createParsingFailedException("modifier `async` is not allowed for variable declarations", modifierManager.get(ASYNC).get());
+        }
+
         if (asBoolean(ctx.typeNamePairs())) { // e.g. def (int a, int b) = [1, 2]
             return this.createMultiAssignmentDeclarationListStatement(ctx, modifierManager);
         }
@@ -2883,6 +2996,72 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
         throw createParsingFailedException("Unsupported unary expression: " + ctx.getText(), ctx);
     }
 
+    @Override
+    public Expression visitAwaitExprAlt(final AwaitExprAltContext ctx) {
+        List<? extends ExpressionContext> exprCtxs = ctx.expression();
+        if (exprCtxs.size() == 1) {
+            Expression expr = (Expression) this.visit(exprCtxs.get(0));
+            return configureAST(
+                    AsyncTransformHelper.buildAwaitCall(expr),
+                    ctx);
+        }
+
+        // Multi-arg: await(p1, p2, ..., pn)
+        List<Expression> exprs = exprCtxs.stream()
+                                            .map(ec -> (Expression) this.visit(ec))
+                                            .collect(Collectors.toList());
+        return configureAST(
+            AsyncTransformHelper.buildAwaitCall(new ArgumentListExpression(exprs)),
+            ctx);
+    }
+
+    @Override
+    public Expression visitAsyncClosureExprAlt(final AsyncClosureExprAltContext ctx) {
+        asyncContextDepth++;
+        ClosureExpression closure;
+        try {
+            closure = this.visitClosureOrLambdaExpression(ctx.closureOrLambdaExpression());
+        } finally {
+            asyncContextDepth--;
+        }
+        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());
@@ -4733,6 +4912,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<>();
@@ -4744,6 +4924,9 @@ public class AstBuilder extends GroovyParserBaseVisitor<Object> {
     private int visitingClosureCount;
     private int visitingAssertStatementCount;
     private int visitingArrayInitializerCount;
+    /** Tracks nesting depth of async contexts (async methods and async closures)
+     *  to validate that {@code defer} is only used within an async body. */
+    private int asyncContextDepth;
 
     private static final int SLL_THRESHOLD = SystemUtil.getIntegerSafe("groovy.antlr4.sll.threshold", -1);
 
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java b/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
index 4d86449b8d..b84dd808a8 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/ModifierManager.java
@@ -37,6 +37,7 @@ import java.util.Optional;
 import java.util.stream.Collectors;
 
 import static org.apache.groovy.parser.antlr4.GroovyLangParser.ABSTRACT;
+import static org.apache.groovy.parser.antlr4.GroovyLangParser.ASYNC;
 import static org.apache.groovy.parser.antlr4.GroovyLangParser.FINAL;
 import static org.apache.groovy.parser.antlr4.GroovyLangParser.NATIVE;
 import static org.apache.groovy.parser.antlr4.GroovyLangParser.STATIC;
@@ -47,7 +48,7 @@ import static org.apache.groovy.parser.antlr4.GroovyLangParser.VOLATILE;
  */
 class ModifierManager {
     private static final Map<Class, List<Integer>> INVALID_MODIFIERS_MAP = Maps.of(
-            ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT, NATIVE),
+            ConstructorNode.class, Arrays.asList(STATIC, FINAL, ABSTRACT, NATIVE, ASYNC),
             MethodNode.class, Arrays.asList(VOLATILE/*, TRANSIENT*/) // Transient is left open for properties for legacy reasons but should be removed before ClassCompletionVerifier runs (CLASSGEN)
     );
     private AstBuilder astBuilder;
@@ -163,6 +164,7 @@ class ModifierManager {
     }
 
     public Parameter processParameter(Parameter parameter) {
+        rejectAsyncModifier("parameter declarations");
         modifierNodeList.forEach(e -> {
             parameter.setModifiers(parameter.getModifiers() | e.getOpcode());
 
@@ -191,6 +193,7 @@ class ModifierManager {
     }
 
     public VariableExpression processVariableExpression(VariableExpression ve) {
+        rejectAsyncModifier("variable declarations");
         modifierNodeList.forEach(e -> {
             ve.setModifiers(ve.getModifiers() | e.getOpcode());
 
@@ -200,6 +203,23 @@ class ModifierManager {
         return ve;
     }
 
+    /**
+     * Rejects the {@code async} modifier when used in an unsupported context
+     * (e.g. parameter or local variable declarations).  The grammar allows
+     * {@code async} as a general modifier, but semantically it is only valid
+     * on method declarations.  This check prevents silent acceptance.
+     *
+     * @param target human-readable description of the disallowed context
+     */
+    private void rejectAsyncModifier(String target) {
+        Optional<ModifierNode> asyncModifier = get(ASYNC);
+        if (asyncModifier.isPresent()) {
+            throw astBuilder.createParsingFailedException(
+                    "modifier `async` is only allowed for method declarations, not for " + target,
+                    asyncModifier.get());
+        }
+    }
+
     public <T extends AnnotatedNode> T attachAnnotations(T node) {
         this.getAnnotations().forEach(node::addAnnotation);
 
diff --git a/src/main/java/org/apache/groovy/runtime/async/AbstractAsyncStream.java b/src/main/java/org/apache/groovy/runtime/async/AbstractAsyncStream.java
new file mode 100644
index 0000000000..2e70a12848
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AbstractAsyncStream.java
@@ -0,0 +1,287 @@
+/*
+ *  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.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Template base class for queue-based {@link AsyncStream} implementations.
+ *
+ * <p>This class implements the
+ * <a href="https://en.wikipedia.org/wiki/Template_method_pattern">Template Method</a>
+ * pattern, centralising the signal dispatch logic, lifecycle management, and
+ * interrupt handling that is common to all queue-based async streams.
+ * Concrete subclasses only need to supply a {@link BlockingQueue} and override
+ * a small number of hook methods to customise behaviour.</p>
+ *
+ * <h2>Signal protocol</h2>
+ * <p>Elements flowing through the queue are wrapped in one of three signal types:</p>
+ * <ul>
+ *   <li>{@link ValueSignal} — carries a data element (may wrap {@code null})</li>
+ *   <li>{@link ErrorSignal} — carries a {@link Throwable} to propagate</li>
+ *   <li>{@link #COMPLETE} — singleton sentinel indicating normal end-of-stream</li>
+ * </ul>
+ * <p>The template's {@link #moveNext()} dispatches on these signals with a fixed
+ * sequence: value → set current + {@link #afterValueConsumed()} + return {@code true};
+ * error → set closed + sneaky-throw; complete → set closed + return {@code false}.</p>
+ *
+ * <h2>Hook methods (override points)</h2>
+ * <table>
+ *   <caption>Hook methods and their defaults</caption>
+ *   <tr><th>Hook</th><th>Default</th><th>Typical override</th></tr>
+ *   <tr><td>{@link #beforeTake()}</td><td>return {@code MOVE_NEXT_FALSE} if closed</td>
+ *       <td>thread registration, double-check, or drain check</td></tr>
+ *   <tr><td>{@link #afterValueConsumed()}</td><td>no-op</td>
+ *       <td>request more items from upstream (back-pressure)</td></tr>
+ *   <tr><td>{@link #afterMoveNext()}</td><td>no-op</td>
+ *       <td>unregister consumer thread</td></tr>
+ *   <tr><td>{@link #onMoveNextInterrupted(InterruptedException)}</td>
+ *       <td>set closed, restore interrupt, throw {@link CancellationException}</td>
+ *       <td>return {@code MOVE_NEXT_FALSE} if already closed</td></tr>
+ *   <tr><td>{@link #onClose()}</td><td><em>abstract</em></td>
+ *       <td>interrupt threads, cancel subscriptions, drain queue</td></tr>
+ * </table>
+ *
+ * <h2>Thread safety</h2>
+ * <p>The {@link #closed} flag is an {@link AtomicBoolean} shared between the
+ * producer (subclass-managed) and consumer ({@code moveNext()}) sides.
+ * The {@link #close()} method uses CAS to guarantee exactly-once semantics.
+ * The {@link #current} field is {@code volatile} for safe cross-thread visibility.</p>
+ *
+ * <p>This class is an internal implementation detail and should not be referenced
+ * directly by user code.</p>
+ *
+ * @param <T> the element type
+ * @see AsyncStreamGenerator
+ * @see FlowPublisherAdapter
+ * @since 6.0.0
+ */
+public abstract class AbstractAsyncStream<T> implements AsyncStream<T> {
+
+    // ---- Unified signal types ----
+
+    /**
+     * Wraps a data element for transport through the signal queue.
+     * The wrapper is necessary because the queue element type is {@code Object},
+     * and the actual value may be {@code null}.
+     */
+    protected record ValueSignal(Object value) { }
+
+    /**
+     * Wraps an error for transport through the signal queue.
+     * When dispatched by {@link #moveNext()}, the wrapped throwable is
+     * re-thrown via {@link AsyncSupport#sneakyThrow(Throwable)}.
+     */
+    protected record ErrorSignal(Throwable error) { }
+
+    /**
+     * Singleton sentinel indicating normal stream completion.
+     * Identity comparison ({@code ==}) is used in the dispatch logic.
+     */
+    protected static final Object COMPLETE = new Object();
+
+    // ---- Shared state ----
+
+    /** The signal queue bridging producer and consumer. */
+    protected final BlockingQueue<Object> queue;
+
+    /** Lifecycle flag: set exactly once when the stream is closed. */
+    protected final AtomicBoolean closed = new AtomicBoolean(false);
+
+    /** Most recently consumed value, set by {@link #moveNext()} on value signals. */
+    private volatile T current;
+
+    /**
+     * @param queue the blocking queue used for producer→consumer signal delivery;
+     *              must not be {@code null}
+     */
+    protected AbstractAsyncStream(BlockingQueue<Object> queue) {
+        this.queue = Objects.requireNonNull(queue, "queue");
+    }
+
+    // ---- Template method: moveNext ----
+
+    /**
+     * Template method implementing the {@link AsyncStream} iteration protocol.
+     *
+     * <p>Execution sequence:</p>
+     * <ol>
+     *   <li>{@link #beforeTake()} — may short-circuit with an early return</li>
+     *   <li>{@code queue.take()} — blocks until a signal is available</li>
+     *   <li>Signal dispatch: value / error / complete</li>
+     *   <li>{@link #afterMoveNext()} — always runs (finally block)</li>
+     * </ol>
+     *
+     * <p>If {@code queue.take()} throws {@link InterruptedException},
+     * {@link #onMoveNextInterrupted(InterruptedException)} handles it.</p>
+     *
+     * @return an {@code Awaitable<Boolean>} — {@code true} if a new element is
+     *         available via {@link #getCurrent()}, {@code false} if the stream
+     *         is exhausted or closed
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public final Awaitable<Boolean> moveNext() {
+        Awaitable<Boolean> earlyReturn = beforeTake();
+        if (earlyReturn != null) {
+            return earlyReturn;
+        }
+        try {
+            Object signal = queue.take();
+
+            if (signal instanceof ValueSignal vs) {
+                current = (T) vs.value;
+                afterValueConsumed();
+                return MOVE_NEXT_TRUE;
+            }
+            if (signal instanceof ErrorSignal es) {
+                closed.set(true);
+                Throwable cause = es.error;
+                if (cause instanceof Error err) throw err;
+                throw AsyncSupport.sneakyThrow(cause);
+            }
+            // COMPLETE sentinel — end-of-stream
+            closed.set(true);
+            return MOVE_NEXT_FALSE;
+        } catch (InterruptedException e) {
+            return onMoveNextInterrupted(e);
+        } finally {
+            afterMoveNext();
+        }
+    }
+
+    // ---- Final implementations ----
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Returns the most recently consumed element, set by the last successful
+     * {@link #moveNext()} call.
+     *
+     * @return the current element, or {@code null} before the first successful
+     *         {@code moveNext()} call
+     */
+    @Override
+    public final T getCurrent() {
+        return current;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Atomically marks this stream as closed via CAS on the {@link #closed}
+     * flag, then delegates to {@link #onClose()} for subclass-specific cleanup.
+     * Idempotent: only the first invocation triggers {@code onClose()}.
+     */
+    @Override
+    public final void close() {
+        if (!closed.compareAndSet(false, true)) {
+            return;
+        }
+        onClose();
+    }
+
+    // ---- Abstract methods ----
+
+    /**
+     * Subclass-specific cleanup, called exactly once from {@link #close()}.
+     * Typical actions include interrupting blocked threads, cancelling
+     * upstream subscriptions, and draining the queue.
+     */
+    protected abstract void onClose();
+
+    // ---- Hook methods with defaults ----
+
+    /**
+     * Pre-take hook invoked at the start of {@link #moveNext()}.
+     * <p>
+     * Return a non-{@code null} {@link Awaitable} to short-circuit
+     * {@code moveNext()} without blocking on the queue.  Return {@code null}
+     * to proceed with the normal take-and-dispatch sequence.
+     * <p>
+     * The default implementation returns {@link #MOVE_NEXT_FALSE} when
+     * the stream is closed, and {@code null} otherwise.
+     *
+     * @return an early-return value, or {@code null} to continue
+     */
+    protected Awaitable<Boolean> beforeTake() {
+        return closed.get() ? MOVE_NEXT_FALSE : null;
+    }
+
+    /**
+     * Post-value hook invoked after a {@link ValueSignal} has been consumed
+     * and the {@link #current} field updated.
+     * <p>
+     * Subclasses may use this to signal demand to an upstream source
+     * (e.g. {@code Subscription.request(1)} for reactive streams).
+     * <p>
+     * The default implementation is a no-op.
+     */
+    protected void afterValueConsumed() { }
+
+    /**
+     * Finally hook invoked after every {@link #moveNext()} attempt,
+     * regardless of outcome (value, error, completion, or interrupt).
+     * <p>
+     * Subclasses may use this to unregister the consumer thread.
+     * <p>
+     * The default implementation is a no-op.
+     */
+    protected void afterMoveNext() { }
+
+    /**
+     * Interrupt handler invoked when {@code queue.take()} throws
+     * {@link InterruptedException} inside {@link #moveNext()}.
+     * <p>
+     * The default implementation sets {@link #closed} to {@code true},
+     * restores the interrupt flag, and throws a {@link CancellationException}.
+     * Subclasses may override to return {@link #MOVE_NEXT_FALSE} instead
+     * of throwing (e.g. when an external {@code close()} caused the interrupt).
+     *
+     * @param e the interrupt exception
+     * @return an {@link Awaitable} to return from {@code moveNext()}, or
+     *         the method may throw instead
+     */
+    protected Awaitable<Boolean> onMoveNextInterrupted(InterruptedException e) {
+        closed.set(true);
+        Thread.currentThread().interrupt();
+        throw newCancellationException("Interrupted during moveNext", e);
+    }
+
+    // ---- Utilities for subclasses ----
+
+    /**
+     * Creates a {@link CancellationException} with the given message and cause.
+     *
+     * @param message the detail message
+     * @param cause   the interrupt that triggered the cancellation
+     * @return a new {@code CancellationException}
+     */
+    protected static CancellationException newCancellationException(String message, InterruptedException cause) {
+        CancellationException ce = new CancellationException(message);
+        ce.initCause(cause);
+        return ce;
+    }
+}
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..e01d5e67f6
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncStreamGenerator.java
@@ -0,0 +1,279 @@
+/*
+ *  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.concurrent.CancellationException;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A producer/consumer implementation of {@link groovy.concurrent.AsyncStream AsyncStream}
+ * backed by a {@link SynchronousQueue}, used by {@code async} methods that
+ * contain {@code yield return} statements.
+ * <p>
+ * Extends {@link AbstractAsyncStream} which provides the common
+ * {@link #moveNext()}/{@link #getCurrent()}/{@link #close()} template.
+ * This class adds the producer-side API ({@link #yield}, {@link #complete},
+ * {@link #error}) and overrides the following hooks:
+ *
+ * <ul>
+ *   <li>{@link #beforeTake()} — enforces single-consumer semantics via
+ *       {@link #consumerThread} CAS + double-check of the {@link #closed} flag</li>
+ *   <li>{@link #afterMoveNext()} — unregisters the consumer thread</li>
+ *   <li>{@link #onMoveNextInterrupted(InterruptedException)} — returns
+ *       {@code MOVE_NEXT_FALSE} if the stream was already closed (cooperative
+ *       cancellation), otherwise throws {@link CancellationException}</li>
+ *   <li>{@link #onClose()} — interrupts both producer and consumer threads</li>
+ * </ul>
+ *
+ * <h2>Back-pressure</h2>
+ * 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, providing natural one-at-a-time
+ * delivery semantics.
+ *
+ * <h2>Cooperative cancellation via thread tracking</h2>
+ * The {@link #producerThread} and {@link #consumerThread} fields track the
+ * threads currently blocked inside {@link SynchronousQueue#put} (producer)
+ * and {@link SynchronousQueue#take} (consumer).  This tracking is essential
+ * because {@code SynchronousQueue} operations block <em>indefinitely</em>
+ * when the counterpart is absent:
+ * <ul>
+ *   <li>If the consumer exits the {@code for await} loop early (via
+ *       {@code break}, {@code return}, or an exception), the producer may
+ *       still be blocked in {@code queue.put()}.  Without a reference to the
+ *       producer thread, there is no way to unblock it, resulting in a
+ *       <b>permanent thread leak</b>.</li>
+ *   <li>Conversely, if the producer finishes but the consumer is still
+ *       blocked in {@code queue.take()} (e.g. an external cancellation
+ *       closed the stream), the consumer thread would also leak.</li>
+ * </ul>
+ * When {@link #close()} is called (typically from the compiler-generated
+ * {@code finally} block of a {@code for await} loop), it atomically sets the
+ * {@link #closed} flag and then interrupts both the producer and consumer
+ * threads (if any), causing their blocking {@code SynchronousQueue} operations
+ * to throw {@link InterruptedException}.  The interrupted methods detect the
+ * closed state and exit gracefully — the producer's {@link #yield} throws
+ * {@link java.util.concurrent.CancellationException}, while the consumer's
+ * {@link #moveNext()} returns {@code Awaitable.of(false)}.
+ * <p>
+ * A <em>double-check</em> pattern in {@link #moveNext()} closes a TOCTOU race
+ * window: the {@link #closed} flag is re-checked <em>after</em> registering the
+ * consumer thread, ensuring that an externally-triggered {@code close()} cannot
+ * slip between the initial check and the registration, which would otherwise
+ * leave the consumer blocked forever in {@code queue.take()}.
+ * <p>
+ * The {@link #attachProducer}/{@link #detachProducer} lifecycle methods are
+ * called by the async runtime (in {@link AsyncSupport}) to register and
+ * unregister the producer thread.  The consumer thread is tracked
+ * automatically inside {@link #moveNext()}.
+ * <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> extends AbstractAsyncStream<T> {
+
+    private final AtomicReference<Thread> producerThread = new AtomicReference<>();
+    private final AtomicReference<Thread> consumerThread = new AtomicReference<>();
+
+    public AsyncStreamGenerator() {
+        super(new SynchronousQueue<>());
+    }
+
+    /**
+     * Registers the given thread as the producer for this stream.
+     * Called by the async runtime immediately after the producer
+     * thread starts, <em>before</em> the generator body executes.
+     * <p>
+     * If the stream has already been {@linkplain #close() closed} by the
+     * time this method runs, the thread is immediately interrupted so
+     * that the generator body can exit promptly.
+     *
+     * @param thread the producer thread to register
+     */
+    void attachProducer(Thread thread) {
+        producerThread.set(thread);
+        if (closed.get()) {
+            thread.interrupt();
+        }
+    }
+
+    /**
+     * Unregisters the given thread as the producer for this stream.
+     * Called from a {@code finally} block after the generator body
+     * completes (normally or exceptionally).
+     *
+     * @param thread the producer thread to unregister
+     */
+    void detachProducer(Thread thread) {
+        producerThread.compareAndSet(thread, null);
+    }
+
+    /**
+     * 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) {
+        if (closed.get()) {
+            throw streamClosed(null);
+        }
+        try {
+            queue.put(new ValueSignal(value));
+        } catch (InterruptedException e) {
+            if (closed.get()) {
+                throw streamClosed(e);
+            }
+            Thread.currentThread().interrupt();
+            throw newCancellationException("Interrupted during yield", e);
+        }
+    }
+
+    /**
+     * Signals that the generator has completed (no more elements).
+     * <p>
+     * If the blocking {@code queue.put()} is interrupted, a best-effort
+     * non-blocking {@code queue.offer()} is attempted.  If that also fails
+     * (no consumer is currently blocked in {@code take()}), the stream is
+     * force-closed to prevent the consumer from blocking indefinitely on a
+     * subsequent {@link #moveNext()} call.  This defensive close ensures no
+     * thread leak occurs even under unexpected interrupt timing.
+     */
+    public void complete() {
+        putTerminalSignal(COMPLETE);
+    }
+
+    /**
+     * Signals that the generator failed with an exception.
+     * <p>
+     * If the blocking {@code queue.put()} is interrupted, a best-effort
+     * non-blocking {@code queue.offer()} is attempted.  If that also fails,
+     * the stream is force-closed (same rationale as {@link #complete()}).
+     * The original error is not propagated to the consumer in this edge case;
+     * instead the consumer sees a clean stream closure — this is acceptable
+     * because the interrupt itself indicates an external cancellation.
+     */
+    public void error(Throwable t) {
+        putTerminalSignal(new ErrorSignal(t != null ? t : new NullPointerException("null error in generator")));
+    }
+
+    /**
+     * Shared logic for {@link #complete()} and {@link #error(Throwable)}:
+     * attempts a blocking {@code put()}, falling back to non-blocking
+     * {@code offer()} on interrupt, and force-closing if both fail.
+     */
+    private void putTerminalSignal(Object signal) {
+        if (closed.get()) {
+            return;
+        }
+        try {
+            queue.put(signal);
+        } catch (InterruptedException e) {
+            if (!closed.get()) {
+                Thread.currentThread().interrupt();
+                if (!queue.offer(signal)) {
+                    close();
+                }
+            }
+        }
+    }
+
+    // ---- Template method overrides ----
+
+    /**
+     * Enforces single-consumer semantics via CAS on {@link #consumerThread},
+     * with a double-check of the {@link #closed} flag to close the TOCTOU
+     * race window (see class-level javadoc).
+     */
+    @Override
+    protected Awaitable<Boolean> beforeTake() {
+        if (closed.get()) {
+            return MOVE_NEXT_FALSE;
+        }
+        Thread ct = Thread.currentThread();
+        if (!consumerThread.compareAndSet(null, ct)) {
+            Thread existing = consumerThread.get();
+            if (existing != ct) {
+                throw new IllegalStateException(
+                        "AsyncStream does not support concurrent consumers. "
+                        + "Current consumer: " + existing);
+            }
+        }
+        // Double-check: if close() raced between the first closed check and
+        // consumerThread CAS, the consumer reference was not yet visible to
+        // close(), so no interrupt was delivered.
+        if (closed.get()) {
+            consumerThread.compareAndSet(ct, null);
+            return MOVE_NEXT_FALSE;
+        }
+        return null;
+    }
+
+    /**
+     * Unregisters the consumer thread after every {@link #moveNext()} attempt.
+     */
+    @Override
+    protected void afterMoveNext() {
+        consumerThread.compareAndSet(Thread.currentThread(), null);
+    }
+
+    /**
+     * If the stream was already closed (cooperative cancellation), returns
+     * {@code MOVE_NEXT_FALSE}; otherwise restores the interrupt flag and
+     * throws {@link CancellationException}.
+     */
+    @Override
+    protected Awaitable<Boolean> onMoveNextInterrupted(InterruptedException e) {
+        if (closed.get()) {
+            return MOVE_NEXT_FALSE;
+        }
+        Thread.currentThread().interrupt();
+        throw newCancellationException("Interrupted during moveNext", e);
+    }
+
+    /**
+     * Interrupts both producer and consumer threads (if any) to unblock
+     * pending {@link SynchronousQueue} operations.  A thread is never
+     * self-interrupted.
+     */
+    @Override
+    protected void onClose() {
+        Thread producer = producerThread.getAndSet(null);
+        if (producer != null && producer != Thread.currentThread()) {
+            producer.interrupt();
+        }
+        Thread consumer = consumerThread.getAndSet(null);
+        if (consumer != null && consumer != Thread.currentThread()) {
+            consumer.interrupt();
+        }
+    }
+
+    private static CancellationException streamClosed(InterruptedException cause) {
+        CancellationException ce = new CancellationException("Async stream was closed");
+        if (cause != null) {
+            ce.initCause(cause);
+        }
+        return ce;
+    }
+}
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..29cd6f7450
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/AsyncSupport.java
@@ -0,0 +1,1135 @@
+/*
+ *  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.AsyncContext;
+import groovy.concurrent.AsyncScope;
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.AwaitResult;
+import groovy.concurrent.Awaitable;
+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.Locale;
+import java.util.Objects;
+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.ScheduledFuture;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * 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()},
+ *       {@code wrapAsync()}, and {@code go()} run closures on the configured
+ *       executor while preserving the current {@link AsyncContext}</li>
+ *   <li><b>Await</b> — all {@code await()} overloads use
+ *       {@link groovy.concurrent.Awaitable#from(Object) Awaitable.from()} 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>Async stream cleanup</b> — {@code closeStream()} lets compiler-generated
+ *       {@code for await} loops release resources and propagate early-exit
+ *       cancellation upstream</li>
+ *   <li><b>Defer</b> — {@code createDeferScope()}, {@code defer()}, and
+ *       {@code executeDeferScope()} implement deferred cleanup with
+ *       LIFO execution, async cleanup awareness, and exception suppression</li>
+ *   <li><b>Channels</b> — {@code channel()} creates asynchronous channels that
+ *       compose with {@link Awaitable#any(Object...)} / {@code select()}</li>
+ *   <li><b>Delay</b> — {@code delay()} provides non-blocking delays using a
+ *       shared {@link java.util.concurrent.ScheduledExecutorService}</li>
+ *   <li><b>Timeouts</b> — {@code orTimeout()} and {@code completeOnTimeout()} apply
+ *       non-blocking deadlines while preserving the {@link Awaitable} abstraction</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 state-machine-based
+ *       approaches 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 a transparency principle: 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
+ * @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
+     * {@link Awaitable#from(Object)}.  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(Awaitable.from(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.
+     * <p>
+     * Captures the current {@link AsyncContext} at submission time and
+     * restores that snapshot when the closure runs.
+     *
+     * @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) {
+        Objects.requireNonNull(closure, "closure must not be null");
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        Executor targetExecutor = executor != null ? executor : defaultExecutor;
+        return GroovyPromise.of(CompletableFuture.supplyAsync(() ->
+                AsyncContext.withSnapshot(contextSnapshot, () -> {
+                    try {
+                        return closure.call();
+                    } catch (Throwable t) {
+                        throw wrapForFuture(t);
+                    }
+                }), targetExecutor));
+    }
+
+    /**
+     * Void variant of {@link #executeAsync(Closure, Executor)} for
+     * {@link groovy.transform.Async @Async} methods whose return type is
+     * {@code void}.
+     * <p>
+     * Captures and restores the current {@link AsyncContext} in the same way
+     * as {@link #executeAsync(Closure, Executor)}.
+     *
+     * @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) {
+        Objects.requireNonNull(closure, "closure must not be null");
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        Executor targetExecutor = executor != null ? executor : defaultExecutor;
+        return GroovyPromise.of(CompletableFuture.runAsync(() ->
+                AsyncContext.withSnapshot(contextSnapshot, () -> {
+                    try {
+                        closure.call();
+                    } catch (Throwable t) {
+                        throw wrapForFuture(t);
+                    }
+                }), targetExecutor));
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Task spawn primitive.
+     * <p>
+     * When called inside {@link AsyncScope#withScope(groovy.lang.Closure)},
+     * the new task is registered as a structured child of the current scope.
+     * Outside a scope it behaves like a lightweight detached async task.
+     *
+     * @param closure the task body
+     * @param <T>     the result type
+     * @return an awaitable representing the spawned task
+     */
+    public static <T> Awaitable<T> go(Closure<T> closure) {
+        AsyncScope currentScope = AsyncScope.current();
+        if (currentScope != null) {
+            return currentScope.async(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.  Each invocation
+     * captures the caller's {@link AsyncContext} and restores it when the
+     * wrapped closure executes:
+     * <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) {
+        Objects.requireNonNull(closure, "closure must not be null");
+        return new Closure<Awaitable<T>>(closure.getOwner(), closure.getThisObject()) {
+            @SuppressWarnings("unused")
+            public Awaitable<T> doCall(Object... args) {
+                AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+                return GroovyPromise.of(CompletableFuture.supplyAsync(() ->
+                        AsyncContext.withSnapshot(contextSnapshot, () -> {
+                            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.  As with
+     * ordinary async closures, each invocation captures the caller's
+     * {@link AsyncContext} snapshot:
+     * <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) {
+        Objects.requireNonNull(closure, "closure must not be null");
+        return new Closure<AsyncStream<T>>(closure.getOwner(), closure.getThisObject()) {
+            @SuppressWarnings("unused")
+            public AsyncStream<T> doCall(Object... args) {
+                AsyncStreamGenerator<T> gen = new AsyncStreamGenerator<>();
+                AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+                CompletableFuture.runAsync(() -> AsyncContext.withSnapshot(contextSnapshot, () -> {
+                    gen.attachProducer(Thread.currentThread());
+                    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);
+                    } finally {
+                        gen.detachProducer(Thread.currentThread());
+                    }
+                }), defaultExecutor);
+                return gen;
+            }
+        };
+    }
+
+    // ---- awaitAll / awaitAny / awaitAllSettled ---------------------------
+
+    /**
+     * Waits for all given awaitables to complete and returns their results
+     * as a list, preserving argument order.  Like JavaScript's
+     * {@code Promise.all()}, the combined wait fails as soon as any input
+     * fails; slower siblings are not cancelled automatically.
+     * <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<>();
+        }
+        CompletableFuture<?>[] futures = toCompletableFutures(awaitables, "awaitAll");
+        return await(collectAllFailFast(futures));
+    }
+
+    /**
+     * Waits for any one of the given awaitables to complete and returns
+     * its result.
+     *
+     * @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");
+        }
+        CompletableFuture<?>[] futures = toCompletableFutures(awaitables, "awaitAny");
+        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.
+     * 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<>();
+        }
+        CompletableFuture<?>[] futures = toCompletableFutures(awaitables, "awaitAllSettled");
+        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 Awaitable.from(source).toCompletableFuture();
+    }
+
+    /**
+     * Validates that no element is {@code null} and converts all elements to
+     * {@link CompletableFuture}s in one pass.  Used by combinator methods
+     * ({@code awaitAll}, {@code awaitAny}, {@code awaitAllSettled}, and their
+     * non-blocking {@code Async} variants) to eliminate duplicate
+     * null-checking and conversion loops.
+     *
+     * @param sources    the source objects to validate and convert
+     * @param callerName the method name for error messages
+     * @return an array of completable futures corresponding to the sources
+     * @throws IllegalArgumentException if any element is {@code null}
+     */
+    private static CompletableFuture<?>[] toCompletableFutures(Object[] sources, String callerName) {
+        CompletableFuture<?>[] futures = new CompletableFuture<?>[sources.length];
+        for (int i = 0; i < sources.length; i++) {
+            if (sources[i] == null) {
+                throw new IllegalArgumentException(callerName + ": element at index " + i + " is null");
+            }
+            futures[i] = toCompletableFuture(sources[i]);
+        }
+        return futures;
+    }
+
+    /**
+     * Collects results in input order while completing exceptionally as soon as
+     * the first input fails.  Mirrors {@code Promise.all()} early-rejection
+     * semantics without implicitly cancelling sibling work.
+     * <p>
+     * Failures are unwrapped <em>before</em> they are stored on the combined
+     * future so that {@code await Awaitable.all(...)} and
+     * {@link #awaitAll(Object...)} both observe the original throwable type.
+     */
+    private static CompletableFuture<List<Object>> collectAllFailFast(CompletableFuture<?>[] futures) {
+        CompletableFuture<List<Object>> combined = new CompletableFuture<>();
+        Object[] results = new Object[futures.length];
+        AtomicInteger remaining = new AtomicInteger(futures.length);
+
+        for (int i = 0; i < futures.length; i++) {
+            final int index = i;
+            futures[i].whenComplete((value, error) -> {
+                if (error != null) {
+                    combined.completeExceptionally(deepUnwrap(error));
+                    return;
+                }
+
+                results[index] = value;
+                if (remaining.decrementAndGet() == 0) {
+                    combined.complete(new ArrayList<Object>(Arrays.asList(results)));
+                }
+            });
+        }
+
+        return combined;
+    }
+
+    // ---- non-blocking combinators (return Awaitable) --------------------
+
+    /**
+     * Non-blocking variant of {@link #awaitAll(Object...)} that returns an
+     * {@link Awaitable} instead of blocking.  Like JavaScript's
+     * {@code Promise.all()}, the returned awaitable rejects on the first
+     * observed failure while preserving ordered results on success.
+     * 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 = toCompletableFutures(sources, "Awaitable.all");
+        return GroovyPromise.of(collectAllFailFast(futures));
+    }
+
+    /**
+     * 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");
+        }
+        CompletableFuture<?>[] futures = toCompletableFutures(sources, "Awaitable.any");
+        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<>());
+        }
+        CompletableFuture<?>[] futures = toCompletableFutures(sources, "Awaitable.allSettled");
+        // 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;
+    }
+
+    /**
+     * Applies fail-fast timeout semantics to the given source.
+     * Returns an awaitable that fails with {@link TimeoutException} if the
+     * source does not complete before the timeout elapses.
+     * <p>
+     * The timeout does <em>not</em> cancel the original source automatically.
+     * This mirrors value-level race composition and keeps cancellation explicit.
+     *
+     * @param source the async source to time out
+     * @param timeout the timeout duration
+     * @param unit the timeout unit
+     * @param <T> the result type
+     * @return an awaitable that fails on timeout
+     * @throws IllegalArgumentException if any argument is invalid
+     * @since 6.0.0
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Awaitable<T> orTimeout(Object source, long timeout, TimeUnit unit) {
+        validateTimeoutArguments(source, timeout, unit, "Awaitable.orTimeout");
+        CompletableFuture<T> sourceFuture = (CompletableFuture<T>) toCompletableFuture(source);
+        CompletableFuture<T> result = new CompletableFuture<>();
+        TimeoutException te = new TimeoutException(
+                "Timed out after " + timeout + " " + unit.name().toLowerCase(Locale.ROOT));
+        scheduleTimeoutRace(sourceFuture, result, () -> result.completeExceptionally(te), timeout, unit);
+        return GroovyPromise.of(result);
+    }
+
+    /**
+     * Millisecond shortcut for {@link #orTimeout(Object, long, TimeUnit)}.
+     *
+     * @param source the async source to time out
+     * @param timeoutMillis timeout in milliseconds
+     * @param <T> result type
+     * @return an awaitable that fails on timeout
+     * @since 6.0.0
+     */
+    public static <T> Awaitable<T> orTimeoutMillis(Object source, long timeoutMillis) {
+        return orTimeout(source, timeoutMillis, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Applies fallback-on-timeout semantics to the given source.
+     * Returns an awaitable that completes with the supplied fallback value if
+     * the source has not completed before the timeout elapses.
+     *
+     * @param source the async source to wait for
+     * @param fallback fallback value when timeout elapses first
+     * @param timeout the timeout duration
+     * @param unit the timeout unit
+     * @param <T> the result type
+     * @return an awaitable that yields either source result or fallback value
+     * @throws IllegalArgumentException if any argument is invalid
+     * @since 6.0.0
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Awaitable<T> completeOnTimeout(Object source, T fallback, long timeout, TimeUnit unit) {
+        validateTimeoutArguments(source, timeout, unit, "Awaitable.completeOnTimeout");
+        CompletableFuture<T> sourceFuture = (CompletableFuture<T>) toCompletableFuture(source);
+        CompletableFuture<T> result = new CompletableFuture<>();
+        scheduleTimeoutRace(sourceFuture, result, () -> result.complete(fallback), timeout, unit);
+        return GroovyPromise.of(result);
+    }
+
+    /**
+     * Millisecond shortcut for {@link #completeOnTimeout(Object, Object, long, TimeUnit)}.
+     *
+     * @param source the async source to wait for
+     * @param fallback fallback value when timeout elapses first
+     * @param timeoutMillis timeout in milliseconds
+     * @param <T> result type
+     * @return an awaitable that yields either source result or fallback
+     * @since 6.0.0
+     */
+    public static <T> Awaitable<T> completeOnTimeoutMillis(Object source, T fallback, long timeoutMillis) {
+        return completeOnTimeout(source, fallback, timeoutMillis, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Shared logic for {@link #orTimeout} and {@link #completeOnTimeout}:
+     * schedules the timeout action and wires up source completion to cancel
+     * the timer and propagate the result or error.
+     */
+    private static <T> void scheduleTimeoutRace(
+            CompletableFuture<T> sourceFuture,
+            CompletableFuture<T> result,
+            Runnable onTimeout,
+            long duration, TimeUnit unit) {
+        ScheduledFuture<?> timeoutTask = DELAY_SCHEDULER.schedule(onTimeout, duration, unit);
+        sourceFuture.whenComplete((value, error) -> {
+            timeoutTask.cancel(false);
+            if (error != null) {
+                result.completeExceptionally(deepUnwrap(error));
+            } else {
+                result.complete(value);
+            }
+        });
+    }
+
+    private static void validateTimeoutArguments(Object source, long duration, TimeUnit unit, String methodName) {
+        if (source == null) {
+            throw new IllegalArgumentException(methodName + ": source must not be null");
+        }
+        if (unit == null) {
+            throw new IllegalArgumentException(methodName + ": TimeUnit must not be null");
+        }
+        if (duration < 0) {
+            throw new IllegalArgumentException(methodName + ": duration must not be negative: " + duration);
+        }
+    }
+
+    // ---- 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.
+     * <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 AsyncStream.from(source);
+    }
+
+    /**
+     * Closes the given async stream if it is non-null.
+     * <p>
+     * Compiler-generated {@code for await} loops call this from a synthetic
+     * {@code finally} block so that early {@code break}, {@code return}, or
+     * exceptional exit reliably releases the underlying stream resources and
+     * propagates cancellation to upstream producers where supported.
+     *
+     * @param stream the stream to close, or {@code null}
+     * @since 6.0.0
+     */
+    public static void closeStream(Object stream) {
+        if (stream instanceof AsyncStream<?> asyncStream) {
+            asyncStream.close();
+        }
+    }
+
+    // ---- 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)}.
+     * <p>
+     * The generator body executes with the {@link AsyncContext} snapshot that
+     * was active when generation started.
+     *
+     * @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) {
+        Objects.requireNonNull(body, "body must not be null");
+        AsyncStreamGenerator<T> gen = new AsyncStreamGenerator<>();
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        CompletableFuture.runAsync(() -> AsyncContext.withSnapshot(contextSnapshot, () -> {
+            gen.attachProducer(Thread.currentThread());
+            try {
+                body.call(gen);
+                gen.complete();
+            } catch (Throwable t) {
+                gen.error(t);
+            } finally {
+                gen.detachProducer(Thread.currentThread());
+            }
+        }), 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.  The deferred action
+     * is bound to the current {@link AsyncContext} snapshot so that cleanup
+     * observes the same request/trace metadata that was active when the defer
+     * statement was executed.
+     *
+     * @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");
+        }
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        scope.push(new Closure<Object>(action.getOwner(), action.getThisObject()) {
+            @SuppressWarnings("unused")
+            public Object doCall() {
+                return AsyncContext.withSnapshot(contextSnapshot, () -> action.call());
+            }
+        });
+    }
+
+    /**
+     * 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}.
+     * If a deferred action returns an {@link Awaitable},
+     * {@link CompletionStage}, {@link CompletableFuture}, or {@link Future},
+     * the async cleanup is awaited before moving on to the next deferred
+     * action.
+     * <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 {
+                Object result = scope.pop().call();
+                awaitDeferredResult(result);
+            } 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);
+    }
+
+    private static void awaitDeferredResult(Object result) {
+        if (result instanceof Awaitable
+            || result instanceof CompletionStage
+            || result instanceof Future) {
+            await(result);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncChannel.java b/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncChannel.java
new file mode 100644
index 0000000000..47d06cf584
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncChannel.java
@@ -0,0 +1,410 @@
+/*
+ *  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.AsyncChannel;
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.ChannelClosedException;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Default lock-based implementation of the {@link AsyncChannel} interface.
+ *
+ * <p>This implementation uses a {@link ReentrantLock} to coordinate access
+ * to the internal buffer and the waiting-sender / waiting-receiver queues.
+ * All operations are non-blocking from the caller's perspective: they
+ * return an {@link Awaitable} immediately, and the underlying
+ * {@link CompletableFuture} is completed asynchronously when matching
+ * counterparts arrive.
+ *
+ * <h2>Internal Data Structures</h2>
+ * <ul>
+ *   <li><b>buffer</b> — an {@link ArrayDeque} of capacity {@code capacity}
+ *       that holds values sent but not yet received.  For unbuffered
+ *       channels (capacity {@code 0}) this deque is always empty.</li>
+ *   <li><b>waitingSenders</b> — FIFO queue of senders whose values could
+ *       not be delivered or buffered at send time.</li>
+ *   <li><b>waitingReceivers</b> — FIFO queue of receivers for which no
+ *       value was available at receive time.</li>
+ * </ul>
+ *
+ * <h2>Cancellation Cleanup</h2>
+ * <p>When a pending send or receive is cancelled or completed exceptionally
+ * (e.g., via {@link Awaitable#orTimeout}), a cleanup callback removes the
+ * stale entry from the appropriate queue.  This prevents memory leaks in
+ * long-lived channels.
+ *
+ * <h2>Thread Safety</h2>
+ * <p>All public methods are thread-safe.  The lock is held only for the
+ * duration of queue/buffer manipulation — never across asynchronous
+ * boundaries — ensuring that lock hold times remain bounded and short.
+ *
+ * @param <T> the payload type carried through the channel
+ * @see AsyncChannel
+ * @since 6.0.0
+ */
+public final class DefaultAsyncChannel<T> implements AsyncChannel<T> {
+
+    private final ReentrantLock lock = new ReentrantLock();
+    private final Deque<T> buffer = new ArrayDeque<>();
+    private final Deque<PendingSend<T>> waitingSenders = new ArrayDeque<>();
+    private final Deque<CompletableFuture<T>> waitingReceivers = new ArrayDeque<>();
+    private final int capacity;
+
+    private volatile boolean closed;
+
+    /**
+     * Creates an unbuffered (rendezvous) channel with a capacity of {@code 0}.
+     *
+     * <p>In this mode every {@link #send(Object)} suspends until a matching
+     * {@link #receive()} arrives, providing the strongest producer–consumer
+     * synchronization guarantee.
+     */
+    public DefaultAsyncChannel() {
+        this(0);
+    }
+
+    /**
+     * Creates a channel with the specified buffer capacity.
+     *
+     * @param capacity the maximum number of values that can be buffered;
+     *                 {@code 0} for an unbuffered (rendezvous) channel
+     * @throws IllegalArgumentException if {@code capacity} is negative
+     */
+    public DefaultAsyncChannel(int capacity) {
+        if (capacity < 0) {
+            throw new IllegalArgumentException("channel capacity must not be negative: " + capacity);
+        }
+        this.capacity = capacity;
+    }
+
+    // ---- Query Methods --------------------------------------------------
+
+    @Override
+    public int getCapacity() {
+        return capacity;
+    }
+
+    @Override
+    public int getBufferedSize() {
+        lock.lock();
+        try {
+            return buffer.size();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public boolean isClosed() {
+        return closed;
+    }
+
+    // ---- Core Operations ------------------------------------------------
+
+    @Override
+    public Awaitable<Void> send(T value) {
+        Objects.requireNonNull(value, "channel does not support null values");
+
+        CompletableFuture<Void> completion = new CompletableFuture<>();
+        PendingSend<T> pending = new PendingSend<>(value, completion);
+        boolean queued;
+
+        lock.lock();
+        try {
+            if (closed) {
+                completion.completeExceptionally(closedForSend());
+                queued = false;
+            } else if (deliverToWaitingReceiver(value)) {
+                completion.complete(null);
+                queued = false;
+            } else if (buffer.size() < capacity) {
+                buffer.addLast(value);
+                completion.complete(null);
+                queued = false;
+            } else {
+                waitingSenders.addLast(pending);
+                queued = true;
+            }
+        } finally {
+            lock.unlock();
+        }
+
+        // Register cleanup for cancellation/timeout of a queued send.
+        // The callback is registered unconditionally for queued sends to
+        // avoid a race where an external cancel between lock release and
+        // callback registration leaves a stale entry in the queue.
+        if (queued) {
+            completion.whenComplete((ignored, error) -> {
+                if (error != null || completion.isCancelled()) {
+                    removePendingSender(pending);
+                }
+            });
+        }
+
+        return GroovyPromise.of(completion);
+    }
+
+    @Override
+    public Awaitable<T> receive() {
+        CompletableFuture<T> completion = new CompletableFuture<>();
+        boolean queued;
+
+        lock.lock();
+        try {
+            T buffered = pollBuffer();
+            if (buffered != null) {
+                completion.complete(buffered);
+                queued = false;
+            } else {
+                PendingSend<T> sender = pollPendingSender();
+                if (sender != null) {
+                    sender.completion.complete(null);
+                    completion.complete(sender.value);
+                    queued = false;
+                } else if (closed) {
+                    completion.completeExceptionally(closedForReceive());
+                    queued = false;
+                } else {
+                    waitingReceivers.addLast(completion);
+                    queued = true;
+                }
+            }
+        } finally {
+            lock.unlock();
+        }
+
+        // Register cleanup for cancellation/timeout of a queued receive.
+        // Same rationale as in send(): unconditional registration avoids
+        // a race between lock release and external cancellation.
+        if (queued) {
+            completion.whenComplete((ignored, error) -> {
+                if (error != null || completion.isCancelled()) {
+                    removePendingReceiver(completion);
+                }
+            });
+        }
+
+        return GroovyPromise.of(completion);
+    }
+
+    @Override
+    public boolean close() {
+        lock.lock();
+        try {
+            if (closed) {
+                return false;
+            }
+            closed = true;
+
+            drainBufferToReceivers();
+
+            while (!waitingReceivers.isEmpty()) {
+                waitingReceivers.removeFirst().completeExceptionally(closedForReceive());
+            }
+
+            while (!waitingSenders.isEmpty()) {
+                waitingSenders.removeFirst().completion.completeExceptionally(closedForSend());
+            }
+
+            return true;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    @Override
+    public AsyncStream<T> asStream() {
+        return new ChannelAsyncStream<>(this);
+    }
+
+    /**
+     * Returns a diagnostic string showing the channel's current state.
+     *
+     * <p>The output includes capacity, buffered element count, queue sizes,
+     * and closed status.  Useful for logging and debugging.
+     *
+     * @return a human-readable representation of this channel
+     */
+    @Override
+    public String toString() {
+        lock.lock();
+        try {
+            return "AsyncChannel{capacity=" + capacity
+                    + ", buffered=" + buffer.size()
+                    + ", waitingSenders=" + waitingSenders.size()
+                    + ", waitingReceivers=" + waitingReceivers.size()
+                    + ", closed=" + closed
+                    + '}';
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    // ---- Internal Helpers -----------------------------------------------
+
+    /**
+     * Attempts to deliver a value directly to the first viable waiting
+     * receiver.  Skips receivers that have already been completed (e.g.,
+     * due to cancellation or timeout).
+     */
+    private boolean deliverToWaitingReceiver(T value) {
+        while (!waitingReceivers.isEmpty()) {
+            CompletableFuture<T> receiver = waitingReceivers.removeFirst();
+            if (receiver.complete(value)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Delivers buffered values to as many waiting receivers as possible.
+     * Called during {@link #close()} to ensure buffered data is not lost.
+     */
+    private void drainBufferToReceivers() {
+        while (!waitingReceivers.isEmpty() && !buffer.isEmpty()) {
+            CompletableFuture<T> receiver = waitingReceivers.removeFirst();
+            if (receiver.complete(buffer.peekFirst())) {
+                buffer.removeFirst();
+            }
+        }
+    }
+
+    /**
+     * Removes and returns the first buffered value, refilling the freed
+     * slot from waiting senders if possible.
+     */
+    private T pollBuffer() {
+        if (buffer.isEmpty()) {
+            return null;
+        }
+        T value = buffer.removeFirst();
+        refillBufferFromWaitingSenders();
+        return value;
+    }
+
+    /**
+     * Admits waiting senders into freed buffer slots up to the capacity.
+     */
+    private void refillBufferFromWaitingSenders() {
+        while (buffer.size() < capacity) {
+            PendingSend<T> sender = pollPendingSender();
+            if (sender == null) {
+                return;
+            }
+            buffer.addLast(sender.value);
+            sender.completion.complete(null);
+        }
+    }
+
+    /**
+     * Returns the first pending sender whose future has not been completed
+     * (i.e., not yet cancelled or timed out), or {@code null} if none.
+     */
+    private PendingSend<T> pollPendingSender() {
+        while (!waitingSenders.isEmpty()) {
+            PendingSend<T> sender = waitingSenders.removeFirst();
+            if (!sender.completion.isDone()) {
+                return sender;
+            }
+        }
+        return null;
+    }
+
+    private void removePendingSender(PendingSend<T> sender) {
+        lock.lock();
+        try {
+            waitingSenders.remove(sender);
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private void removePendingReceiver(CompletableFuture<T> receiver) {
+        lock.lock();
+        try {
+            waitingReceivers.remove(receiver);
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private static ChannelClosedException closedForSend() {
+        return new ChannelClosedException("channel is closed for send");
+    }
+
+    private static ChannelClosedException closedForReceive() {
+        return new ChannelClosedException("channel is closed");
+    }
+
+    // ---- Inner Classes --------------------------------------------------
+
+    /**
+     * Holds a value and its associated completion future for a sender
+     * that is waiting for buffer space or a matching receiver.
+     */
+    private record PendingSend<T>(T value, CompletableFuture<Void> completion) {}
+
+    /**
+     * Read-only {@link AsyncStream} view over an {@link AsyncChannel}.
+     *
+     * <p>Each {@link #moveNext()} call delegates to
+     * {@link AsyncChannel#receive()}; a {@link ChannelClosedException} is
+     * translated to end-of-stream ({@code false}).  The stream does not
+     * own the channel, so its {@link #close()} is a no-op.
+     *
+     * @param <T> the element type
+     */
+    private static final class ChannelAsyncStream<T> implements AsyncStream<T> {
+        private final AsyncChannel<T> channel;
+        private volatile T current;
+
+        ChannelAsyncStream(AsyncChannel<T> channel) {
+            this.channel = channel;
+        }
+
+        @Override
+        public Awaitable<Boolean> moveNext() {
+            return channel.receive().handle((value, error) -> {
+                if (error != null) {
+                    if (error instanceof ChannelClosedException) {
+                        return false;
+                    }
+                    if (error instanceof RuntimeException re) throw re;
+                    if (error instanceof Error e) throw e;
+                    throw new RuntimeException(error);
+                }
+                current = value;
+                return true;
+            });
+        }
+
+        @Override
+        public T getCurrent() {
+            return current;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncContext.java b/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncContext.java
new file mode 100644
index 0000000000..424eda3db6
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncContext.java
@@ -0,0 +1,345 @@
+/*
+ *  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.AsyncContext;
+import groovy.lang.Closure;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Default implementation of {@link AsyncContext} backed by a
+ * {@link LinkedHashMap} for insertion-ordered key-value storage.
+ *
+ * <h2>Thread-scoped storage</h2>
+ * <p>A per-thread {@code DefaultAsyncContext} instance is managed via a
+ * {@link ScopedLocal} with a lazy initial-value supplier.  On JDK&nbsp;25+
+ * this leverages {@code ScopedValue} for optimal virtual-thread
+ * performance; on earlier JDKs it falls back to {@code ThreadLocal}.</p>
+ *
+ * <p>This class is an internal implementation detail of the Groovy async
+ * runtime.  User code should program against the {@link AsyncContext}
+ * interface and use its static factory and utility methods.</p>
+ *
+ * @see AsyncContext
+ * @since 6.0.0
+ */
+public final class DefaultAsyncContext implements AsyncContext {
+
+    /**
+     * Per-thread binding of the current async context.
+     * Lazy-initialized with an empty {@code DefaultAsyncContext}.
+     */
+    private static final ScopedLocal<DefaultAsyncContext> CURRENT =
+            ScopedLocal.withInitial(DefaultAsyncContext::new);
+
+    private final Map<String, Object> values;
+
+    /**
+     * Creates an empty async context.
+     */
+    public DefaultAsyncContext() {
+        this.values = new LinkedHashMap<>();
+    }
+
+    /**
+     * Creates a context seeded from the supplied entries.
+     *
+     * @param initialValues initial context entries; may be {@code null}
+     */
+    public DefaultAsyncContext(Map<?, ?> initialValues) {
+        this();
+        putAll(initialValues);
+    }
+
+    /**
+     * Internal constructor that directly adopts a pre-copied map.
+     * The caller must guarantee the map will not be mutated externally.
+     */
+    DefaultAsyncContext(LinkedHashMap<String, Object> values, boolean ignored) {
+        this.values = values;
+    }
+
+    // ---- Static operations (delegated from AsyncContext interface) -------
+
+    /**
+     * Returns the live async context associated with the current thread.
+     *
+     * @return the current async context; never {@code null}
+     */
+    public static DefaultAsyncContext current() {
+        return CURRENT.get();
+    }
+
+    /**
+     * Captures an immutable snapshot of the current async context.
+     *
+     * @return the captured snapshot; never {@code null}
+     */
+    public static AsyncContext.Snapshot capture() {
+        return DefaultSnapshot.of(current().copyValues());
+    }
+
+    /**
+     * Executes the supplier with the given snapshot installed as the
+     * current async context, restoring the previous context afterwards.
+     *
+     * @param snapshot the snapshot to install; must not be {@code null}
+     * @param supplier the action to execute; must not be {@code null}
+     * @param <T>      the result type
+     * @return the supplier's result
+     */
+    public static <T> T withSnapshot(AsyncContext.Snapshot snapshot, Supplier<T> supplier) {
+        Objects.requireNonNull(snapshot, "snapshot must not be null");
+        Objects.requireNonNull(supplier, "supplier must not be null");
+        DefaultAsyncContext restored = new DefaultAsyncContext(
+                new LinkedHashMap<>(snapshot.asMap()), true);
+        return CURRENT.where(restored, supplier);
+    }
+
+    /**
+     * Executes the runnable with the given snapshot installed as the
+     * current async context, restoring the previous context afterwards.
+     *
+     * @param snapshot the snapshot to install; must not be {@code null}
+     * @param action   the action to execute; must not be {@code null}
+     */
+    public static void withSnapshot(AsyncContext.Snapshot snapshot, Runnable action) {
+        withSnapshot(snapshot, () -> {
+            action.run();
+            return null;
+        });
+    }
+
+    /**
+     * Temporarily overlays the current async context with the supplied
+     * entries for the duration of the closure.
+     *
+     * @param entries the entries to overlay; may be {@code null}
+     * @param action  the closure to execute; must not be {@code null}
+     * @param <T>     the result type
+     * @return the closure's result
+     */
+    public static <T> T with(Map<?, ?> entries, Closure<T> action) {
+        Objects.requireNonNull(action, "action must not be null");
+        AsyncContext.Snapshot merged = capture().with(entries);
+        return withSnapshot(merged, () -> action.call());
+    }
+
+    /**
+     * Creates a {@link AsyncContext.Snapshot} from a pre-copied map.
+     * Called by the {@link AsyncContext.Snapshot#of(Map)} static factory.
+     *
+     * @param values the context entries (defensively copied by the caller)
+     * @return an immutable snapshot
+     */
+    public static AsyncContext.Snapshot createSnapshot(Map<String, Object> values) {
+        return DefaultSnapshot.of(new LinkedHashMap<>(values));
+    }
+
+    // ---- Instance methods (AsyncContext interface) -----------------------
+
+    @Override
+    public Object get(String key) {
+        return values.get(normalizeKey(key));
+    }
+
+    @Override
+    public Object getOrDefault(String key, Object defaultValue) {
+        String normalized = normalizeKey(key);
+        Object val = values.get(normalized);
+        return val != null || values.containsKey(normalized) ? val : defaultValue;
+    }
+
+    @Override
+    public Object computeIfAbsent(String key, Function<String, Object> mappingFunction) {
+        Objects.requireNonNull(mappingFunction, "mappingFunction must not be null");
+        String normalized = normalizeKey(key);
+        Object val = values.get(normalized);
+        if (val == null && !values.containsKey(normalized)) {
+            val = mappingFunction.apply(normalized);
+            if (val != null) {
+                values.put(normalized, val);
+            }
+        }
+        return val;
+    }
+
+    @Override
+    public Object getAt(String key) {
+        return get(key);
+    }
+
+    @Override
+    public Object put(String key, Object value) {
+        String normalized = normalizeKey(key);
+        if (value == null) {
+            return values.remove(normalized);
+        }
+        return values.put(normalized, value);
+    }
+
+    @Override
+    public void putAt(String key, Object value) {
+        put(key, value);
+    }
+
+    @Override
+    public Object remove(String key) {
+        return values.remove(normalizeKey(key));
+    }
+
+    @Override
+    public void putAll(Map<?, ?> entries) {
+        if (entries == null || entries.isEmpty()) {
+            return;
+        }
+        for (Map.Entry<?, ?> entry : entries.entrySet()) {
+            put(normalizeKey(entry.getKey()), entry.getValue());
+        }
+    }
+
+    @Override
+    public void clear() {
+        values.clear();
+    }
+
+    @Override
+    public boolean containsKey(String key) {
+        return values.containsKey(normalizeKey(key));
+    }
+
+    @Override
+    public int size() {
+        return values.size();
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return values.isEmpty();
+    }
+
+    @Override
+    public Map<String, Object> snapshot() {
+        return Collections.unmodifiableMap(copyValues());
+    }
+
+    @Override
+    public String toString() {
+        return "AsyncContext" + values;
+    }
+
+    // ---- Internal -------------------------------------------------------
+
+    Map<String, Object> copyValues() {
+        return new LinkedHashMap<>(values);
+    }
+
+    static String normalizeKey(Object key) {
+        return Objects.requireNonNull(key, "context key must not be null").toString();
+    }
+
+    // ---- DefaultSnapshot ------------------------------------------------
+
+    /**
+     * Default implementation of {@link AsyncContext.Snapshot} backed by an
+     * unmodifiable {@link Map}.
+     * <p>
+     * Instances are created via the package-private {@link #of(Map)} factory.
+     * The caller is responsible for ensuring that the supplied map has
+     * already been defensively copied.
+     *
+     * @since 6.0.0
+     */
+    static final class DefaultSnapshot implements AsyncContext.Snapshot {
+
+        private final Map<String, Object> values;
+
+        private DefaultSnapshot(Map<String, Object> values) {
+            this.values = values;
+        }
+
+        /**
+         * Wraps a pre-copied map into an immutable snapshot.
+         */
+        static DefaultSnapshot of(Map<String, Object> preCopied) {
+            return new DefaultSnapshot(Collections.unmodifiableMap(preCopied));
+        }
+
+        @Override
+        public boolean containsKey(String key) {
+            return values.containsKey(normalizeKey(key));
+        }
+
+        @Override
+        public Object get(String key) {
+            return values.get(normalizeKey(key));
+        }
+
+        @Override
+        public Object getOrDefault(String key, Object defaultValue) {
+            String normalized = normalizeKey(key);
+            Object val = values.get(normalized);
+            return val != null || values.containsKey(normalized) ? val : defaultValue;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return values.isEmpty();
+        }
+
+        @Override
+        public int size() {
+            return values.size();
+        }
+
+        @Override
+        public Map<String, Object> asMap() {
+            return values;
+        }
+
+        @Override
+        public AsyncContext.Snapshot with(Map<?, ?> entries) {
+            if (entries == null || entries.isEmpty()) {
+                return this;
+            }
+            Map<String, Object> merged = new LinkedHashMap<>(values);
+            for (Map.Entry<?, ?> entry : entries.entrySet()) {
+                String key = normalizeKey(entry.getKey());
+                Object value = entry.getValue();
+                if (value == null) {
+                    merged.remove(key);
+                } else {
+                    merged.put(key, value);
+                }
+            }
+            return DefaultSnapshot.of(merged);
+        }
+
+        @Override
+        public String toString() {
+            return "AsyncContext.Snapshot" + values;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncScope.java b/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncScope.java
new file mode 100644
index 0000000000..40c1fd4aaa
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/DefaultAsyncScope.java
@@ -0,0 +1,314 @@
+/*
+ *  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.AsyncContext;
+import groovy.concurrent.AsyncScope;
+import groovy.concurrent.Awaitable;
+import groovy.lang.Closure;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.Executor;
+import java.util.function.Supplier;
+
+/**
+ * Default implementation of {@link AsyncScope} providing structured
+ * concurrency with configurable failure policy.
+ *
+ * <h2>Concurrency design</h2>
+ * <p>A dedicated {@code lock} object guards the child task list and the
+ * {@code closed} flag jointly.  This ensures that {@link #async(Closure)}
+ * and {@link #close()} cannot race: no child can be registered after the
+ * scope is marked closed.</p>
+ *
+ * <h3>Register-before-submit protocol</h3>
+ * <p>Child {@link CompletableFuture} instances are created and registered in
+ * the {@code children} list under the lock <em>before</em> the task body is
+ * submitted to the executor.  This eliminates a race window where
+ * {@link #close()} could snapshot the child list between task submission
+ * and registration, causing a submitted task to run unsupervised.  The
+ * protocol guarantees that every child is always joined (or cancelled) by
+ * {@link #close()}, upholding the structured concurrency invariant.</p>
+ *
+ * <h2>Memory management</h2>
+ * <p>Completed children are pruned from the internal list when its size
+ * exceeds {@link #PRUNE_THRESHOLD}, preventing unbounded memory growth
+ * in long-lived scopes.</p>
+ *
+ * <p>This class is an internal implementation detail of the Groovy async
+ * runtime.  User code should program against the {@link AsyncScope}
+ * interface and use its static factory methods.</p>
+ *
+ * @see AsyncScope
+ * @since 6.0.0
+ */
+public final class DefaultAsyncScope implements AsyncScope {
+
+    private static final ScopedLocal<AsyncScope> CURRENT = ScopedLocal.newInstance();
+
+    /**
+     * Pruning threshold: completed children are purged when the list
+     * exceeds this size, keeping memory bounded for long-lived scopes.
+     */
+    private static final int PRUNE_THRESHOLD = 64;
+
+    private final Object lock = new Object();
+    private final List<CompletableFuture<?>> children = new ArrayList<>();
+    private boolean closed;
+    private final Executor executor;
+    private final boolean failFast;
+
+    /**
+     * Creates a new scope with the given executor and fail-fast policy.
+     *
+     * @param executor the executor for child tasks; must not be {@code null}
+     * @param failFast if {@code true}, cancel all siblings when any
+     *                 child fails
+     * @throws NullPointerException if {@code executor} is {@code null}
+     */
+    public DefaultAsyncScope(Executor executor, boolean failFast) {
+        Objects.requireNonNull(executor, "executor must not be null");
+        this.executor = executor;
+        this.failFast = failFast;
+    }
+
+    /**
+     * Creates a new scope with the given executor and fail-fast enabled.
+     *
+     * @param executor the executor for child tasks; must not be {@code null}
+     * @throws NullPointerException if {@code executor} is {@code null}
+     */
+    public DefaultAsyncScope(Executor executor) {
+        this(executor, true);
+    }
+
+    /**
+     * Creates a new scope with the default async executor and fail-fast
+     * enabled.
+     */
+    public DefaultAsyncScope() {
+        this(AsyncSupport.getExecutor(), true);
+    }
+
+    // ---- Static operations (delegated from AsyncScope interface) --------
+
+    /**
+     * Returns the structured async scope currently bound to this thread,
+     * or {@code null} if no scope is active.
+     *
+     * @return the current scope, or {@code null}
+     */
+    public static AsyncScope current() {
+        return CURRENT.orElse(null);
+    }
+
+    /**
+     * Executes the supplier with the given scope installed as the current
+     * structured scope, restoring the previous binding afterwards.
+     *
+     * @param scope    the scope to install; may be {@code null}
+     * @param supplier the action to execute; must not be {@code null}
+     * @param <T>      the result type
+     * @return the supplier's result
+     */
+    public static <T> T withCurrent(AsyncScope scope, Supplier<T> supplier) {
+        Objects.requireNonNull(supplier, "supplier must not be null");
+        return CURRENT.where(scope, supplier);
+    }
+
+    /**
+     * Void overload of {@link #withCurrent(AsyncScope, Supplier)}.
+     *
+     * @param scope  the scope to install; may be {@code null}
+     * @param action the action to execute; must not be {@code null}
+     */
+    public static void withCurrent(AsyncScope scope, Runnable action) {
+        withCurrent(scope, () -> {
+            action.run();
+            return null;
+        });
+    }
+
+    // ---- Instance methods (AsyncScope interface) ------------------------
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> Awaitable<T> async(Closure<T> body) {
+        Objects.requireNonNull(body, "body must not be null");
+        return launchChild(body::call);
+    }
+
+    @Override
+    public <T> Awaitable<T> async(Supplier<T> supplier) {
+        Objects.requireNonNull(supplier, "supplier must not be null");
+        return launchChild(supplier::get);
+    }
+
+    @Override
+    public int getChildCount() {
+        synchronized (lock) {
+            return children.size();
+        }
+    }
+
+    @Override
+    public void cancelAll() {
+        List<CompletableFuture<?>> snapshot;
+        synchronized (lock) {
+            snapshot = new ArrayList<>(children);
+        }
+        for (CompletableFuture<?> child : snapshot) {
+            child.cancel(true);
+        }
+    }
+
+    @Override
+    public void close() {
+        List<CompletableFuture<?>> snapshot;
+        synchronized (lock) {
+            if (closed) return;
+            closed = true;
+            snapshot = new ArrayList<>(children);
+        }
+        Throwable firstError = null;
+        for (CompletableFuture<?> child : snapshot) {
+            try {
+                child.join();
+            } catch (CancellationException ignored) {
+                // Cancelled tasks are silently ignored
+            } catch (CompletionException e) {
+                Throwable cause = AsyncSupport.deepUnwrap(e);
+                if (cause instanceof CancellationException) {
+                    continue;
+                }
+                if (firstError == null) {
+                    firstError = cause;
+                } else {
+                    firstError.addSuppressed(cause);
+                }
+            } catch (Exception e) {
+                if (firstError == null) {
+                    firstError = e;
+                } else {
+                    firstError.addSuppressed(e);
+                }
+            }
+        }
+        if (firstError != null) {
+            if (firstError instanceof RuntimeException re) throw re;
+            if (firstError instanceof Error err) throw err;
+            throw new RuntimeException(firstError);
+        }
+    }
+
+    // ---- Internal -------------------------------------------------------
+
+    /**
+     * Core child-launch logic shared by both {@link #async(Closure)} and
+     * {@link #async(Supplier)}.
+     *
+     * <h3>Register-before-submit protocol</h3>
+     * <p>The {@link CompletableFuture} is created and registered in
+     * {@code children} <em>before</em> the task is submitted to the
+     * executor.  This eliminates a race window that would otherwise
+     * exist if the task were submitted first: {@link #close()} could
+     * snapshot the children list between task submission and
+     * registration, causing the submitted task to run unsupervised —
+     * violating the structured concurrency guarantee that all child
+     * tasks complete (or are cancelled) before the scope exits.</p>
+     *
+     * <h3>Fail-fast callback</h3>
+     * <p>The fail-fast {@code whenComplete} callback is registered inside
+     * the {@code lock} so that no concurrent {@code async()} call can add
+     * a child between the callback's {@code cancelAllLocked()} sweep and
+     * its own lock release.  Java's reentrant {@code synchronized} ensures
+     * this is safe even when the callback fires synchronously (i.e., the
+     * future has already completed when {@code whenComplete} is called).</p>
+     */
+    private <T> Awaitable<T> launchChild(Supplier<T> task) {
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+
+        // 1. Create the CF first — not yet wired to any computation.
+        CompletableFuture<T> cf = new CompletableFuture<>();
+
+        // 2. Register under lock BEFORE submitting work to the executor.
+        //    This guarantees close() will always join this CF.
+        synchronized (lock) {
+            if (closed) {
+                cf.cancel(true);
+                throw new IllegalStateException(
+                        "AsyncScope is closed — cannot launch new tasks");
+            }
+            pruneCompleted();
+            children.add(cf);
+            if (failFast) {
+                cf.whenComplete((v, err) -> {
+                    if (err != null) {
+                        synchronized (lock) {
+                            if (!closed) cancelAllLocked();
+                        }
+                    }
+                });
+            }
+        }
+
+        // 3. Submit the task and wire its outcome to the pre-registered CF.
+        executor.execute(() -> {
+            try {
+                T result = withCurrent(this, () ->
+                        AsyncContext.withSnapshot(contextSnapshot, task));
+                cf.complete(result);
+            } catch (Throwable t) {
+                cf.completeExceptionally(t);
+            }
+        });
+
+        return GroovyPromise.of(cf);
+    }
+
+    private void cancelAllLocked() {
+        for (CompletableFuture<?> child : children) {
+            child.cancel(true);
+        }
+    }
+
+    /**
+     * Removes completed futures to prevent unbounded memory growth.
+     * Must be called while holding {@code lock}.
+     */
+    private void pruneCompleted() {
+        if (children.size() >= PRUNE_THRESHOLD) {
+            children.removeIf(CompletableFuture::isDone);
+        }
+    }
+
+    @Override
+    public String toString() {
+        synchronized (lock) {
+            return "AsyncScope[children=" + children.size()
+                    + ", closed=" + closed
+                    + ", failFast=" + failFast + "]";
+        }
+    }
+}
diff --git a/src/main/java/org/apache/groovy/runtime/async/FlowPublisherAdapter.java b/src/main/java/org/apache/groovy/runtime/async/FlowPublisherAdapter.java
new file mode 100644
index 0000000000..cf6770cf7f
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/FlowPublisherAdapter.java
@@ -0,0 +1,415 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.runtime.async;
+
+import groovy.concurrent.AsyncStream;
+import groovy.concurrent.Awaitable;
+import groovy.concurrent.AwaitableAdapter;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Bridges JDK {@link Flow.Publisher} instances into Groovy's
+ * {@link Awaitable}/{@link AsyncStream} world.
+ *
+ * <h2>Registration</h2>
+ * This adapter is auto-discovered by the
+ * {@link groovy.concurrent.AwaitableAdapterRegistry} via the
+ * {@code META-INF/services/groovy.concurrent.AwaitableAdapter} file.
+ * It handles any object that implements {@link Flow.Publisher}.
+ *
+ * <h2>Adaptation modes</h2>
+ * <ul>
+ *   <li><b>Single-value</b> ({@code await publisher}):
+ *       subscribes, takes the first {@code onNext} item, cancels the
+ *       upstream subscription, and completes the returned {@link Awaitable}.</li>
+ *   <li><b>Multi-value</b> ({@code for await (item in publisher)}):
+ *       wraps the publisher into an {@link AsyncStream} backed by a
+ *       bounded {@link LinkedBlockingQueue}, providing natural
+ *       back-pressure by requesting one item at a time.</li>
+ * </ul>
+ *
+ * <h2>Thread safety</h2>
+ * <p>All subscriber callbacks ({@code onSubscribe}, {@code onNext},
+ * {@code onError}, {@code onComplete}) are safe for invocation from
+ * any thread.  Subscription references use {@link AtomicReference}
+ * for safe publication and race-free cancellation.  The close path
+ * uses a CAS on an {@link AtomicBoolean} to guarantee exactly-once
+ * cleanup semantics.</p>
+ *
+ * <h2>Reactive Streams compliance</h2>
+ * <p>This adapter follows the Reactive Streams specification
+ * (JDK {@link Flow} variant) rules:</p>
+ * <ul>
+ *   <li>§2.5 — duplicate {@code onSubscribe} cancels the second subscription</li>
+ *   <li>§2.13 — {@code null} items in {@code onNext} are rejected immediately</li>
+ *   <li>All signals ({@code onNext}, {@code onError}, {@code onComplete}) use
+ *       blocking {@code put()} with a non-blocking {@code offer()} fallback
+ *       when the publisher thread is interrupted — this prevents both silent
+ *       item loss and consumer deadlock even under unexpected interrupts</li>
+ *   <li>Terminal callbacks enqueue an explicit terminal signal
+ *       ({@code onError}/{@code onComplete}) before the consumer observes
+ *       end-of-stream.  A dedicated {@link AtomicBoolean} suppresses
+ *       post-terminal {@code onNext} calls from non-compliant publishers
+ *       without racing the consumer into a premature normal completion.</li>
+ *   <li>Back-pressure is enforced by requesting exactly one item after
+ *       each consumed element; demand is signalled <em>before</em>
+ *       {@code moveNext()} returns, preventing livelock when producer and
+ *       consumer share the same thread pool</li>
+ * </ul>
+ *
+ * @see groovy.concurrent.AwaitableAdapterRegistry
+ * @see AsyncStream
+ * @since 6.0.0
+ */
+public class FlowPublisherAdapter implements AwaitableAdapter {
+
+    /**
+     * Returns {@code true} if the given type is assignable to
+     * {@link Flow.Publisher}, enabling single-value {@code await}.
+     *
+     * @param type the source type to check
+     * @return {@code true} if this adapter can handle the type
+     */
+    @Override
+    public boolean supportsAwaitable(Class<?> type) {
+        return Flow.Publisher.class.isAssignableFrom(type);
+    }
+
+    /**
+     * Returns {@code true} if the given type is assignable to
+     * {@link Flow.Publisher}, enabling multi-value {@code for await}.
+     *
+     * @param type the source type to check
+     * @return {@code true} if this adapter can produce an async stream
+     */
+    @Override
+    public boolean supportsAsyncStream(Class<?> type) {
+        return Flow.Publisher.class.isAssignableFrom(type);
+    }
+
+    /**
+     * Converts a {@link Flow.Publisher} to a single-value {@link Awaitable}
+     * by subscribing and taking the first emitted item.
+     *
+     * @param source the publisher instance
+     * @param <T>    the element type
+     * @return an awaitable that resolves to the first emitted value
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> Awaitable<T> toAwaitable(Object source) {
+        return publisherToAwaitable((Flow.Publisher<T>) source);
+    }
+
+    /**
+     * Converts a {@link Flow.Publisher} to a multi-value {@link AsyncStream}
+     * for use with {@code for await} loops.
+     *
+     * @param source the publisher instance
+     * @param <T>    the element type
+     * @return an async stream that yields publisher items
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> AsyncStream<T> toAsyncStream(Object source) {
+        return publisherToAsyncStream((Flow.Publisher<T>) source);
+    }
+
+    // ---- Single-value adaptation (await publisher) ----
+
+    /**
+     * Subscribes to the publisher, takes the <em>first</em> emitted item,
+     * cancels the upstream subscription, and returns a completed
+     * {@link Awaitable}.
+     *
+     * <p>If the publisher completes without emitting any item,
+     * the returned awaitable resolves to {@code null}.</p>
+     *
+     * @param publisher the upstream publisher
+     * @param <T>       the element type
+     * @return an awaitable that completes with the first emitted value
+     */
+    private <T> Awaitable<T> publisherToAwaitable(Flow.Publisher<T> publisher) {
+        CompletableFuture<T> cf = new CompletableFuture<>();
+        // AtomicReference ensures safe publication of the subscription
+        // across the onSubscribe thread and callback threads (§1.3).
+        AtomicReference<Flow.Subscription> subRef = new AtomicReference<>();
+        // Guard against non-compliant publishers that send multiple signals
+        AtomicBoolean done = new AtomicBoolean(false);
+
+        publisher.subscribe(new Flow.Subscriber<T>() {
+            @Override
+            public void onSubscribe(Flow.Subscription s) {
+                // §2.5: reject duplicate subscriptions
+                if (!subRef.compareAndSet(null, s)) {
+                    s.cancel();
+                    return;
+                }
+                s.request(1);
+            }
+
+            @Override
+            public void onNext(T item) {
+                // §2.13: null items are spec violations
+                if (item == null) {
+                    onError(new NullPointerException(
+                            "Flow.Publisher onNext received null (Reactive Streams §2.13)"));
+                    return;
+                }
+                if (done.compareAndSet(false, true)) {
+                    cf.complete(item);
+                    Flow.Subscription sub = subRef.getAndSet(null);
+                    if (sub != null) sub.cancel();
+                }
+            }
+
+            @Override
+            public void onError(Throwable t) {
+                if (done.compareAndSet(false, true)) {
+                    // §2.13 requires non-null, but defend against non-compliant publishers
+                    cf.completeExceptionally(t != null ? t
+                            : new NullPointerException("onError called with null (Reactive Streams §2.13 violation)"));
+                    // Cancel subscription to release resources (idempotent per §3.7)
+                    Flow.Subscription sub = subRef.getAndSet(null);
+                    if (sub != null) sub.cancel();
+                }
+            }
+
+            @Override
+            public void onComplete() {
+                // Publisher completed before emitting — resolve to null
+                if (done.compareAndSet(false, true)) {
+                    cf.complete(null);
+                    // Mirror onNext/onError cleanup for prompt resource release.
+                    Flow.Subscription sub = subRef.getAndSet(null);
+                    if (sub != null) sub.cancel();
+                }
+            }
+        });
+
+        return new GroovyPromise<>(cf);
+    }
+
+    // ---- Multi-value adaptation (for await publisher) ----
+
+    /**
+     * Wraps a {@link Flow.Publisher} into an {@link AsyncStream},
+     * providing a pull-based iteration interface over a push-based source.
+     *
+     * <p>Back-pressure is enforced by requesting exactly one item after
+     * each consumed element.  Demand is signalled <em>before</em> the
+     * consumer's {@code moveNext()} awaitable completes, so the publisher
+     * can begin producing the next value while the consumer processes the
+     * current one — this prevents livelock when producer and consumer
+     * share the same thread pool.</p>
+     *
+     * <p><b>Resource management:</b> When the consumer calls
+     * {@link AsyncStream#close()} (e.g. via {@code break} in a
+     * {@code for await} loop), the upstream subscription is cancelled
+     * and a completion sentinel is injected to unblock any pending
+     * {@code moveNext()} call.</p>
+     *
+     * @param publisher the upstream publisher
+     * @param <T>       the element type
+     * @return an async stream that yields publisher items
+     */
+    private <T> AsyncStream<T> publisherToAsyncStream(Flow.Publisher<T> publisher) {
+        FlowAsyncStream<T> stream = new FlowAsyncStream<>();
+        publisher.subscribe(stream.newSubscriber());
+        return stream;
+    }
+
+    /**
+     * Named implementation of {@link AsyncStream} that extends
+     * {@link AbstractAsyncStream} to bridge a push-based
+     * {@link Flow.Publisher} into a pull-based async stream.
+     *
+     * <p>Inherits the common {@link AbstractAsyncStream#moveNext() moveNext()}/
+     * {@link AbstractAsyncStream#getCurrent() getCurrent()}/
+     * {@link AbstractAsyncStream#close() close()} template and overrides:</p>
+     * <ul>
+     *   <li>{@link #beforeTake()} — drains remaining signals before returning
+     *       false (checks {@code closed && queue.isEmpty()})</li>
+     *   <li>{@link #afterValueConsumed()} — signals back-pressure demand via
+     *       {@code Subscription.request(1)}</li>
+     *   <li>{@link #onClose()} — cancels the upstream subscription, drains
+     *       the queue, and injects a {@link #COMPLETE} sentinel to unblock
+     *       any pending {@code moveNext()}</li>
+     * </ul>
+     *
+     * <p>The internal bounded queue (capacity {@value QUEUE_CAPACITY})
+     * absorbs minor timing jitter.  Signals use blocking {@code put()}
+     * for normal delivery with a non-blocking {@code offer()} fallback
+     * when the publisher thread is interrupted — ensuring no items or
+     * terminal events are silently dropped.</p>
+     *
+     * @param <T> the element type
+     */
+    private static final class FlowAsyncStream<T> extends AbstractAsyncStream<T> {
+
+        /**
+         * Queue capacity for the push→pull bridge.  With one-at-a-time
+         * demand ({@code request(1)} per consumed element), a well-behaved
+         * publisher will never enqueue more than one value at a time.
+         * A capacity of 2 accommodates the value + a racing terminal
+         * signal without blocking, while keeping memory minimal.
+         */
+        private static final int QUEUE_CAPACITY = 2;
+
+        private final AtomicReference<Flow.Subscription> subRef = new AtomicReference<>();
+
+        /**
+         * Guards idempotency of terminal signal delivery.  Set exactly once
+         * by {@code putTerminalSignal()} (or the {@code onNext()} interrupt
+         * fallback) <em>before</em> the signal is enqueued.  This flag is
+         * intentionally separate from {@link #closed}: it prevents duplicate
+         * or post-terminal signals from entering the queue, while {@code closed}
+         * is set only when the consumer ({@link #moveNext()}) actually consumes
+         * the terminal signal.  Keeping them separate eliminates a TOCTOU race
+         * where the consumer's {@link #beforeTake()} could observe
+         * {@code closed == true} and an empty queue simultaneously — returning
+         * a premature normal completion before the terminal signal is visible.
+         */
+        private final AtomicBoolean terminalSignalQueued = new AtomicBoolean(false);
+
+        FlowAsyncStream() {
+            super(new LinkedBlockingQueue<>(QUEUE_CAPACITY));
+        }
+
+        /**
+         * Creates a new {@link Flow.Subscriber} wired to this stream's
+         * internal queue and lifecycle state.
+         */
+        Flow.Subscriber<T> newSubscriber() {
+            return new Flow.Subscriber<>() {
+                @Override
+                public void onSubscribe(Flow.Subscription s) {
+                    // §2.5: reject duplicate subscriptions
+                    if (!subRef.compareAndSet(null, s)) {
+                        s.cancel();
+                        return;
+                    }
+                    // Double-check: if close() raced before subscription was set,
+                    // cancel immediately to avoid a dangling upstream.
+                    if (closed.get() || terminalSignalQueued.get()) {
+                        cancelSubscription();
+                        return;
+                    }
+                    s.request(1);
+                }
+
+                @Override
+                public void onNext(T item) {
+                    // §2.13: null items are spec violations
+                    if (item == null) {
+                        onError(new NullPointerException(
+                                "Flow.Publisher onNext received null (Reactive Streams §2.13)"));
+                        return;
+                    }
+                    if (closed.get() || terminalSignalQueued.get()) return;
+                    try {
+                        queue.put(new ValueSignal(item));
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        if (!queue.offer(new ValueSignal(item))) {
+                            cancelSubscription();
+                            terminalSignalQueued.set(true);
+                            // Clear the queue to guarantee room for the terminal signal;
+                            // without this, a full queue could leave the consumer blocked
+                            // in take() with no signal to unblock it.
+                            queue.clear();
+                            queue.offer(COMPLETE);
+                        }
+                    }
+                }
+
+                @Override
+                public void onError(Throwable t) {
+                    // §2.13 requires non-null, but defend against non-compliant publishers
+                    Throwable cause = t != null ? t
+                            : new NullPointerException("onError called with null (Reactive Streams §2.13 violation)");
+                    putTerminalSignal(new ErrorSignal(cause));
+                }
+
+                @Override
+                public void onComplete() {
+                    putTerminalSignal(COMPLETE);
+                }
+
+                /**
+                 * Shared logic for {@code onError()} and {@code onComplete()}:
+                 * atomically marks a terminal signal as queued, cancels the
+                 * upstream subscription, and delivers the signal to the
+                 * consumer queue.
+                 *
+                 * <p>This method CASes on {@link #terminalSignalQueued} — not
+                 * on {@link #closed} — to guarantee that the terminal signal
+                 * is visible in the queue before the consumer can observe
+                 * end-of-stream.  {@code closed} is later set by
+                 * {@link AbstractAsyncStream#moveNext()} when it actually
+                 * consumes the terminal signal.
+                 */
+                private void putTerminalSignal(Object signal) {
+                    if (!terminalSignalQueued.compareAndSet(false, true)) return;
+                    cancelSubscription();
+                    try {
+                        queue.put(signal);
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        queue.offer(signal);
+                    }
+                }
+            };
+        }
+
+        @Override
+        protected Awaitable<Boolean> beforeTake() {
+            return (closed.get() && queue.isEmpty()) ? MOVE_NEXT_FALSE : null;
+        }
+
+        @Override
+        protected void afterValueConsumed() {
+            // Signal demand BEFORE returning so the publisher can begin
+            // producing the next value while the consumer processes this
+            // one — prevents livelock when both share a thread pool.
+            Flow.Subscription sub = subRef.get();
+            if (sub != null) sub.request(1);
+        }
+
+        @Override
+        protected void onClose() {
+            cancelSubscription();
+            // Drain the queue and inject a sentinel to unblock a
+            // concurrent moveNext() that may be blocked in take().
+            queue.clear();
+            queue.offer(COMPLETE);
+        }
+
+        private void cancelSubscription() {
+            Flow.Subscription sub = subRef.getAndSet(null);
+            if (sub != null) sub.cancel();
+        }
+    }
+}
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..490f9c6951
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/GroovyPromise.java
@@ -0,0 +1,264 @@
+/*
+ *  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.AsyncContext;
+import groovy.concurrent.Awaitable;
+
+import java.util.Objects;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+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;
+
+    /**
+     * Creates a new {@code GroovyPromise} wrapping the given {@link CompletableFuture}.
+     *
+     * @param future the backing future; must not be {@code null}
+     * @throws NullPointerException if {@code future} is {@code null}
+     */
+    public GroovyPromise(CompletableFuture<T> future) {
+        this.future = Objects.requireNonNull(future, "future must not be null");
+    }
+
+    /**
+     * Creates a {@code GroovyPromise} wrapping the given {@link CompletableFuture}.
+     * <p>
+     * This is a convenience factory that delegates to
+     * {@link #GroovyPromise(CompletableFuture)}.
+     *
+     * @param future the backing future; must not be {@code null}
+     * @param <T>    the result type
+     * @return a new {@code GroovyPromise} wrapping {@code future}
+     * @throws NullPointerException if {@code future} is {@code null}
+     */
+    public static <T> GroovyPromise<T> of(CompletableFuture<T> future) {
+        return new GroovyPromise<>(future);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Includes a synchronous completion fast-path: if the underlying
+     * {@link CompletableFuture} is already done, the result is extracted
+     * via {@link CompletableFuture#join()} which avoids the full
+     * park/unpark machinery of {@link CompletableFuture#get()}.
+     * This optimisation provides a synchronous completion fast-path
+     * and eliminates unnecessary thread state transitions
+     * on the hot path where async operations complete before being awaited.
+     * <p>
+     * If the future was cancelled, the original {@link CancellationException} is
+     * unwrapped from the JDK 23+ wrapper for cross-version consistency.
+     */
+    @Override
+    public T get() throws InterruptedException, ExecutionException {
+        // Fast path: already completed — skip wait queue and thread parking
+        if (future.isDone()) {
+            return getCompleted();
+        }
+        try {
+            return future.get();
+        } catch (CancellationException e) {
+            throw unwrapCancellation(e);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Includes a synchronous completion fast-path for already-done futures,
+     * consistent with the zero-argument {@link #get()} overload.
+     * Unwraps JDK 23+ {@link CancellationException} wrappers for consistency.
+     */
+    @Override
+    public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        // Fast path: already completed — skip wait queue and thread parking
+        if (future.isDone()) {
+            return getCompleted();
+        }
+        try {
+            return future.get(timeout, unit);
+        } catch (CancellationException e) {
+            throw unwrapCancellation(e);
+        }
+    }
+
+    /**
+     * Extracts the result from an already-completed future using
+     * {@link CompletableFuture#join()}, which is cheaper than
+     * {@link CompletableFuture#get()} for completed futures because it
+     * bypasses the interruptible wait path.
+     * <p>
+     * Translates {@link CompletionException} to {@link ExecutionException}
+     * to preserve the {@code get()} contract.
+     */
+    private T getCompleted() throws ExecutionException {
+        try {
+            return future.join();
+        } catch (CompletionException e) {
+            throw new ExecutionException(AsyncSupport.deepUnwrap(e));
+        } catch (CancellationException e) {
+            throw unwrapCancellation(e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @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);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isCancelled() {
+        return future.isCancelled();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isCompletedExceptionally() {
+        return future.isCompletedExceptionally();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Returns a new {@code GroovyPromise} whose result is obtained by applying
+     * the given function to this promise's result.  The current
+     * {@link AsyncContext} snapshot is captured when the continuation is
+     * registered and restored when it executes.
+     */
+    @Override
+    public <U> Awaitable<U> then(Function<? super T, ? extends U> fn) {
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        return new GroovyPromise<>(future.thenApply(value ->
+                AsyncContext.withSnapshot(contextSnapshot, () -> fn.apply(value))));
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Returns a new {@code GroovyPromise} that is the result of composing this
+     * promise with the async function, enabling flat-mapping of awaitables.
+     * The current {@link AsyncContext} snapshot is captured when the
+     * continuation is registered and restored when it executes.
+     */
+    @Override
+    public <U> Awaitable<U> thenCompose(Function<? super T, ? extends Awaitable<U>> fn) {
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        return new GroovyPromise<>(future.thenCompose(value ->
+                AsyncContext.withSnapshot(contextSnapshot,
+                        () -> fn.apply(value).toCompletableFuture())));
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Returns a new {@code GroovyPromise} that handles exceptions thrown by this promise.
+     * The throwable passed to the handler is deeply unwrapped to strip JDK
+     * wrapper layers ({@code CompletionException}, {@code ExecutionException}).
+     * The handler runs with the {@link AsyncContext} snapshot that was active
+     * when the recovery continuation was registered.
+     */
+    @Override
+    public Awaitable<T> exceptionally(Function<Throwable, ? extends T> fn) {
+        AsyncContext.Snapshot contextSnapshot = AsyncContext.capture();
+        return new GroovyPromise<>(future.exceptionally(t ->
+                AsyncContext.withSnapshot(contextSnapshot, () -> {
+                    Throwable cause = AsyncSupport.deepUnwrap(t);
+                    return fn.apply(cause);
+                })));
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Returns the underlying {@link CompletableFuture} for interop with JDK APIs.
+     */
+    @Override
+    public CompletableFuture<T> toCompletableFuture() {
+        return future;
+    }
+
+    /**
+     * JDK 23+ wraps a stored {@link CancellationException} in a new instance
+     * with the generic message {@code "get"} when {@link CompletableFuture#get()}
+     * is called.  Unwrap it here so Groovy users consistently observe the
+     * original cancellation message and cause chain across all supported JDKs.
+     */
+    private static CancellationException unwrapCancellation(CancellationException exception) {
+        Throwable cause = exception.getCause();
+        return cause instanceof CancellationException ce ? ce : exception;
+    }
+
+    /**
+     * Returns a human-readable representation showing the promise state:
+     * {@code GroovyPromise{pending}}, {@code GroovyPromise{completed}}, or
+     * {@code GroovyPromise{failed}}.
+     */
+    @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/apache/groovy/runtime/async/ScopedLocal.java b/src/main/java/org/apache/groovy/runtime/async/ScopedLocal.java
new file mode 100644
index 0000000000..4275ef93a3
--- /dev/null
+++ b/src/main/java/org/apache/groovy/runtime/async/ScopedLocal.java
@@ -0,0 +1,525 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.runtime.async;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A thread-scoped value holder that abstracts over {@link ThreadLocal}
+ * (JDK&nbsp;&lt;&nbsp;25) and {@code java.lang.ScopedValue} (JDK&nbsp;25+),
+ * presenting a unified API modelled after {@code ScopedValue}.
+ *
+ * <h2>Backend selection</h2>
+ * <p>On JDK&nbsp;25+, where {@code java.lang.ScopedValue} is available,
+ * this class delegates to the {@code ScopedValue} API, which is optimized
+ * for virtual threads: bindings are automatically inherited by child
+ * virtual threads, require no explicit cleanup, and impose lower
+ * per-thread memory overhead than {@code ThreadLocal}.</p>
+ * <p>On earlier JDK versions, the implementation falls back to a
+ * conventional {@code ThreadLocal} with a save-and-restore pattern
+ * inside {@code try}/{@code finally} blocks.</p>
+ *
+ * <h2>API overview</h2>
+ * <p>The API mirrors {@code ScopedValue} as closely as possible:</p>
+ * <ul>
+ *   <li>{@link #get()}, {@link #orElse(Object)}, {@link #isBound()} —
+ *       query the current binding.</li>
+ *   <li>{@link #where(ScopedLocal, Object)} — create a {@link Carrier}
+ *       that can bind one or more values for a scoped execution.</li>
+ *   <li>{@link Carrier#run(Runnable)}, {@link Carrier#call(Supplier)} —
+ *       execute code with the bindings active.</li>
+ * </ul>
+ *
+ * <h2>Usage</h2>
+ * <pre>{@code
+ * private static final ScopedLocal<String> REQUEST_ID =
+ *         ScopedLocal.newInstance();
+ *
+ * // Single binding:
+ * ScopedLocal.where(REQUEST_ID, "req-42").run(() -> {
+ *     assert "req-42".equals(REQUEST_ID.get());
+ * });
+ *
+ * // Chained bindings:
+ * ScopedLocal.where(REQUEST_ID, "req-1")
+ *            .where(TENANT_ID, "acme")
+ *            .call(() -> handleRequest());
+ *
+ * // Convenience instance methods:
+ * String result = REQUEST_ID.where("req-7", () -> process());
+ *
+ * // With an initial-value supplier:
+ * private static final ScopedLocal<MyCtx> CTX =
+ *         ScopedLocal.withInitial(MyCtx::new);
+ * }</pre>
+ *
+ * @param <T> the type of the scoped value
+ * @since 6.0.0
+ */
+public abstract class ScopedLocal<T> {
+    private static final boolean SCOPED_VALUE_AVAILABLE;
+    private static final MethodHandle SV_NEW_INSTANCE;   // () → Object  [adapted from ScopedValue.newInstance()]
+    private static final MethodHandle SV_WHERE;          // (Object, Object) → Object  [adapted from ScopedValue.where(SV, Object)]
+    private static final MethodHandle SV_GET;            // (Object) → Object  [adapted from scopedValue.get()]
+    private static final MethodHandle SV_IS_BOUND;       // (Object) → boolean  [adapted from scopedValue.isBound()]
+    private static final MethodHandle CARRIER_RUN;       // (Object, Runnable) → void  [adapted from carrier.run(Runnable)]
+
+    static {
+        boolean available = false;
+        MethodHandle newInstance = null, where = null, get = null;
+        MethodHandle isBound = null;
+        MethodHandle carrierRun = null;
+
+        // ScopedValue is available as a preview API since JDK 21, but
+        // was only finalized (non-preview) in JDK 25.  We require the
+        // finalized version to avoid depending on preview semantics
+        // that may change between releases.
+        if (Runtime.version().feature() >= 25) {
+            try {
+                Class<?> svClass = Class.forName("java.lang.ScopedValue");
+                Class<?> carrierCls = Class.forName("java.lang.ScopedValue$Carrier");
+                MethodHandles.Lookup lookup = MethodHandles.publicLookup();
+
+                // Look up the raw handles, then adapt their types so that
+                // every ScopedValue/Carrier parameter and return type is
+                // erased to Object.  This allows call sites to use
+                // invokeExact — which the JVM can inline aggressively —
+                // without needing compile-time access to the ScopedValue class.
+                newInstance = lookup.findStatic(svClass, "newInstance",
+                        MethodType.methodType(svClass))
+                        .asType(MethodType.methodType(Object.class));
+
+                where = lookup.findStatic(svClass, "where",
+                        MethodType.methodType(carrierCls, svClass, Object.class))
+                        .asType(MethodType.methodType(Object.class, Object.class, Object.class));
+
+                get = lookup.findVirtual(svClass, "get",
+                        MethodType.methodType(Object.class))
+                        .asType(MethodType.methodType(Object.class, Object.class));
+
+                isBound = lookup.findVirtual(svClass, "isBound",
+                        MethodType.methodType(boolean.class))
+                        .asType(MethodType.methodType(boolean.class, Object.class));
+
+                carrierRun = lookup.findVirtual(carrierCls, "run",
+                        MethodType.methodType(void.class, Runnable.class))
+                        .asType(MethodType.methodType(void.class, Object.class, Runnable.class));
+
+                available = true;
+            } catch (Throwable t) {
+                final Logger logger = Logger.getLogger(ScopedLocal.class.getName());
+                if (logger.isLoggable(Level.FINE)) {
+                    logger.fine("ScopedValue not available on JDK "
+                            + Runtime.version().feature()
+                            + "; using ThreadLocal fallback: " + t);
+                }
+            }
+        }
+
+        SCOPED_VALUE_AVAILABLE = available;
+        SV_NEW_INSTANCE = newInstance;
+        SV_WHERE = where;
+        SV_GET = get;
+        SV_IS_BOUND = isBound;
+        CARRIER_RUN = carrierRun;
+    }
+
+    // ---- Factory methods ----
+
+    /**
+     * Creates a new {@code ScopedLocal} with no initial value.
+     * {@link #get()} throws {@link NoSuchElementException} when no
+     * binding is active; use {@link #orElse(Object)} for a safe
+     * default, or check {@link #isBound()} first.
+     *
+     * @param <T> the value type
+     * @return a new unbound scoped-local instance
+     */
+    public static <T> ScopedLocal<T> newInstance() {
+        if (SCOPED_VALUE_AVAILABLE) {
+            return new ScopedValueImpl<>();
+        }
+        return new ThreadLocalImpl<>();
+    }
+
+    /**
+     * Creates a new {@code ScopedLocal} whose {@link #get()} method
+     * returns a lazily-initialized default when no explicit binding
+     * exists.  The supplier is invoked at most once per thread and
+     * the result is cached, analogous to
+     * {@link ThreadLocal#withInitial(Supplier)}.
+     *
+     * @param <T>             the value type
+     * @param initialSupplier supplies the default value; must not be
+     *                        {@code null}
+     * @return a new scoped-local instance with a default supplier
+     */
+    public static <T> ScopedLocal<T> withInitial(Supplier<T> initialSupplier) {
+        Objects.requireNonNull(initialSupplier, "initialSupplier must not be null");
+        if (SCOPED_VALUE_AVAILABLE) {
+            return new ScopedValueImpl<>(initialSupplier);
+        }
+        return new ThreadLocalImpl<>(initialSupplier);
+    }
+
+    // ---- Static binding (mirrors ScopedValue.where) ----
+
+    /**
+     * Creates a {@link Carrier} that binds {@code key} to {@code value}.
+     * The binding takes effect when
+     * {@link Carrier#run(Runnable) Carrier.run()} or
+     * {@link Carrier#call(Supplier) Carrier.call()} is invoked.
+     * Multiple bindings can be chained via
+     * {@link Carrier#where(ScopedLocal, Object)}.
+     *
+     * @param <T>   the value type
+     * @param key   the scoped-local to bind
+     * @param value the value to bind; may be {@code null}
+     * @return a carrier holding the binding
+     */
+    public static <T> Carrier where(ScopedLocal<T> key, T value) {
+        Objects.requireNonNull(key, "key must not be null");
+        return new Carrier(key, value, null);
+    }
+
+    // ---- Accessors ----
+
+    /**
+     * Returns the value bound to this scoped-local on the current
+     * thread.  If created with {@link #withInitial(Supplier)}, the
+     * initial value is returned (and cached) when no explicit binding
+     * is active.  Otherwise, throws {@link NoSuchElementException}.
+     *
+     * @return the current value
+     * @throws NoSuchElementException if no value is bound and no
+     *                                initial supplier was provided
+     */
+    public abstract T get();
+
+    /**
+     * Returns the value bound to this scoped-local, or {@code other}
+     * if no explicit binding and no initial supplier are active.
+     *
+     * @param other the fallback value
+     * @return the current value or {@code other}
+     */
+    public abstract T orElse(T other);
+
+    /**
+     * Returns {@code true} if an explicit binding is active or an
+     * initial supplier was provided.
+     *
+     * @return whether a value is available via {@link #get()}
+     */
+    public abstract boolean isBound();
+
+    // ---- Convenience instance methods ----
+
+    /**
+     * Binds this scoped-local to {@code value} for the duration of
+     * the supplier, then restores the previous binding.
+     *
+     * @param <R>      the result type
+     * @param value    the value to bind; may be {@code null}
+     * @param supplier the action to execute with the binding active
+     * @return the supplier's result
+     */
+    public <R> R where(T value, Supplier<R> supplier) {
+        return ScopedLocal.where(this, value).call(supplier);
+    }
+
+    /**
+     * Binds this scoped-local to {@code value} for the duration of
+     * the action, then restores the previous binding.
+     *
+     * @param value  the value to bind; may be {@code null}
+     * @param action the action to execute with the binding active
+     */
+    public void where(T value, Runnable action) {
+        ScopedLocal.where(this, value).run(action);
+    }
+
+    // ---- Internal ----
+
+    /**
+     * Binds this scoped-local within a scope.  Called by
+     * {@link Carrier#run(Runnable)} and {@link Carrier#call(Supplier)}.
+     */
+    abstract void bind(Object value, Runnable action);
+
+    @SuppressWarnings("unchecked")
+    static <E extends Throwable> void sneakyThrow(Throwable t) throws E {
+        throw (E) t;
+    }
+
+    // ================================================================
+    //  Carrier — mirrors ScopedValue.Carrier
+    // ================================================================
+
+    /**
+     * An immutable set of scoped-local bindings that can be applied
+     * atomically for the duration of a {@link Runnable} or
+     * {@link Supplier}.  Obtain via
+     * {@link ScopedLocal#where(ScopedLocal, Object)}.
+     *
+     * <p>Carriers are immutable; calling {@link #where(ScopedLocal, Object)}
+     * returns a new carrier that includes the additional binding.</p>
+     *
+     * @since 6.0.0
+     */
+    public static final class Carrier {
+
+        private final ScopedLocal<?> key;
+        private final Object value;
+        private final Carrier prev;
+
+        Carrier(ScopedLocal<?> key, Object value, Carrier prev) {
+            this.key = key;
+            this.value = value;
+            this.prev = prev;
+        }
+
+        /**
+         * Adds another binding to this carrier, returning a new
+         * carrier that includes all previous bindings plus the new one.
+         *
+         * @param <T>   the value type
+         * @param key   the scoped-local to bind
+         * @param value the value; may be {@code null}
+         * @return a new carrier with the additional binding
+         */
+        public <T> Carrier where(ScopedLocal<T> key, T value) {
+            Objects.requireNonNull(key, "key must not be null");
+            return new Carrier(key, value, this);
+        }
+
+        /**
+         * Executes the action with all bindings in this carrier active.
+         * Bindings are applied in the order they were added and
+         * automatically restored when the action completes (normally or
+         * via exception).
+         *
+         * @param action the action to execute
+         */
+        public void run(Runnable action) {
+            Objects.requireNonNull(action, "action must not be null");
+            execute(action);
+        }
+
+        /**
+         * Executes the supplier with all bindings in this carrier active
+         * and returns its result.
+         *
+         * @param <R>      the result type
+         * @param supplier the supplier to execute
+         * @return the supplier's result
+         */
+        public <R> R call(Supplier<R> supplier) {
+            Objects.requireNonNull(supplier, "supplier must not be null");
+            Object[] result = new Object[1];
+            execute(() -> result[0] = supplier.get());
+            @SuppressWarnings("unchecked")
+            R r = (R) result[0];
+            return r;
+        }
+
+        private void execute(Runnable action) {
+            if (prev == null) {
+                key.bind(value, action);
+            } else {
+                prev.execute(() -> key.bind(value, action));
+            }
+        }
+    }
+
+    // ================================================================
+    //  ThreadLocal-based implementation (JDK < 25)
+    // ================================================================
+
+    private static final class ThreadLocalImpl<T> extends ScopedLocal<T> {
+
+        // Distinguishes "not set" from an explicit null binding
+        private static final Object UNSET = new Object();
+        private static final Object NULL_SENTINEL = new Object();
+
+        private final ThreadLocal<Object> delegate =
+                ThreadLocal.withInitial(() -> UNSET);
+        private final Supplier<T> initialSupplier;
+
+        ThreadLocalImpl() {
+            this.initialSupplier = null;
+        }
+
+        ThreadLocalImpl(Supplier<T> supplier) {
+            this.initialSupplier = supplier;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public T get() {
+            Object val = delegate.get();
+            if (val != UNSET) {
+                return val == NULL_SENTINEL ? null : (T) val;
+            }
+            if (initialSupplier != null) {
+                T initial = initialSupplier.get();
+                // Cache per-thread (same semantics as ThreadLocal.withInitial)
+                delegate.set(initial == null ? NULL_SENTINEL : initial);
+                return initial;
+            }
+            throw new NoSuchElementException(
+                    "ScopedLocal is not bound and has no initial supplier");
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public T orElse(T other) {
+            Object val = delegate.get();
+            if (val != UNSET) {
+                return val == NULL_SENTINEL ? null : (T) val;
+            }
+            if (initialSupplier != null) {
+                T initial = initialSupplier.get();
+                delegate.set(initial == null ? NULL_SENTINEL : initial);
+                return initial;
+            }
+            return other;
+        }
+
+        @Override
+        public boolean isBound() {
+            return delegate.get() != UNSET || initialSupplier != null;
+        }
+
+        @Override
+        void bind(Object value, Runnable action) {
+            Object encoded = value == null ? NULL_SENTINEL : value;
+            Object previous = delegate.get();
+            delegate.set(encoded);
+            try {
+                action.run();
+            } finally {
+                delegate.set(previous);
+            }
+        }
+    }
+
+    // ================================================================
+    //  ScopedValue-based implementation (JDK 25+)
+    // ================================================================
+
+    private static final class ScopedValueImpl<T> extends ScopedLocal<T> {
+
+        /**
+         * Sentinel for {@code null} bindings.
+         * {@code ScopedValue} does not accept {@code null} values,
+         * so we wrap/unwrap through this sentinel transparently.
+         */
+        private static final Object NULL_SENTINEL = new Object();
+
+        /** The underlying {@code ScopedValue<Object>}. */
+        private final Object scopedValue;
+
+        /** Fallback for the withInitial pattern (per-thread cache). */
+        private final ThreadLocal<T> fallback;
+
+        ScopedValueImpl() {
+            try {
+                this.scopedValue = (Object) SV_NEW_INSTANCE.invokeExact();
+            } catch (Throwable t) {
+                throw new ExceptionInInitializerError(t);
+            }
+            this.fallback = null;
+        }
+
+        ScopedValueImpl(Supplier<T> initialSupplier) {
+            try {
+                this.scopedValue = (Object) SV_NEW_INSTANCE.invokeExact();
+            } catch (Throwable t) {
+                throw new ExceptionInInitializerError(t);
+            }
+            this.fallback = ThreadLocal.withInitial(initialSupplier);
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public T get() {
+            try {
+                if ((boolean) SV_IS_BOUND.invokeExact(scopedValue)) {
+                    Object val = (Object) SV_GET.invokeExact(scopedValue);
+                    return val == NULL_SENTINEL ? null : (T) val;
+                }
+            } catch (Throwable t) {
+                sneakyThrow(t);
+            }
+            if (fallback != null) {
+                return fallback.get();
+            }
+            throw new NoSuchElementException(
+                    "ScopedLocal is not bound and has no initial supplier");
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public T orElse(T other) {
+            try {
+                if ((boolean) SV_IS_BOUND.invokeExact(scopedValue)) {
+                    Object val = (Object) SV_GET.invokeExact(scopedValue);
+                    return val == NULL_SENTINEL ? null : (T) val;
+                }
+            } catch (Throwable t) {
+                sneakyThrow(t);
+            }
+            if (fallback != null) {
+                return fallback.get();
+            }
+            return other;
+        }
+
+        @Override
+        public boolean isBound() {
+            try {
+                if ((boolean) SV_IS_BOUND.invokeExact(scopedValue)) {
+                    return true;
+                }
+            } catch (Throwable t) {
+                sneakyThrow(t);
+            }
+            return fallback != null;
+        }
+
+        @Override
+        void bind(Object value, Runnable action) {
+            Object encoded = value == null ? NULL_SENTINEL : value;
+            try {
+                Object carrier = (Object) SV_WHERE.invokeExact(scopedValue, encoded);
+                CARRIER_RUN.invokeExact(carrier, action);
+            } catch (Throwable t) {
+                sneakyThrow(t);
+            }
+        }
+    }
+}
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..84d5823ad9
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncASTTransformation.java
@@ -0,0 +1,254 @@
+/*
+ *  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} (or {@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}.  The runtime preserves
+ *       {@link groovy.concurrent.AsyncContext} snapshots across those async
+ *       boundaries.</li>
+ *   <li>Methods containing {@code defer} statements are wrapped in a
+ *       try-finally block that executes deferred actions in LIFO order, with
+ *       each deferred closure bound to the async context active at registration
+ *       time</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())
+                || "java.util.concurrent.CompletionStage".equals(originalReturnType.getName())
+                || "java.util.concurrent.Future".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} (or {@code await(expr)}) 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} may be 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..728382ef28
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/AsyncTransformHelper.java
@@ -0,0 +1,427 @@
+/*
+ *  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 org.apache.groovy.runtime.async.AsyncSupport;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.Parameter;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.ClosureExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.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.classX;
+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.  The
+ * generated runtime calls now also inherit async-local context automatically,
+ * so compiler-generated code retains request/trace metadata across thread hops
+ * without special-case AST handling.
+ *
+ * @see AsyncASTTransformation
+ * @since 6.0.0
+ */
+public final class AsyncTransformHelper {
+
+    // ---- internal constants (all private) --------------------------------
+
+    private static final String ASYNC_SUPPORT_CLASS = AsyncSupport.class.getName();
+    private static final ClassNode ASYNC_SUPPORT_TYPE = ClassHelper.makeWithoutCaching(AsyncSupport.class, false);
+    private static final ClassNode AWAITABLE_TYPE = ClassHelper.makeWithoutCaching(Awaitable.class, false);
+    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 the AST for an {@code await} expression.
+     * <p>
+     * <b>Single-argument:</b> produces {@code AsyncSupport.await(arg)}.
+     * <br>
+     * <b>Multi-argument:</b> when {@code arg} is an {@link ArgumentListExpression}
+     * with two or more entries, produces
+     * {@code AsyncSupport.await(Awaitable.all(arg1, arg2, ..., argN))},
+     * making {@code await(p1, p2, p3)} semantically equivalent to
+     * {@code await Awaitable.all(p1, p2, p3)}.
+     *
+     * @param arg the expression(s) to await — a single expression or a
+     *            pre-assembled {@link ArgumentListExpression}
+     * @return an AST node representing the static call
+     */
+    public static Expression buildAwaitCall(Expression arg) {
+        if (arg instanceof ArgumentListExpression args && args.getExpressions().size() > 1) {
+            // Multi-arg: await(p1, p2, ..., pn) → await(Awaitable.all(p1, p2, ..., pn))
+            Expression allCall = callX(classX(AWAITABLE_TYPE), "all", args);
+            return callX(ASYNC_SUPPORT_TYPE, AWAIT_METHOD, new ArgumentListExpression(allCall));
+        }
+        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
+     * {@code AsyncASTTransformation.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 by
+     * {@link #wrapWithDeferScope(Statement)} during AST transformation.
+     * <p>
+     * The parser validates that {@code defer} only appears inside an async
+     * context (async method or async closure); using it elsewhere is a
+     * compile-time error.
+     *
+     * @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) {
+        return containsStaticCall(code, YIELD_RETURN_METHOD);
+    }
+
+    // ---- 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) {
+        return containsStaticCall(code, DEFER_METHOD);
+    }
+
+    /**
+     * Scans the given statement tree for a static method call to
+     * {@code AsyncSupport.<methodName>()}.  Does <em>not</em> descend
+     * into nested {@link ClosureExpression}s, since each nested closure
+     * manages its own transformation independently.
+     *
+     * @param code       the statement tree to scan
+     * @param methodName the method name to look for on {@code AsyncSupport}
+     * @return {@code true} if at least one matching call is found
+     */
+    private static boolean containsStaticCall(Statement code, String methodName) {
+        boolean[] found = {false};
+        code.visit(new CodeVisitorSupport() {
+            @Override
+            public void visitStaticMethodCallExpression(StaticMethodCallExpression call) {
+                if (methodName.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/resources/META-INF/services/groovy.concurrent.AwaitableAdapter b/src/resources/META-INF/services/groovy.concurrent.AwaitableAdapter
new file mode 100644
index 0000000000..a591ba6a55
--- /dev/null
+++ b/src/resources/META-INF/services/groovy.concurrent.AwaitableAdapter
@@ -0,0 +1,16 @@
+# 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.
+
+org.apache.groovy.runtime.async.FlowPublisherAdapter
diff --git a/src/spec/doc/core-async-await.adoc b/src/spec/doc/core-async-await.adoc
new file mode 100644
index 0000000000..105bb7a77b
--- /dev/null
+++ b/src/spec/doc/core-async-await.adoc
@@ -0,0 +1,2185 @@
+//////////////////////////////////////////
+
+  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#, Kotlin,
+Swift, 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 cleanup actions that run when the method completes (LIFO order)
+* **`AsyncContext`** — propagate request, trace, and tenant metadata across async boundaries (interface with factory methods, `getOrDefault`, and `computeIfAbsent`)
+* **`AsyncScope`** — structured concurrency scopes (interface with factory methods and Supplier-based Java interop)
+* **`Awaitable.go`, `AsyncChannel`, and `Awaitable.any`** — task spawning and channel-based communication primitives
+* **Framework integration** — built-in adapters for `CompletableFuture` and `Future`;
+`Flow.Publisher` support via an auto-discovered runtime adapter;
+drop-in Reactor and RxJava 3 modules (`groovy-reactor`, `groovy-rxjava`) with
+`ServiceLoader`-based auto-discovery; extensible to any framework via the adapter registry
+
+On JDK 21+, async methods automatically leverage
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual threads] for optimal scalability.
+
+[[why-async-await]]
+== Why Groovy Needs Async/Await
+
+Modern applications are overwhelmingly concurrent. Web servers, microservices, data
+pipelines, and interactive UIs all spend most of their time waiting — waiting for
+network responses, database queries, file I/O, or downstream services. The question
+is not _whether_ to write asynchronous code, but _how pleasant the experience will
+be_.
+
+=== The Problem: Callback Complexity on the JVM
+
+Before async/await, JVM developers faced a fundamental tension: synchronous code is
+easy to read but wastes threads, while asynchronous code scales well but becomes
+hard to follow.
+
+Consider a typical three-step workflow — fetch a user ID, look up a name, then load a
+profile — expressed with `CompletableFuture`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_callback_hell,indent=0]
+----
+
+This code works, but the business logic is obscured by the plumbing.
+Each `.thenCompose()` introduces a nesting level, exception recovery is
+separated from the code that causes the exception, and the control flow
+reads inside-out rather than top-to-bottom.
+
+With `async`/`await`, exactly the same logic becomes:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_async_await_clean,indent=0]
+----
+
+The async version reads like a synchronous script. Variables are declared
+at the point of use, the return value is obvious, and there are no
+callbacks, lambdas, or chained combinators.
+
+=== Native Exception Handling
+
+One of the most painful aspects of callback-style async code is exception handling.
+With `CompletableFuture`, exceptions are wrapped in `CompletionException` or
+`ExecutionException`, requiring manual unwrapping. Groovy's `await` _automatically_
+unwraps these layers and re-throws the original exception, making standard
+`try`/`catch` blocks work exactly as they do in synchronous code:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_exception_handling,indent=0]
+----
+<1> The original `IOException` is caught directly — no `CompletionException` wrapper
+
+This _exception transparency_ guarantee means that switching a synchronous method
+to `async` does not change the exception types visible to callers.
+
+=== Async Streams and Back-Pressure
+
+Asynchronous programming isn't limited to single-value futures. Many real-world
+scenarios involve _streams_ of values — sensor readings, paginated API results,
+log events — where values arrive over time. Groovy's `yield return` and
+`for await` offer a first-class solution:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_stream_processing,indent=0]
+----
+
+The producer yields values on demand, and the consumer pulls them with
+`for await`. Internally, the runtime uses a handoff queue between the two, providing
+natural **back-pressure**: the producer blocks on each `yield return` until the
+consumer is ready, preventing unbounded memory growth — all without requiring
+the developer to manage queues, signals, or synchronization.
+
+=== Seamless JVM Ecosystem Integration
+
+Groovy does not force developers to abandon their existing async investments.
+The `await` keyword natively understands `CompletableFuture`, `CompletionStage`,
+`Future`, and `Flow.Publisher` (via an auto-discovered runtime adapter):
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=motivation_interop,indent=0]
+----
+
+For third-party frameworks like RxJava, Reactor, or Spring, Groovy provides
+dedicated adapter modules — `groovy-reactor` and `groovy-rxjava` — that are
+auto-discovered via `ServiceLoader` when placed on the classpath.
+No manual registration is required: simply add the dependency and `await`
+works transparently with `Mono`, `Flux`, `Single`, `Maybe`, `Observable`,
+and `Flowable`.
+
+=== Virtual Threads: Free Scalability
+
+On JDK 21+, every `async` method runs on a
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual thread].
+When the thread blocks on `await`, the JVM automatically parks the virtual thread
+and releases the underlying carrier (OS) thread. This means millions of concurrent
+`async` tasks can coexist on a handful of OS threads — achieving the same practical
+scalability as compiler-generated state machines in C# or Kotlin, but with
+_dramatically simpler_ compiler implementation and full debugging support
+(complete stack traces, breakpoints, step-through debugging all work unmodified).
+
+On JDK 17–20, Groovy falls back to a managed executor while preserving the
+same user-facing async programming model.
+
+=== Why a Language Feature, Not a Library?
+
+GPars, RxJava, and Reactor already provide powerful async toolkits for the JVM. So
+why build async/await into the _language_?
+
+[cols="1,3"]
+|===
+| Reason | Explanation
+
+| **Readability**
+| `await` makes asynchronous code indistinguishable from synchronous code.
+Libraries can provide combinators, but they cannot change the way `for`, `try`,
+`if`, and assignment statements compose.
+
+| **Exception transparency**
+| Language-level `await` can intercept and unwrap JVM exception wrappers before they
+reach the programmer. A library-level `await()` method cannot prevent `CompletionException`
+from leaking into user code without bytecode manipulation.
+
+| **Control-flow integration**
+| `for await` integrates with the parser and AST. There is no way for a library to add
+new loop syntax. Similarly, `yield return` requires the compiler to rewrite the method
+into a generator — a transformation that no library can perform.
+
+| **`defer` semantics**
+| `defer` requires compiler-generated `try`/`finally` wrapping with LIFO
+execution ordering. This cannot be emulated with a simple method call.
+
+| **Tooling**
+| Syntax highlighting, code completion, and static analysis can recognize `async`,
+`await`, `for await`, and `yield return` as first-class constructs. Library-level
+equivalents are just method calls to the IDE.
+|===
+
+In short, async/await elevates asynchronous programming from a _library concern_
+to a _language concern_, giving Groovy the same expressive power that developers
+have come to expect in JavaScript, C#, Kotlin, and Swift.
+
+=== Why Keywords, Not Global Methods
+
+The previous section explains why async/await belongs in the _language_ rather than
+in a library. A more subtle question follows: even within the language, why introduce
+reserved keywords instead of predefined global methods named `async`, `await`, and
+`defer`? Groovy already provides built-in methods such as `println` — could
+`await(expr)` work the same way?
+
+The fundamental reason is that these constructs introduce _new syntactic forms_ that
+the parser must recognize, and trigger _compile-time code transformations_ that must
+complete before the program runs — requirements that go beyond what any method call
+can express.
+
+**New syntactic forms that methods cannot express.**
+Several async constructs occupy syntactic positions where method calls are
+grammatically impossible:
+
+* `async` serves as a _method modifier_, appearing in the same syntactic position as
+  `static` or `final` — for example, `async def fetch()` or `async String lookup()`.
+  Modifiers are a distinct grammar category that precedes the return type and method
+  name; a method call cannot fill this role.
+* `for await (item in source)` embeds the `await` keyword into the loop header.
+  There is no way for a method call to extend the `for` statement syntax.
+* `yield return expr` is a two-keyword compound statement that signals the compiler
+  to transform the enclosing method into an async generator. Two-word method names
+  are not expressible in standard call syntax.
+* `await expr` — without parentheses — is a dedicated grammar production that can
+  appear in any expression context: assignments, method arguments, return statements,
+  and complex expressions. A parenthesis-free method call would follow Groovy's
+  command expression rules, which have contextual restrictions and can interact
+  unpredictably with surrounding syntax.
+* `async { ... }` must be distinguished from a method call named `async` with a
+  trailing closure argument. The grammar resolves this by placing the `async`
+  closure/lambda rule before general method-call rules, so the parser treats `async`
+  as a keyword rather than a method name. Without keyword status, `async { ... }`
+  would be parsed as an ordinary method invocation, preventing the compiler from
+  applying the async transformation.
+
+These are not edge cases; they are the _primary_ syntax of the feature. If `async`,
+`await`, and `defer` were methods, the most natural forms of async code could not be
+written.
+
+**Compile-time context validation.**
+The compiler maintains an _async context depth_ counter during parsing. When it
+encounters `await`, `defer`, or `yield return` outside an `async` method or closure,
+it reports a compile-time error immediately — before the program runs. A global
+method can only detect misuse at runtime, turning what should be an instant
+compilation error into a latent `IllegalStateException` that may surface only under
+specific execution paths.
+
+**AST rewriting and control-flow transformation.**
+Each keyword triggers a distinct structural transformation of the abstract syntax tree:
+
+* `async` extracts the entire method body into a closure, captures parameters as
+  closure-shared variables, and delegates execution to the async runtime.
+* `defer` injects a synthetic scope variable and wraps the method body in a
+  compiler-generated `try`/`finally` block to guarantee LIFO cleanup execution.
+* `for await` rewrites a for-each loop into a `while` loop driven by
+  `moveNext()`/`getCurrent()` calls, enclosed in `try`/`finally` for automatic
+  stream cleanup.
+* `yield return` signals the compiler to emit a generator that produces an
+  `AsyncStream<T>` instead of an `Awaitable<T>`.
+
+These transformations alter control flow, variable scoping, and exception-handling
+boundaries — operations that a method call, which executes _within_ the existing
+control flow, fundamentally cannot perform.
+
+**Return-type rewriting.**
+When the compiler encounters an `async` method returning `String`, it automatically
+rewrites the return type to `Awaitable<String>` during the canonicalization phase —
+_before_ type checking runs. If the method body contains `yield return`, the return
+type becomes `AsyncStream<String>` instead. Because the keyword triggers this
+rewriting automatically, the programmer declares the natural return type (`String`)
+while the compiler handles the wrapping — keeping method signatures clean and
+allowing the transformed type to flow through Groovy's type checker and IDE tooling
+seamlessly.
+
+**Guaranteed identity.**
+Reserved keywords cannot be shadowed by local variables, overridden in subclasses,
+or replaced by imported methods. This guarantee ensures that `await` carries the
+same semantics in every source file, every class, and every scope. A global method,
+by contrast, can be redefined — accidentally or intentionally — breaking async
+behavior in ways that are difficult to diagnose.
+
+[[async-methods]]
+== Async Methods
+
+An `async` method executes its body asynchronously and returns a
+`groovy.concurrent.Awaitable` — a promise-like abstraction representing a computation that
+may not have completed yet.
+
+=== 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.
+====
+
+=== Multi-Argument `await` — Parallel Awaiting
+
+The parenthesized form supports multiple comma-separated arguments:
+
+[source,groovy]
+----
+def (user, orders) = await(userTask, orderTask)
+----
+
+This is semantically equivalent to `await Awaitable.all(...)` — all arguments are
+awaited concurrently and results are returned as a `List` in the same order as the
+arguments. This is Groovy's counterpart to JavaScript's `await Promise.all(...)`.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=multi_arg_await_paren,indent=0]
+----
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=multi_arg_await_operator,indent=0]
+----
+
+[IMPORTANT]
+====
+Multi-argument `await` requires explicit parentheses: `await(a, b, c)`.
+The unparenthesized form `await a` accepts only a single expression — commas after
+it are interpreted as part of the enclosing syntax (list literals, argument lists,
+variable declarations).  This design preserves the natural meaning of
+`[await a, await b]` (a list of two separately awaited values) and matches the
+convention of all other mainstream languages.
+====
+
+[[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_section]]
+== `for await` — Async Iteration
+
+The `for await` loop iterates over asynchronous data sources (`AsyncStream`, `Flow.Publisher`, `AsyncChannel`, `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`.
+
+The compiler generates a `try`/`finally` block around the loop that calls `close()` on the
+stream when iteration ends — whether by exhaustion, `break`, `return`, or exception.
+This guarantees that upstream resources (producer threads, `Flow.Publisher` subscriptions)
+are released promptly.
+
+=== 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
+handoff queue, providing natural back-pressure: the producer blocks on each `yield return`
+until the consumer is ready for the next value. This design means:
+
+* The generator body runs on its own thread, executing sequentially like any normal method
+* No values are buffered — the producer suspends until the consumer pulls the next item
+* If the consumer stops early (`break`, `return`, exception), the producer thread is
+interrupted, preventing resource leaks
+* The generator can mix `yield return` with `await`, performing I/O between yields
+
+NOTE: `yield return` can only appear inside an `async` method or `async` closure/lambda.
+Using it elsewhere produces a compile-time error:
+_"`yield return` can only be used inside an async method or async closure/lambda"_.
+
+=== 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` — Deferred 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].
+
+NOTE: `defer` can only appear inside an `async` method or `async` closure.  Using it
+elsewhere produces a compile-time error: _"`defer` can only be used inside an async method or
+async closure"_.
+
+=== 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]
+----
+
+=== Async Cleanup in Deferred Blocks
+
+Deferred blocks may also return an `Awaitable`, `CompletionStage`, or `Future`.
+Groovy waits for that asynchronous cleanup to finish before the enclosing async
+method completes, which makes `defer` practical for non-blocking shutdown paths
+such as flushing telemetry, closing async streams, or returning buffered work
+to a channel:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=defer_async_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()`, C#'s `Task.WhenAll()` / `Task.WhenAny()`, and Kotlin's coroutine
+coordination helpers. In particular, `Awaitable.all(...)` follows `Promise.all()`'s
+early-rejection behavior: the combined awaitable fails as soon as one input fails.
+
+=== `all` — Parallel Execution
+
+Wait for all tasks to complete successfully and collect their results. Like JavaScript's
+`Promise.all()`, the combined wait fails immediately on the first error:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all,indent=0]
+----
+
+The fail-fast behavior applies to the combined awaitable itself; slower inputs continue
+running unless you cancel them explicitly or place them in an `AsyncScope`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all_fail_fast,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`, `error`,
+`map()`, and `getOrElse()` for safe inspection and transformation:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_all_settled,indent=0]
+----
+
+=== Choosing a Coordination Helper
+
+Use the combinator that matches the lifecycle you want to enforce:
+
+[cols="1,3,2"]
+|===
+| If you need... | Use... | Why
+
+| every dependency must succeed before you continue
+| `Awaitable.all(...)`
+| It models "all-or-nothing" coordination and fails as soon as one dependency makes the overall result unusable.
+
+| the first completion wins
+| `Awaitable.any(...)`
+| It expresses races, replicas, hedged requests, and "take the earliest answer" workflows.
+
+| the full outcome of every task, including failures
+| `Awaitable.allSettled(...)`
+| It is the right choice for reporting, cleanup, bulk processing, and partial-success workflows where you need every result.
+|===
+
+[[await-result]]
+=== `AwaitResult` — Outcome Wrapper
+
+Each element returned by `allSettled` is an `AwaitResult<T>`. This immutable value type
+carries either a success value or a failure throwable. It follows the
+_value object_ pattern (like Kotlin's `Result`, Rust's `Result<T, E>`),
+providing proper `equals()` / `hashCode()` semantics and functional composition:
+
+* `isSuccess()` / `isFailure()` — check outcome type
+* `value` — the result (throws `IllegalStateException` on failure)
+* `error` — the exception (throws `IllegalStateException` on success)
+* `getOrElse(Function)` — recover from failures with a fallback function
+* `map(Function)` — transform a successful value, propagating failures unchanged
+* `equals(Object)` / `hashCode()` — two instances are equal if they carry the same state and equal values/errors
+* `toString()` — returns `AwaitResult.Success[value]` or `AwaitResult.Failure[error]`
+
+The `map` method enables functional composition chains without explicit unwrapping:
+
+[source,groovy]
+----
+def result = AwaitResult.success("Groovy")
+def length = result.map { it.length() }
+assert length.value == 6
+
+// map on a failure is a no-op — the error passes through
+def failed = AwaitResult.failure(new IOException("err"))
+def mapped = failed.map { it.toString() }
+assert mapped.isFailure()
+assert mapped.error.message == "err"
+----
+
+Value equality means `AwaitResult` instances work naturally in collections and assertions:
+
+[source,groovy]
+----
+assert AwaitResult.success(42) == AwaitResult.success(42)
+assert AwaitResult.success(1) != AwaitResult.success(2)
+
+def set = [AwaitResult.success('a'), AwaitResult.success('a')] as Set
+assert set.size() == 1  // deduplicated by equals/hashCode
+----
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=await_result_api,indent=0]
+----
+
+=== Continuation Helpers
+
+Use `thenAccept`, `whenComplete`, and `handle` when you want continuation-style
+composition rather than a direct `await`. This is especially useful at API boundaries
+where you want to attach logging, recovery, or side effects without immediately
+blocking for the result.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=continuation_helpers,indent=0]
+----
+
+=== `delay` — Non-Blocking Pause
+
+Create an awaitable timer that completes after the specified duration:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=delay_example,indent=0]
+----
+
+=== `orTimeout` / `completeOnTimeout` — Non-Blocking Deadlines
+
+Apply a deadline without blocking the calling thread. `Awaitable.orTimeoutMillis(...)`
+(or `Awaitable.orTimeout(source, duration, unit)` for explicit units)
+fails with `TimeoutException`, while `Awaitable.completeOnTimeoutMillis(...)`
+(or `Awaitable.completeOnTimeout(source, fallback, duration, unit)`) completes with a
+fallback value. The instance forms `orTimeoutMillis(...)` and `completeOnTimeoutMillis(...)`
+provide the same capability on an existing awaitable.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=timeout_combinators,indent=0]
+----
+
+==== Instance Forms: `orTimeoutMillis` and `completeOnTimeoutMillis`
+
+The instance methods `orTimeoutMillis(millis)` and `completeOnTimeoutMillis(fallback, millis)` provide
+a fluent alternative to the static `Awaitable.orTimeoutMillis()` and `Awaitable.completeOnTimeoutMillis()` forms:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=instance_timeout_methods,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`],
+the reactive streams interface standardized in JDK 9. This enables seamless consumption of
+reactive streams from any library that implements the `Flow.Publisher` contract (Project Reactor,
+RxJava 3 via adapters, Vert.x, etc.).
+
+The internal adapter uses a small bounded buffer to bridge the push-based `Publisher`
+model to Groovy's pull-based `AsyncStream` model, preventing unbounded memory growth with
+fast publishers while maintaining throughput.  Back-pressure is enforced by requesting exactly
+one item at a time; demand for the next element is signalled _before_ the consumer's
+`moveNext()` awaitable completes, so the publisher can begin producing the next value while
+the consumer processes the current one.
+
+The examples below use deterministic subscriber-handshake waiting with a bounded timeout
+before submitting values, avoiding timing races and unbounded waits in CI environments.
+
+=== 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]
+----
+
+[NOTE]
+====
+`for await` closes the adapted stream from a compiler-generated `finally` block.
+For `Flow.Publisher` sources, this means an early `break`, `return`, or exception
+cancels the upstream subscription rather than leaving it running in the background.
+This is conceptually similar to JavaScript async iterators' `return()`, C#'s
+`await foreach` disposal, and Swift's `AsyncSequence` early-exit cleanup.
+====
+
+[[adapter-registry]]
+== Adapter Registry
+
+=== Converting External Types with `from()`
+
+The `Awaitable.from(source)` and `AsyncStream.from(source)` static methods are the recommended
+entry points for converting external async types to Groovy's native abstractions. They delegate
+to the `AwaitableAdapterRegistry` internally but provide a more discoverable, user-friendly API:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=from_conversion,indent=0]
+----
+
+`Awaitable.from()` accepts any type supported by a registered adapter: `CompletableFuture`,
+`CompletionStage`, `Future`, `Flow.Publisher`, or any third-party type with a custom adapter.
+If the source is already an `Awaitable`, it is returned as-is. `AsyncStream.from()` works
+similarly for multi-value sources such as `Iterable`, `Iterator`, `AsyncChannel`, or `Flow.Publisher`.
+
+=== Built-in Adapters
+
+The `groovy.concurrent.AwaitableAdapterRegistry` allows extending `await` to support additional
+asynchronous types from third-party frameworks. The built-in adapter handles:
+
+* `groovy.concurrent.Awaitable` (native Groovy promise — passthrough)
+* `java.util.concurrent.CompletableFuture` and `CompletionStage`
+* `java.util.concurrent.Future` (blocking, delegated to a configurable executor)
+* `groovy.concurrent.AsyncChannel` (automatically converted to `AsyncStream` for `for await`)
+
+Additionally, `java.util.concurrent.Flow.Publisher` support (single-value `await` and
+multi-value `for await`) is provided by an internal runtime adapter
+that is auto-discovered via `ServiceLoader`.
+This decoupled design allows applications to override the default
+reactive-streams bridge with a framework-specific adapter (e.g., one backed by Reactor's
+native scheduler) by registering a higher-priority adapter.
+
+=== Registering a Custom Adapter
+
+Implement the `AwaitableAdapter` interface and register it with the registry. The `register()`
+method returns an `AutoCloseable` handle for deregistration, making it easy to use in tests
+or scoped contexts:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=adapter_registration,indent=0]
+----
+
+=== Framework Integration
+
+Groovy ships dedicated adapter modules for the two most popular JVM reactive frameworks.
+Adding either module to the classpath enables zero-configuration `async`/`await` support
+for that framework's types — no manual adapter registration is needed.
+
+The root async guide stays framework-neutral. Choose an integration module when your
+application boundary already speaks that framework's native types:
+
+[cols="1,1,1,2"]
+|===
+| Module | Single-value types | Streaming types | Typical fit
+
+| Core runtime only
+| `CompletableFuture`, `CompletionStage`, `Future`, `Flow.Publisher`
+| `Flow.Publisher`, `Iterable`, `AsyncChannel`
+| Framework-neutral services, gradual adoption, or applications already centered on JDK concurrency types
+
+| `groovy-reactor`
+| `Mono`
+| `Flux`
+| Reactor-first applications, especially Spring WebFlux stacks
+
+| `groovy-rxjava`
+| `Single`, `Maybe`
+| `Observable`, `Flowable`
+| Applications whose service and client layers already use RxJava 3
+|===
+
+==== Project Reactor (`groovy-reactor`)
+
+Use `groovy-reactor` when your public APIs, repositories, or HTTP boundaries already
+expose `Mono` or `Flux`. The module lets Groovy async code consume those types directly
+without rewriting the surrounding application model.
+
+For verified examples covering operators, Spring WebFlux integration, and `Awaitable`
+combinators, see the <<reactor-userguide,Project Reactor Integration>> guide.
+
+==== RxJava 3 (`groovy-rxjava`)
+
+Use `groovy-rxjava` when your application already standardizes on RxJava 3 and you want
+to introduce Groovy async/await at orchestration boundaries without replacing `Single`,
+`Maybe`, `Observable`, or `Flowable` in the rest of the codebase.
+
+For verified examples covering `Maybe`, `Flowable`, back-pressure, and `Awaitable`
+combinators, see the <<rxjava-userguide,RxJava 3 Integration>> guide.
+
+==== Custom Adapters for Other Frameworks
+
+For frameworks not covered by the built-in modules, implement the `AwaitableAdapter` interface
+and delegate to the framework's native blocking or conversion methods:
+
+* **Spring**: Return `Awaitable` from `@Async` service methods; adapt `ListenableFuture` via `toCompletableFuture()`
+* **Vert.x**: Adapt `io.vertx.core.Future` via `toCompletionStage()`
+
+=== Automatic Discovery
+
+Adapters are discovered automatically via `java.util.ServiceLoader` by placing a
+provider configuration file at `META-INF/services/groovy.concurrent.AwaitableAdapter`.
+The `groovy-reactor` and `groovy-rxjava` modules use this mechanism — adding them to the
+classpath is sufficient for transparent integration.
+
+SPI-discovered adapters are loaded at class-initialization time and take priority over
+the built-in adapters. Runtime-registered adapters (via `register()`) take highest
+priority, allowing application code to override any default.
+
+[[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.
+
+=== Custom Executor via `@Async`
+
+By default, `@Async` methods use the global async executor (virtual threads on JDK 21+, or
+the configurable thread pool on earlier JDKs).  You can route specific methods to a dedicated
+executor by naming a field in the declaring class:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_annotation_executor,indent=0]
+----
+
+The `executor` parameter must refer to a field of type `java.util.concurrent.Executor`
+(or its subtype).  This is particularly useful when different methods have different
+threading requirements — for example, CPU-bound work on a fixed pool and I/O-bound work
+on virtual threads.
+
+[[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**, Groovy uses a managed executor whose parallelism can be tuned via
+the `groovy.async.parallelism` system property. The important user-visible contract is that
+async work remains bounded and continues to apply back-pressure instead of silently dropping tasks.
+
+=== 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.orTimeoutMillis()` (or the instance form `orTimeoutMillis()`) to apply a
+deadline without dropping back to manual race logic:
+
+[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)` 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]
+----
+
+[[inspection]]
+== Inspecting Awaitable State
+
+An `Awaitable` provides non-blocking inspection methods for checking its state without
+blocking the calling thread:
+
+* `isDone()` — `true` when the awaitable has completed (successfully, exceptionally, or via cancellation)
+* `isCancelled()` — `true` when the awaitable was cancelled via `cancel()`
+* `isCompletedExceptionally()` — `true` when the awaitable completed with an exception (including cancellation)
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=inspection_methods,indent=0]
+----
+
+[[common-pitfalls]]
+== Troubleshooting & Common Pitfalls
+
+This section catalogues the most frequently encountered issues when using async/await
+and explains how to avoid or resolve them.
+
+[[pitfall-blocking-get]]
+=== Calling `.get()` Inside an `async` Method
+
+Calling `get()` or `join()` on a `CompletableFuture` inside an async body defeats the
+purpose of async/await: it blocks the current async thread instead of suspending at an
+`await` boundary, which can lead to poorer throughput and harder-to-reason-about scheduling.
+
+[cols="1,1"]
+|===
+| ❌ Anti-pattern | ✅ Correct
+
+a|
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=pitfall_blocking_get_bad,indent=0]
+----
+
+a|
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=pitfall_blocking_get_good,indent=0]
+----
+|===
+
+[[pitfall-missing-await]]
+=== Forgetting `await` — Fire-and-Forget
+
+An `async` method returns an `Awaitable` immediately. If you discard that handle, you
+also discard your structured way to await completion, observe failure, cancel the task,
+or compose it with other work.
+
+[cols="1,1"]
+|===
+| ❌ Anti-pattern | ✅ Correct
+
+a|
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=pitfall_missing_await_bad,indent=0]
+----
+
+a|
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=pitfall_missing_await_good,indent=0]
+----
+|===
+
+[TIP]
+====
+If a task is intentionally detached, record that decision explicitly — for example by
+storing the returned `Awaitable`, attaching logging/recovery callbacks, or launching it
+inside a scope whose lifecycle you control.
+====
+
+[[pitfall-null-in-channel]]
+=== Sending `null` Through a Channel
+
+`AsyncChannel.send(null)` throws a `NullPointerException`.  If you need to signal the
+absence of a value, either close the channel or send a sentinel wrapper:
+
+[cols="1,1"]
+|===
+| ❌ Anti-pattern | ✅ Correct
+
+a|
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=pitfall_null_channel_bad,indent=0]
+----
+
+a|
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=pitfall_null_channel_good,indent=0]
+----
+|===
+
+[[pitfall-timeout-semantics]]
+=== Timeout Is Clock-Based, Not CPU-Based
+
+`orTimeoutMillis(ms)` starts a wall-clock timer from the moment it is called, *not*
+from when the async body begins executing.  If the `Awaitable` has already been running
+for a while before you attach the timeout, the effective deadline is shorter than `ms`
+milliseconds of execution time.  For precise control, capture the awaitable at creation
+and attach the timeout immediately:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=timeout_pattern,indent=0]
+----
+
+[[pitfall-scope-cancellation]]
+=== Scope Cancellation and Uncooperative Tasks
+
+Scope cancellation is cooperative. Child tasks should be written so they can reach a
+cancellation point promptly instead of running forever after the surrounding scope has
+already failed or been closed.
+
+In practice, that means:
+
+* let `InterruptedException` propagate unless you are translating it deliberately;
+* avoid broad catch blocks that accidentally turn cancellation into success;
+* break long CPU loops or polling loops into chunks so they can observe cancellation;
+* prefer blocking APIs that respond to interruption or have their own timeout support.
+
+[[async-modifier-restrictions]]
+== `async` Modifier Restrictions
+
+The `async` keyword is valid only on **method declarations** (including script-level methods)
+and **closure/lambda expressions**. Applying `async` to other declarations is a compile-time error:
+
+* **Class/interface declarations**: `async class Foo {}` → parser error
+* **Field/variable declarations**: `async def x = 1` → compilation error ("async not allowed for variable declarations")
+* **Constructor declarations**: `async Foo() {}` → compilation error ("async not allowed for constructors")
+
+[[implementation-details]]
+== Design Notes
+
+This section captures the stable design ideas behind Groovy's async/await support.
+It intentionally focuses on semantics and responsibilities rather than internal helper
+classes, so it remains useful as the implementation evolves.
+
+[[implementation-strategy]]
+=== Execution Model
+
+Groovy preserves the ordinary control-flow model of the language: `async` methods still
+look and behave like Groovy methods, `await` reads like an expression, and `try`/`catch`,
+loops, and local variables keep their familiar meaning.
+
+The runtime executes async bodies on JVM threads. On JDK 21+, this maps naturally to
+virtual threads; on JDK 17–20, it maps to a managed executor. The key
+user-visible consequences are stable across both strategies:
+
+* stack traces and debugging stay close to ordinary Groovy code;
+* blocking at an `await` boundary is handled by the runtime, not by callback plumbing;
+* existing JVM async types can be adapted without forcing user code into one framework.
+
+[[architecture]]
+=== Compiler and Runtime Responsibilities
+
+[cols="1,3"]
+|===
+| Layer | Responsibility
+
+| Parser and compiler
+| Recognize `async`, `await`, `defer`, `for await`, and `yield return`; validate where
+they are legal; and rewrite declarations and control flow into the async runtime model.
+
+| Runtime
+| Schedule async bodies, coordinate waits and timeouts, bridge supported async types,
+manage stream cleanup, and preserve context and scope across async boundaries.
+|===
+
+[[thread-safety]]
+=== Behavioural Guarantees
+
+[cols="2,3"]
+|===
+| Area | Guarantee
+
+| Exception propagation
+| `await` unwraps wrapper exceptions and rethrows the original failure type whenever possible.
+
+| Stream lifecycle
+| `for await` consumes values at the pace of the consumer and closes the source automatically
+when the loop exits early.
+
+| Structured concurrency
+| Child tasks launched inside an `AsyncScope` are joined or cancelled before the scope exits.
+
+| Context propagation
+| Tasks and continuations see the async context that was active when they were created or registered.
+
+| Interoperability
+| External async frameworks integrate through public abstractions such as `Awaitable`,
+`AsyncStream`, and the adapter registry rather than leaking framework-specific mechanics into user code.
+|===
+
+These guarantees are the part of the design application code should rely on. The exact
+runtime strategy behind them may evolve, but the user-facing model is intended to remain stable.
+
+[[cross-language-comparison]]
+== Cross-Language Comparison
+
+The following table compares Groovy's async/await with the corresponding features in
+JavaScript, C#, Kotlin, and Swift, for developers familiar with those languages.
+
+[cols="2,3,3,3,3,3"]
+|===
+| Feature | Groovy | JavaScript | C# | Kotlin | Swift
+
+| **Async declaration**
+| `async foo() { }` / `async returnType foo() { }`
+| `async function foo() { }`
+| `async Task<T> Foo() { }`
+| `suspend fun foo(): T`
+| `func foo() async throws -> T`
+
+| **Await expression**
+| `await expr` / `await(expr)` / `await(a, b, c)`
+| `await expr`
+| `await expr`
+| `deferred.await()` / suspend call
+| `try await expr`
+
+| **Return type**
+| `Awaitable<T>`
+| `Promise<T>`
+| `Task<T>` / `ValueTask<T>`
+| `Deferred<T>` / suspended `T`
+| `Task<T, Error>` / `async` function returns `T`
+
+| **Async closure/lambda**
+| `async { \-> body }` / `async () \-> { body }`
+| `async () \=> body`
+| `async () \=> body`
+| `scope.async { body }`
+| `Task { body }`
+
+| **Async iteration**
+| `for await (x in src) { }` +
+_(supports `AsyncStream`, `Flow.Publisher`, `AsyncChannel`, `Iterable`, Reactor `Flux`, RxJava `Observable`/`Flowable`)_
+| `for await (x of src) { }`
+| `await foreach (x in src) { }`
+| _(manual via `Flow`, `AsyncChannel`, or `Flow.collect`)_
+| `for try await x in seq`
+
+| **Channels**
+| `AsyncChannel.create(n)` +
+`await ch.send(x)` / `await ch.receive()` +
+`for await (x in ch) { }`
+| _(none; use streams or message ports)_
+| `System.Threading.Channels` +
+`AsyncChannel<T>` / `ChannelReader.ReadAllAsync()`
+| `AsyncChannel<T>` / `channel.send()` +
+`for (x in channel) { }`
+| `AsyncStream` / `AsyncChannel` (3rd party)
+
+| **Async generator**
+| `yield return expr`
+| `yield expr` (in `async function*`)
+| `yield return expr` (in `IAsyncEnumerable<T>`)
+| `flow { emit(x) }` / channel producers
+| `AsyncStream { continuation.yield(x) }`
+
+| **Deferred cleanup**
+| `defer { cleanup }`
+| _(none; use `try`/`finally`)_
+| `await using` / `try`/`finally`
+| `try`/`finally` / `use`
+| `defer`
+
+| **Wait all (fail-fast)**
+| `Awaitable.all(a, b, c)`
+| `Promise.all([a, b, c])`
+| `Task.WhenAll(a, b, c)`
+| `awaitAll(a, b, c)`
+| `async let` / `withTaskGroup`
+
+| **Wait any**
+| `Awaitable.any(a, b, c)`
+| `Promise.any([a, b, c])`
+| `Task.WhenAny(a, b, c)`
+| `select {}` / manual race
+| `TaskGroup.next()`
+
+| **Wait all settled**
+| `Awaitable.allSettled(a, b, c)`
+| `Promise.allSettled([a, b, c])`
+| _(manual; no built-in equivalent)_
+| _(manual; typically `runCatching`)_
+| _(manual; typically `Result`)_
+
+| **Pre-computed result**
+| `Awaitable.of(value)`
+| `Promise.resolve(value)`
+| `Task.FromResult(value)`
+| `CompletableDeferred(value)` / `async { value }`
+| `Task { value }`
+
+| **Pre-computed error**
+| `Awaitable.failed(error)`
+| `Promise.reject(error)`
+| `Task.FromException(ex)`
+| `CompletableDeferred<T>().completeExceptionally(ex)`
+| `Task { throw error }`
+
+| **Delay**
+| `Awaitable.delay(ms)`
+| `new Promise(r => setTimeout(r, ms))`
+| `Task.Delay(ms)`
+| `delay(ms)`
+| `Task.sleep(for:)`
+
+| **Timeout**
+| `Awaitable.orTimeoutMillis(task, ms)` / `task.orTimeoutMillis(ms)`
+| `Promise.race([...])` / `AbortSignal.timeout()`
+| `task.WaitAsync(timeout)` / `CancelAfter`
+| `withTimeout(ms) { ... }`
+| _(manual race with `TaskGroup` / sleep)_
+
+| **Cancellation**
+| `awaitable.cancel()`
+| `AbortController.abort()`
+| `CancellationToken`
+| `Job.cancel()`
+| `Task.cancel()`
+
+| **Executor/scheduler**
+| `Awaitable.setExecutor(exec)`
+| _(event loop; not configurable)_
+| `TaskScheduler`
+| `CoroutineDispatcher`
+| task priority / executor chosen by runtime
+
+| **Third-party support**
+| `AwaitableAdapterRegistry` SPI +
+`Awaitable.from()` / `AsyncStream.from()` +
+Built-in modules: `groovy-reactor`, `groovy-rxjava`
+| _(native `thenable` protocol)_
+| Custom awaiters via `GetAwaiter()`
+| coroutine adapters / bridges
+| `AsyncSequence` / `Task` interop patterns
+
+| **Annotation form**
+| `@Async`
+| _(none)_
+| _(none; attribute-driven async not supported)_
+| _(none)_
+| _(none)_
+
+| **Implementation**
+| Thread-per-task (virtual threads on JDK 21+)
+| Event loop + microtask queue
+| State machine (compiled IL transformation)
+| Coroutine state machine
+| Compiler-generated async state machine
+|===
+
+[[structured-concurrency]]
+== Structured Concurrency with `AsyncScope`
+
+=== The Problem: Uncontrolled Task Lifetimes
+
+When you launch async tasks with `async { ... }`, each task runs independently — it can
+outlive the method that created it, leak resources if it is never awaited, or silently
+swallow exceptions if nobody observes the result.  This "fire-and-forget" model is
+convenient for simple cases but becomes a liability in production systems where every task
+must be accounted for.
+
+=== The Solution: Scoped Task Ownership
+
+`AsyncScope` is an interface in `groovy.concurrent` that binds the lifetime of child
+tasks to a well-defined scope.  Inspired by
+Kotlin's `coroutineScope`, Swift's `TaskGroup`, and
+{jdk}/java.base/java/util/concurrent/StructuredTaskScope.html[JEP 453 (Structured Concurrency)],
+it enforces a simple invariant: **when the scope exits, all child tasks have completed or
+been cancelled**.
+
+Create a scope through the interface factories — `AsyncScope.create()`,
+`AsyncScope.create(executor)`, or `AsyncScope.create(executor, failFast)` — or use the
+recommended `AsyncScope.withScope { ... }` convenience form. This interface-based design
+keeps application code tied to the public contract rather than any particular runtime class.
+
+`AsyncScope` integrates naturally with `async` methods and `await` expressions.  You
+can call `async` methods inside `scope.async { ... }` blocks and `await` their results —
+just as you would in any other async context.  The `withScope` convenience method creates
+a scope, executes a closure within it, and automatically closes the scope when the closure
+returns:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_scope_basic,indent=0]
+----
+
+In the example above, `fetchUser` and `fetchOrders` are ordinary `async` methods.
+`scope.async` launches each call as a child task whose lifetime is bound to the scope.
+The `await` expressions inside child tasks suspend until their respective async operations
+complete, and `await userTask` / `await ordersTask` suspend the parent until the scoped
+tasks deliver their results.  When `withScope` returns, both tasks are guaranteed to have
+completed — no orphan tasks can leak.
+
+=== Fail-Fast Semantics
+
+By default, `AsyncScope` uses a _fail-fast_ policy: when any child task throws an
+exception, all sibling tasks are immediately cancelled.  The first failure becomes the
+primary exception; subsequent failures are attached as
+{jdk}/java.base/java/lang/Throwable.html#addSuppressed(java.lang.Throwable)[suppressed exceptions].
+
+This prevents healthy tasks from continuing to consume resources after a sibling has
+already doomed the overall operation.  Combined with `async` / `await`, failures in any
+child propagate cleanly through the scope:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_scope_fail_fast,indent=0]
+----
+
+When `failingTask` throws, the scope cancels `slowTask` immediately — there is no need
+to wait 5 seconds for it to finish or to add manual cancellation logic.
+
+=== Fan-Out / Fan-In
+
+A common concurrency pattern is _fan-out / fan-in_: launch a dynamic number of tasks
+in parallel and collect all results.  `AsyncScope` makes this safe — every task is bound
+to the scope, so even if one fails, all siblings are cancelled before the scope exits:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_scope_fanout,indent=0]
+----
+
+The `collect` call inside `withScope` creates one scoped child task per URL.  All pages
+are fetched concurrently, and if any single fetch fails, the remaining fetches are
+cancelled automatically.
+
+=== Manual Scope Management
+
+For advanced use cases — such as scopes that span multiple method calls or integration
+with resource managers — you can create and manage scopes directly.  `AsyncScope`
+implements `AutoCloseable`, so it works with Groovy's resource-management idioms:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_scope_manual,indent=0]
+----
+
+[IMPORTANT]
+====
+Always close a scope.  An unclosed scope leaves child tasks in an indeterminate state
+and may leak threads.
+====
+
+=== Thread Safety and Memory Efficiency
+
+The `AsyncScope` contract is safe for concurrent use from multiple threads. In particular:
+
+* **`async()` + `close()` cannot race.**  Child tasks are registered in the
+  internal child list _before_ being submitted to the executor (the
+  "register-before-submit" protocol).  This guarantees that `close()` always
+  observes — and therefore joins or cancels — every submitted child, even
+  under aggressive concurrent scheduling.  Once `close()` marks the scope as
+  closed, no further children can be registered.  Any concurrent `async()` call
+  that arrives after the flag is set receives an `IllegalStateException`.
+* **`cancelAll()` is idempotent** and safe to call concurrently with `close()`.
+
+Implementations are free to discard bookkeeping for completed children once it is no
+longer needed, keeping long-lived scopes practical even when they spawn many short-lived tasks.
+The runtime also propagates the current scope into child tasks automatically, so nested
+async work inherits the correct structured-concurrency boundary.
+
+[[async-context]]
+== Async Execution Context with `AsyncContext`
+
+Asynchronous code frequently needs more than just a return value: it must also
+carry request IDs, tenant IDs, security information, locale overrides, or
+trace metadata through thread hops.  `AsyncContext` — an interface in
+`groovy.concurrent` — provides a small, explicit, async-local map for exactly
+that purpose. Create a context through factory methods such as
+`AsyncContext.create()` and `AsyncContext.create(initialValues)`.
+
+Groovy captures the current `AsyncContext` when a task or continuation is
+scheduled and restores that snapshot when it runs.  This gives you the most
+useful production invariant:
+
+* child tasks inherit the parent context as it existed at spawn time;
+* child-side mutations do not leak back into the parent;
+* continuations (`.then()`, `.exceptionally()`, `.handle()`, etc.) see the
+  context that was active when they were registered.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_context_basic,indent=0]
+----
+
+=== Snapshots and Manual Restoration
+
+`AsyncContext.capture()` returns an immutable `Snapshot` of the current context.
+You can restore that snapshot later — even from a different thread — using
+`AsyncContext.withSnapshot(snapshot) { ... }`.  Snapshots are lightweight: they
+are simply frozen copies of the context map at capture time.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=async_context_snapshot,indent=0]
+----
+
+=== Convenience Methods
+
+`AsyncContext` provides Map-like convenience methods for common patterns:
+
+* **`getOrDefault(key, defaultValue)`** — returns the value for a key or a
+  fallback, mirroring `Map.getOrDefault()`.
+* **`computeIfAbsent(key, function)`** — lazily computes and stores a value on
+  first access, mirroring `Map.computeIfAbsent()`.
+
+`AsyncContext.Snapshot` also provides `getOrDefault(key, defaultValue)` for
+reading snapshot entries with a fallback.
+
+[TIP]
+====
+Use `AsyncContext` for small pieces of execution metadata — correlation IDs,
+tenant IDs, principals, feature flags.  Keep large mutable business objects in
+ordinary method parameters or return values.
+====
+
+[NOTE]
+====
+The async runtime captures and restores context automatically for Groovy-managed tasks
+and continuations. If you hand work off to threads or executors outside that runtime,
+capture a snapshot first and restore it explicitly with
+`AsyncContext.withSnapshot(snapshot) { ... }`.
+====
+
+[[go-tasks-and-channels]]
+== Go-Style Tasks and Channels
+
+Groovy's core async model is based on `Awaitable`, but it also provides
+Go-inspired coordination primitives for workflows that benefit from explicit
+message passing.
+
+=== Spawning Tasks with `go`
+
+`Awaitable.go { ... }` starts a lightweight task.  Outside an `AsyncScope` it
+behaves like a detached async job.  Inside an `AsyncScope`, it automatically
+joins the current scope so the task is awaited and cancelled with its siblings.
+
+=== Channels
+
+`AsyncChannel<T>` is a CSP (Communicating Sequential Processes) style interface
+for inter-task communication.  `AsyncChannel.create()` creates an unbuffered
+rendezvous channel; `AsyncChannel.create(n)` creates a buffered channel with
+capacity `n`.  Both `send()` and `receive()` return `Awaitable` values, which
+means they compose naturally with `await` and with `Awaitable.any(...)` for
+first-completion-wins logic.
+
+Key channel properties:
+
+* **Unbuffered (capacity 0)**: `send()` suspends until a matching `receive()`
+  arrives, providing direct handoff rendezvous semantics.
+* **Buffered (capacity > 0)**: `send()` completes immediately if buffer space is
+  available; otherwise it suspends until a consumer drains an element.
+* **FIFO ordering**: values are always delivered in the order they were sent.
+* **Close semantics**: closing a channel is idempotent.  Buffered values remain
+  receivable after close.  Once drained, further receives fail with
+  `ChannelClosedException`.  Pending senders fail immediately on close.
+* **Null rejection**: channels reject `null` payloads to maintain a clean
+  distinction between "no value yet" and actual data.
+* **Thread safety**: all operations are safe for concurrent use by multiple
+  threads.
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=go_channel_select,indent=0]
+----
+
+=== Racing Channel Operations
+
+Use `Awaitable.any(...)` to race channel receives against timeouts or other
+channels — the first completed source wins:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=channel_racing,indent=0]
+----
+
+Because `Awaitable.any` accepts any awaitable source, it works seamlessly with
+channels, timers, and arbitrary async tasks — serving a similar role to Go's
+`select` statement.
+
+=== Iterating Channels with `for await`
+
+Because `AsyncChannel` integrates with the <<for_await_section,`for await`>> syntax,
+you can iterate received values in a natural loop — the Groovy equivalent of Go's
+`for range ch`:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=for_await_channel,indent=0]
+----
+
+Internally, `for await` obtains a read-only `AsyncStream` view from the channel
+(via `AsyncChannel.asStream()`).  Each iteration calls `receive()`; when the channel is
+closed and all buffered values are drained, the stream signals completion and the
+loop exits.
+
+[IMPORTANT]
+====
+The stream view does **not** own the channel.  Exiting the loop — whether
+normally, via `break`, or on exception — does _not_ close the channel.  The
+producer is responsible for closing the channel when it has finished sending.
+This matches Go's convention that only the _sender_ closes a channel.
+====
+
+=== Fan-In Pipelines
+
+Channels are particularly effective for fan-in and fan-out pipelines where
+multiple producers feed a shared channel and one or more consumers drain it:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=channel_fan_in,indent=0]
+----
+
+This pattern is useful for work distribution, aggregating results from
+parallel workers, and building actor-like coordination without explicit locks.
+
+=== Channels vs `Flow.Publisher`
+
+Channels and `Flow.Publisher` both carry asynchronous data, and both work with
+<<for_await_section,`for await`>>.  However, they serve **different purposes** and
+should _not_ be confused or substituted for each other:
+
+[cols="1,2,2"]
+|===
+| Aspect | AsyncChannel | `Flow.Publisher`
+
+| **Model**
+| Bidirectional send/receive (CSP)
+| Unidirectional push (Reactive Streams)
+
+| **Backpressure**
+| Implicit — sender waits when the buffer is full
+| Explicit — subscriber signals demand via `request(n)`
+
+| **Direction**
+| Symmetric — both sides actively write and read
+| Asymmetric — publisher pushes, subscriber pulls demand
+
+| **Close semantics**
+| Idempotent; buffered values survive close and drain first
+| Terminal `onComplete`/`onError` signals after all items
+
+| **Multi-consumer**
+| Competing — each `receive()` is exclusive (only one consumer gets the value)
+| Copied — each subscriber gets its own stream of items
+
+| **Best for**
+| Task-to-task coordination, fan-in/fan-out pipelines
+| Consuming reactive streams, HTTP bodies, database cursors
+|===
+
+Despite these differences, **both are consumed through the same `for await` syntax**
+via the unified `AsyncStream` abstraction:
+
+[source,groovy]
+----
+include::../test/AsyncAwaitSpecTest.groovy[tags=channel_vs_flow_for_await,indent=0]
+----
+
+TIP: Use **channels** when you control both the producer and consumer and need
+explicit coordination (e.g., work queues, pipelines, actor-style messaging).
+Use **`Flow.Publisher`** when you receive a push-based data stream from an
+external source (e.g., a reactive HTTP client, a message broker, or a database
+cursor).
+
+[[performance-notes]]
+== Performance Characteristics
+
+=== Adapter Registry: Amortized O(1) Lookups
+
+Every `await` expression resolves the awaited object's type to a matching
+`AwaitableAdapter` via the `AwaitableAdapterRegistry`.  The registry uses
+{jdk}/java.base/java/lang/ClassValue.html[`ClassValue`]-based per-class memoization,
+reducing adapter resolution from O(n) (linear scan of registered adapters) to amortized
+O(1) after the first lookup for each class.  The cache is automatically invalidated when
+adapters are registered or unregistered at runtime.
+
+=== Synchronous Completion Fast-Path
+
+When an `Awaitable` wraps a `CompletableFuture` that has already completed by the time
+`get()` is called, the runtime extracts the result via `CompletableFuture.join()` instead
+of the full interruptible `get()` path.  This bypasses the park/unpark machinery and
+eliminates unnecessary thread state transitions — a measurable win when async operations
+complete before being awaited, which is common for cached or pre-computed results.
+
+This optimisation is analogous to C#'s
+https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1[`ValueTask<T>`]
+synchronous completion path, where already-completed tasks skip the state machine
+continuation entirely.
+
+=== Virtual Threads (JDK 21+)
+
+On JDK 21+, Groovy automatically uses
+{jdk}/java.base/java/lang/Thread.html#ofVirtual()[virtual threads] for async execution.
+Virtual threads are extremely lightweight — a few hundred bytes of stack compared to
+~1 MB for platform threads — and are scheduled by the JVM's M:N scheduler onto a small
+pool of carrier threads.  The `await` keyword blocks the virtual thread (not the carrier),
+so the JVM can multiplex hundreds of thousands of concurrent async operations onto a
+handful of OS threads.
+
+On JDK 17–20, the runtime falls back to a bounded cached thread pool (default
+256 threads, configurable via the `groovy.async.parallelism` system property) with a
+caller-runs back-pressure policy.
+
+[[api-quick-reference]]
+== API Quick Reference
+
+This section provides a compact method-level reference for the core async/await types.
+For full javadoc, see the API documentation for the `groovy.concurrent` package.
+
+=== `Awaitable<T>` — Core Async Abstraction
+
+[cols="3,4"]
+|===
+| Method | Description
+
+| `get()` / `get(timeout, unit)`
+| Blocks until the result is available (used internally by `await`)
+
+| `then(closure)` / `thenAsync(closure)`
+| Chains a synchronous / asynchronous transformation
+
+| `orTimeoutMillis(ms)`
+| Fails with `TimeoutException` if not complete within `ms`
+
+| `completeOnTimeoutMillis(fallback, ms)`
+| Completes with `fallback` if not done within `ms`
+
+| `cancel()` / `cancel(mayInterrupt)`
+| Cancels the computation
+
+| `isDone()` / `isCancelled()` / `isCompletedExceptionally()`
+| Non-blocking state inspection
+
+| `toCompletableFuture()`
+| Unwraps to the underlying `CompletableFuture`
+|===
+
+**Static methods on `Awaitable`:**
+
+[cols="3,4"]
+|===
+| Method | Description
+
+| `of(value)` / `failed(error)`
+| Pre-computed success / failure (no thread spawned)
+
+| `from(source)`
+| Adapts `CompletableFuture`, `Future`, `Flow.Publisher`, etc.
+
+| `all(a, b, ...)`
+| Parallel wait; fail fast on the first failure
+
+| `allSettled(a, b, ...)`
+| Parallel wait; collect every outcome
+
+| `any(a, b, ...)`
+| Returns the first completed result; cancels the rest
+
+| `delay(millis)`
+| Creates an awaitable that completes after a delay
+
+| `go(closure)`
+| Spawns a lightweight task (scope-aware)
+
+| `channel()` / `channel(capacity)`
+| Creates an `AsyncChannel`
+
+| `getExecutor()` / `setExecutor(executor)`
+| Query / override the async executor
+
+| `isVirtualThreadsAvailable()`
+| `true` on JDK 21+
+|===
+
+=== `AsyncStream<T>` — Async Iteration
+
+[cols="3,4"]
+|===
+| Method | Description
+
+| `hasNext()` → `Awaitable<Boolean>`
+| Asynchronously checks for the next element
+
+| `next()` → `Awaitable<T>`
+| Retrieves the next element
+
+| `from(source)`
+| Adapts `Flow.Publisher`, `AsyncChannel`, or custom types
+|===
+
+=== `AsyncChannel<T>` — Go-Style Communication
+
+`AsyncChannel<T>` is an interface that defines asynchronous inter-task
+communication with optional buffering, following the
+https://en.wikipedia.org/wiki/Communicating_sequential_processes[CSP]
+(Communicating Sequential Processes) paradigm.  It is the Groovy equivalent
+of Go's channels and Kotlin's `Channel` type.
+
+Instances are created via static factory methods on the interface itself,
+following the same convention as `List.of()` and C#'s
+`Channel.CreateBounded<T>()`:
+
+[cols="3,4"]
+|===
+| Method | Description
+
+| `AsyncChannel.create()` _(static)_
+| Creates an unbuffered (rendezvous) channel with capacity `0`
+
+| `AsyncChannel.create(capacity)` _(static)_
+| Creates a buffered channel with the given capacity
+
+| `getCapacity()` → `int`
+| Returns the buffer capacity ({nbsp}`0` = unbuffered / rendezvous)
+
+| `getBufferedSize()` → `int`
+| Returns the number of elements currently in the buffer (snapshot)
+
+| `isClosed()` → `boolean`
+| Returns `true` if the channel has been closed
+
+| `send(value)` → `Awaitable<Void>`
+| Sends a value (suspends if buffer is full); rejects `null`
+
+| `receive()` → `Awaitable<T>`
+| Receives the next value in FIFO order (suspends if no value is available)
+
+| `close()` → `boolean`
+| Closes the channel; returns `true` on the first close, `false` thereafter.
+  Buffered values remain receivable; pending senders receive `ChannelClosedException`
+
+| `asStream()` → `AsyncStream<T>`
+| Exposes the channel as a read-only `AsyncStream` for `for await`.
+  The stream view does **not** own the channel
+|===
+
+=== `AsyncScope` — Structured Concurrency
+
+`AsyncScope` is an **interface** in `groovy.concurrent`; its default implementation
+(`DefaultAsyncScope`) resides in `org.apache.groovy.runtime.async`.  This separation
+allows user code to program against a stable contract while the runtime implementation
+can evolve independently.
+
+[cols="3,4"]
+|===
+| Method | Description
+
+| `create()` / `create(executor)` / `create(executor, failFast)` _(static)_
+| Creates a new scope via factory methods
+
+| `withScope(closure)` _(static)_
+| Opens a scope; waits for all child tasks on exit
+
+| `withCurrent(scope, supplier)` _(static)_
+| Executes code with the given scope installed as the current scope
+
+| `current()` _(static)_
+| Returns the currently active scope, or `null`
+
+| `async(closure)` / `async(supplier)`
+| Spawns a scoped child task (Closure for Groovy, Supplier for Java interop)
+
+| `getChildCount()`
+| Returns the number of tracked child tasks
+
+| `cancelAll()`
+| Cancels remaining tasks in the scope
+
+| `close()`
+| Waits for all children and closes the scope (idempotent)
+|===
+
+=== `AsyncContext` — Execution-Scoped Metadata
+
+`AsyncContext` is an **interface** in `groovy.concurrent`; its default implementation
+(`DefaultAsyncContext`) resides in `org.apache.groovy.runtime.async`.  `AsyncContext.Snapshot`
+is likewise an interface, with the default implementation provided internally.
+
+[cols="3,4"]
+|===
+| Method | Description
+
+| `create()` / `create(initialValues)` _(static)_
+| Creates a new context via factory methods
+
+| `current()` _(static)_
+| Returns the live context associated with the current thread
+
+| `capture()` _(static)_
+| Captures an immutable `Snapshot` of the current context
+
+| `withSnapshot(snapshot, supplier)` _(static)_
+| Executes code with a snapshot installed as the current context
+
+| `with(entries, closure)` _(static)_
+| Temporarily overlays entries on the current context for the closure's duration
+
+| `put(key, value)` / `get(key)` / `remove(key)`
+| Store / retrieve / remove context entries
+
+| `getOrDefault(key, defaultValue)`
+| Returns the value for a key, or the default if absent
+
+| `computeIfAbsent(key, function)`
+| Lazily computes and stores a value if the key is absent
+
+| `putAll(entries)` / `clear()`
+| Bulk operations on context entries
+
+| `containsKey(key)` / `size()` / `isEmpty()`
+| Query operations
+
+| `snapshot()`
+| Returns an unmodifiable `Map` copy of the entries
+
+| `Snapshot.of(map)` _(static)_
+| Creates a snapshot from a map (defensively copied)
+
+| `Snapshot.get(key)` / `Snapshot.getOrDefault(key, default)`
+| Query a snapshot
+
+| `Snapshot.with(entries)`
+| Creates a new snapshot by merging entries
+
+| `Snapshot.asMap()`
+| Returns the snapshot entries as an unmodifiable map
+|===
+
+[[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
... 28032 lines suppressed ...